📄 lion-tut-c03.htm
字号:
.BREAK .IF (!eax) <br>
invoke TranslateMessage, ADDR msg <br>
invoke DispatchMessage, ADDR msg <br>
.ENDW <br>
mov eax,msg.wParam
; return exit code in eax <br>
ret <br>
WinMain endp </font>
<p><font color="#006666">WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
<br>
.IF uMsg==WM_DESTROY
; if the user closes our window <br>
invoke PostQuitMessage,NULL
; quit our application <br>
.ELSE <br>
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
; Default message processing <br>
ret <br>
.ENDIF <br>
xor eax,eax <br>
ret <br>
WndProc endp </font>
<p><font color="#006666">end start </font>
<p><font color="#FF0000">分析:</font></p>
<p> 看到一个简单的 Windows 程序有这么多行,您是不是有点想撤? 但是您必须要知道的是上面的大多数代码都是模板而已,模板的意思即是指这些代码对差不多所有标准
Windows 程序来说都是相同的。在写 Windows 程序时您可以把这些代码拷来拷去,当然把这些重复的代码写到一个库中也挺好。其实真正要写的代码集中在
WinMain 中。这和一些 C 编译器一样,无须要关心其它杂务,集中精力于 WinMain 函数。唯一不同的是 C 编译器要求您的源代码有必须有一个函数叫
WinMain。否则 C 无法知道将哪个函数和有关的前后代码链接。相对C,汇编语言提供了较大的灵活性,它不强行要求一个叫 WinMain 的函数。</p>
<p> 下面我们开始分析,您可得做好思想准备,这可不是一件太轻松的活。</p>
<p> .386<br>
.model flat,stdcall<br>
option casemap:none<br>
<br>
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD<br>
<br>
include \masm32\include\windows.inc<br>
include \masm32\include\user32.inc<br>
include \masm32\include\kernel32.inc<br>
includelib \masm32\lib\user32.lib<br>
includelib \masm32\lib\kernel32.lib <br>
<br>
您可以把前三行看成是"必须"的.<br>
<br>
.386告诉MASN我们要用80386指令集。<br>
. model flat,stdcall告诉MASM 我们用的内存寻址模式,此处也可以加入stdcall告诉MASM我们所用的参数传递约定。</p>
<p> 接下来是函数 WinMain 的原型申明,因为我们稍后要用到该函数,故必须先声明。我们必须包含 window.inc 文件,因为其中包含大量要用到的常量和结构的定义,该文件是一个文本文件,您可以用任何文本编辑器打开它,
window.inc还没有包含所有的常量和结构定义,不过 hutch 和我一直在不断加入新的内容。如果暂时在 window.inc 找不到,您也可以自行加入。</p>
<p>我们的程序调用驻扎在 user32.dll (譬如:CreateWindowEx, RegisterWindowClassEx) 和 kernel32.dll
(ExitProcess)中的函数,所以必须链接这两个库。接下来我如果问:您需要把什么库链入您的程序呢 ? 答案是:先查到您要调用的函数在什么库中,然后包含进来。譬如:若您要调用的函数在
gdi32.dll 中,您就要包含gdi32.inc头文件。和 MASM 相比,TASM 则要简单得多,您只要引入一个库,即:import32.lib。<译者注:但
Tasm5 麻烦的是 windows.inc 非常的不全面,而且如果在 Windows.inc 中包含全部的 API 定义会内存不够,所以每次你得把用到的
API 定义拷贝出来></p>
<p>.DATA<br>
<br>
ClassName db "SimpleWinClass",0 <br>
AppName db "Our First Window",0<br>
<br>
.DATA?<br>
<br>
hInstance HINSTANCE ?<br>
CommandLine LPSTR ?<br>
<br>
接下来是DATA"分段"。 在 .DATA 中我们定义了两个以 NULL 结尾的字符串 (ASCIIZ):其中 ClassName 是 Windows
类名,AppName 是我们窗口的名字。这两个变量都是初始化了的。未进行初始化的两个边量放在 .DATA? "分段"中,其中 hInstance 代表应用程序的句柄,CommandLine
保存从命令行传入的参数。HINSTACE 和 LPSTR 是两个数据类型名,它们在头文件中定义,可以看做是 DWORD 的别名,之所以要这么重新定仅是为了易记。您可以查看
windows.inc 文件,在 .DATA? 中的变量都是未经初始化的,这也就是说在程序刚启动时它们的值是什么无关紧要,只不过占有了一块内存,以后可以再利用而已。</p>
<p> .CODE<br>
start:<br>
invoke GetModuleHandle, NULL<br>
mov hInstance,eax<br>
invoke GetCommandLine<br>
mov CommandLine,eax<br>
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT<br>
invoke ExitProcess,eax<br>
.....<br>
end start</p>
<p> .DATA "分段"包含了您应用程序的所有代码,这些代码必须都在 .code 和 end <starting label>之间。至于 label 的命名只要遵从
Windows 规范而且保证唯一则具体叫什么倒是无所谓。我们程序的第一条语句是调用 GetModuleHandle 去查找我们应用程序的句柄。在Win32下,应用程序的句柄和模块的句柄是一样的。您可以把实例句柄看成是您的应用程序的
ID 号。我们在调用几个函数是都把它作为参数来进行传递,所以在一开始便得到并保存它就可以省许多的事。</p>
<p> 特别注意:WIN32下的实例句柄实际上是您应用程序在内存中的线性地址。</p>
<p>WIN32 中函数的函数如果有返回值,那它是通过 eax 寄存器来传递的。其他的值可以通过传递进来的参数地址进行返回。一个 WIN32 函数被调用时总会保存好段寄存器和
ebx,edi,esi和ebp 寄存器,而 ecx和edx 中的值总是不定的,不能在返回是应用。特别注意:从 Windows API 函数中返回后,eax,ecx,edx
中的值和调用前不一定相同。当函数返回时,返回值放在eax中。如果您应用程序中的函数提供给 Windows 调用时,也必须尊守这一点,即在函数入口处保存段寄存器和
ebx,esp,esi,edi 的值并在函数返回时恢复。如果不这样一来的话,您的应用程序很快会崩溃。从您的程序中提供给 Windows 调用的函数大体上有两种:Windows
窗口过程和 Callback 函数。</p>
<p>如果您的应用程序不处理命令行那么就无须调用 GetCommandLine,这里只是告诉您如果要调用应该怎么做。 </p>
<p>下面则是调用WinMain了。该函数共有4个参数:应用程序的实例句柄,该应用程序的前一实例句柄,命令行参数串指针和窗口如何显示。Win32 没有前一实例句柄的概念,所以第二个参数总为0。之所以保留它是为了和
Win16 兼容的考虑,在 Win16下,如果 hPrevInst 是 NULL,则该函数是第一次运行。特别注意:您不用必须申明一个名为 WinMain
函数,事实上在这方面您可以完全作主,您甚至无须有一个和 WinMain 等同的函数。您只要把 WinMain 中的代码拷到GetCommandLine
之后,其所实现的功能完全相同。在 WinMain 返回时,把返回码放到 eax 中。然后在应用程序结束时通过 ExitProcess 函数把该返回码传递给
Windows 。 </p>
<p><b>WinMain proc Inst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD</b>
</p>
<p> 上面是WinMain的定义。注意跟在 proc 指令后的parameter:type形式的参数,它们是由调用者传给 WinMain 的,我们引用是直接用参数名即可。至于压栈和退栈时的平衡堆栈工作由
MASM 在编译时加入相关的前序和后序汇编指令来进行。 LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND
LOCAL 伪指令为局部变量在栈中分配内存空间,所有的 LOCAL 指令必须紧跟在 PROC 之后。LOCAL 后跟声明的变量,其形式是 变量名:变量类型<variable type>。譬如
LOCAL wc:WNDCLASSEX 即是告诉 MASM 为名字叫 wc 的局部边量在栈中分配长度为 WNDCLASSEX 结构体长度的内存空间,然后我们在用该局部变量是无须考虑堆栈的问题,考虑到
DOS 下的汇编,这不能不说是一种恩赐。不过这就要求这样申明的局部变量在函数结束时释放栈空间,(也即不能在函数体外被引用),另一个缺点是您因不能初始化您的局部变量,不得不在稍后另外再对其赋值。</p>
<p> mov wc.cbSize,SIZEOF WNDCLASSEX<br>
mov wc.style, CS_HREDRAW or CS_VREDRAW<br>
mov wc.lpfnWndProc, OFFSET WndProc<br>
mov wc.cbClsExtra,NULL<br>
mov wc.cbWndExtra,NULL<br>
push hInstance<br>
pop wc.hInstance<br>
mov wc.hbrBackground,COLOR_WINDOW+1 <br>
mov wc.lpszMenuName,NULL<br>
mov wc.lpszClassName,OFFSET ClassName <br>
invoke LoadIcon,NULL,IDI_APPLICATION<br>
mov wc.hIcon,eax<br>
mov wc.hIconSm,eax<br>
invoke LoadCursor,NULL,IDC_ARROW<br>
mov wc.hCursor,eax invoke <br>
RegisterClassEx, addr w <br>
<br>
上面几行从概念上说确实是非常地简单。只要几行指令就可以实现。其中的主要概念就是窗口类(window class),一个窗口类就是一个有关窗口的规范,这个规范定义了几个主要的窗口的元素,如:图标、光标、背景色、和负责处理该窗口的函数。您产生一个窗口时就必须要有这样的一个窗口类。如果您要产生不止一个同种类型的窗口时,最好的方法就是把这个窗口类存储起来,这种方法可以节约许多的内存空间。也许今天您不会太感觉到,可是想想以前
PC 大多数只有 1M 内存时,这么做是非常有必要的。如果您要定义自己的创建窗口类就必须:在一个 WINDCLASS 或 WINDOWCLASSEXE
结构体中指明您窗口的组成元素,然后调用 RegisterClass 或 RegisterClassEx ,再根据该窗口类产生窗口。对不同特色的窗口必须定义不同的窗口类。
WINDOWS有几个预定义的窗口类,譬如:按钮、编辑框等。要产生该种风格的窗口无须预先再定义窗口类了,只要包预定义类的类名作为参数调用 CreateWindowEx
即可。</p>
<p> WNDCLASSEX 中最重要的成员莫过于lpfnWndProc了。前缀 lpfn 表示该成员是一个指向函数的长指针。在 Win32中由于内存模式是
FLAT 型,所以没有 near 或 far 的区别。每一个窗口类必须有一个窗口过程,当 Windows 把属于特定窗口的消息发送给该窗口时,该窗口的窗口类负责处理所有的消息,如键盘消息或鼠标消息。由于窗口过程差不多智能地处理了所有的窗口消息循环,所以您只要在其中加入消息处理过程即可。下面我将要讲解
WNDCLASSEX 的每一个成员</p>
<p>WNDCLASSEX STRUCT DWORD <br>
cbSize
DWORD ? <br>
style
DWORD ? <br>
lpfnWndProc DWORD
? <br>
cbClsExtra DWORD
? <br>
cbWndExtra DWORD
? <br>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -