80386汇编-第一个窗口程序
9unk Lv5

了解窗口

windows 屏幕上的一个个方块就是一个个窗口。windows 是多任务操作系统,可以同时运行多个程序,同样,各个程序在屏幕上显示不能互相干扰,而且多个程序可以看成是“同时”运行的,在后台的程序也可能随时向屏幕输出信息,着中间的跳读是由 windows 向术语自己的窗口显示信息。因此窗口被设计出来。

窗口和程序的关系

大多数程序都是由多个窗口组成的,例如程序中的“图标”、“标题栏”、“放大”、“缩小”、“关闭”、“菜单栏”等等都是该程序(父窗口)的字窗口。当然有的文件没有窗口,例如 bat 脚本。所以说一个程序有没有窗口是自定义的。

窗口程序是如何运行的

DOS 程序是顺序化的、按过程驱动的程序设计方法,这种程序由明显的开始、明显的过程、明显的结束。

窗口程序是事件驱动的,用户可能随时发出各种消息,如拖动边框,程序必须马上调整客户区的内容以适应新的窗口大小;窗口最小化,关闭按钮也可能随时被按下,着意味着程序要随时可以处理退出请求。

1-24.png

编写的 FirstWindow 程序

1-25.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
    .386
.model flat,stdcall
option casemap:none

; include 文件定义

include windows.inc
include gdi32.inc
includelib gdi32.lib
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib

; 数据段

.data?
hInstance dd ?
hWinMain dd ?

.const
szClassName db 'MyClass',0
szCaptionMain db 'My first Window !',0
szText db 'Win32 Assembly, Simple and prowerful !',0

; 代码段

.code
; 窗口过程

_ProcWinMain proc uses ebx edi esi,hWnd,uMsg,wParam,lParam
local @stPs:PAINTSTRUCT
local @stRect:RECT
local @hDc

mov eax,uMsg

.if eax == WM_PAINT
invoke BeginPaint,hWnd,addr @stPs
mov @hDc,eax
invoke GetClientRect,hWnd,addr @stRect
invoke DrawText,@hDc,addr szText,-1,\
addr @stRect,\
DT_SINGLELINE or DT_CENTER or DT_VCENTER

invoke EndPaint,hWnd,addr @stPs
.elseif eax == WM_CLOSE
invoke DestroyWindow,hWinMain
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
_ProcWinMain endp

_WinMain proc
local @stWndClass:WNDCLASSEX
local @stMsg:MSG

invoke GetModuleHandle,NULL
mov hInstance,eax
invoke RtlZeroMemory,addr @stWndClass,Sizeof @stWndClass

; 注册窗口
invoke LoadCursor,0,IDC_ARROW
mov @stWndClass.hCursor,eax
push hInstance
pop @stWndClass.hInstance
mov @stWndClass.cbSize,sizeof WNDCLASSEX
mov @stWndClass.style,CS_HREDRAW or CS_VREDRAW
mov @stWndClass.lpfnWndProc,offset _ProcWinMain
mov @stWndClass.hbrBackground,COLOR_WINDOW + 1
mov @stWndClass.lpszClassName,offset szClassName
invoke RegisterClassEx,addr @stWndClass

; 建立窗口
invoke CreateWindowEx,WS_EX_CLIENTEDGE,\
offset szClassName,offset szCaptionMain,\
WS_OVERLAPPEDWINDOW,\
100,100,600,400,\
NULL,NULL,hInstance,NULL
mov hWinMain,eax
invoke ShowWindow,hWinMain,SW_SHOWNORMAL
invoke UpdateWindow,hWinMain

; 消息循环
.while TRUE
invoke GetMessage,addr @stMsg,NULL,0,0
.break .if eax == 0
invoke TranslateMessage,addr @stMsg
invoke DispatchMessage,addr @stMsg
.endw
ret
_WinMain endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
call _WinMain
invoke ExitProcess,NULL
end start

程序结构分析:程序的入口是 start,然后执行了 _WinMain 子程序,完成后就是程序退出函数 ExitProcess;_WinMain 子程序的 API 结构(GetModuleHandle —> RtlZeroMemory —> LoadCursor —> RegisterClassEx —> CreateWindowEx —> ShowWindow —> UpdateWindow)从这个 API 的名称中就能看出,这是一个窗口的建立过程(从注册窗口到显示窗口)。接下来就是由 3 个 API 组成的消息循环(GetMessage —> TranslateMessage —> DispatchMessage)。_WinMain 中还有一个 _ProcWinMain 子程序,这个子程序的功能就是把参数 uMsg 取出来,然后根据不同的 uMsg 执行不同的代码。

窗口程序的运行过程

在屏幕上显示一个窗口的过程一般有如下几步骤:

  1. 得到应用程序的句柄(GetModuleHandle)
  2. 注册窗口类(RegisterClassEx)。在注册之前,要先填写 RegisterClassEx 的参数 WNDCLASSES 结构。
  3. 建立窗口(CreateWindowEx)
  4. 显示窗口(ShowWindow)
  5. 刷新窗口客户区(UpdateWindow)
  6. 进入无线的消息获取和处理循环。首先获取消息(GetMessage),消息到达,则将消息分派到回调函数处理(DispatchMessage),如果消息是 VM_QUIT,则退出循环。程序的另一半 _ProcWinMain 子程序就是处理消息的回调函数(Callback),也叫窗口过程,这个函数是由 windows 调用的而不是我们自己调用的。我们调用 DispatchMessage,而 DispatchMessage 在自己的内部回过来调用窗口过程。

所有的用户操作都是通过消息来传给应用程序的,如用户按键、鼠标移动、选择菜单和拖动窗口等,应用程序中由窗口过程接收消息并处理,在例子程序中就是 _ProcWinMain。所以说一个应用中几乎所有的功能代码都集中在窗口过程里。

1-26.png

  1. windows 系统内有一个系统消息队列,当输入设备有所动作的时候,windows 就会产生相应的记录放在系统消息队列里。(如上图 a、b)
  2. windows 为每个程序(严格来说是每个线程)维护一个消息队列,windows 检查系统消息队列里消息发生的位置,当位置位于某个程序的窗口范围内的时候,就会把这个消息派送到应用程序的队列里。(如上图 c)
  3. 当应用程序还没有来取消息的时候,消息就暂时保留在消息队列里,当程序中的消息循环执行到 GetMessage 的时候,控制权会转移到 GetMessage 所在的 USER32.DLL 中(箭头1),UESR32.DLL 从程序消息队列中取出消息(箭头2),然后把这条消息返回到应用程序(箭头3)
  4. 应用程序可以对这条信息做预处理,如可以用 TranslateMessage 把基于键盘扫描码的按键消息转换成基于 ASCII 码的键盘消息,以后也会用到 TranslateAccelerator 把键盘快捷键转换成命令消息,但这个步骤不是必须的。
  5. 然后应用程序使用 DispatchMessage 间接调用窗口过程处理这条信息。Dispatch 的英文含义是 “分派”,因为一个程序可能建有不止有一个窗口,不同的窗口消息必须分派给相应的窗口过程。当控制权转移到 USER32.DLL 中的 DispatchMessage 时,DispatchMessage 找出相应窗口的窗口过程,然后把消息的具体信息当作参数来调用它(箭头5),窗口过程根据消息找到对应的分支取处理,然后返回(箭头6),这时控制权限回到 DispatchMessage,最后 DispatchMessage 函数返回应用程序(箭头7)。这样一个循环就结束了,程序又开始新一轮的 GetMessage。
  6. 程序之间也可以护互发消息,PostMessage 是把一个消息放在其他程序的消息队列中(箭头d),目标程序收到这条消息就会把它放入该程序的消息队列取处理;而 SendMessage 则越过消息队列直接调用目标程序的窗口过程(箭头I),窗口过程返回以后才从 SendMessage 返回(II)

windows 在调用 RegisterClassEx 函数的时候会把窗口过程的地址告诉 windows。所以 windows 知道从哪里取回调。

分析窗口程序

模块和句柄

  1. 模块
    一个模块代表一个在内存中运行的 exe 文件或 dll 文件,用来代表这个文件所有的代码和资源,磁盘上的文件不是模块,装入内存后运行时就叫模块。为了区分地址空间中的不同模块,每个模块都有一个唯一的模块句柄来标识。

由于很多 API 函数都要用到程序的模块句柄,为了方便程序中的各种资源,所以在程序一开始就先取得模块句柄并存放到一个全局变量中可以省去很多麻烦。在 win32 中,模块句柄在数值上程序在内存中装入的起始地址。

取得模块的句柄使用的 API 函数是 GetModuleHandle ,它的使用方法是:

1
invoke GetModuleHandle,lpModuleName

lpModuleName 参数是一个指向含有模块名称字符串的指针,可以用这个函数取得程序地址空间中各个模块的句柄。例如想用 Use32.dll 的句柄以便使用其中包含的图标资源,那么可以如下使用:

1
2
3
szUserDll db 'user32.dll',0
...
invoke GetModuleHandle,addr azUserDll

如果使用的参数是 NULL 调用 GetModuleHandle ,那么得到的是调用者本模块的句柄,如下:

1
2
invoke GetModuleHandle,NULL
mov hInstance,eax

这里把取得的句柄放在 hInstance 变量中,hInstance 就是 hModule。

  1. 句柄
    句柄可以理解为是一个编号,windows 会根据句柄的值去标识相应的窗口、文件、线程和模块等。

窗口创建

windows 中创建窗口需要先定义一个窗口类,然后在窗口类的基础上添加其他的属性建立窗口。对于按钮、文本输入框和选择框等,这些特殊的窗口 windows 都预先定义了相应的类。只有用户自定义的窗口才需要先定义自己的类,再建立窗口。

  1. 注册窗口类
    建立窗口类的方法是在系统中注册,注册窗口类的 API 函数是 RegisterClassEx,最后的 “Ex” 是扩展的意思,因为它是 win16 RegisterClass 的扩展。一个窗口的一些主要的属性,如:光标、图标、背景色、菜单和负责处理该窗口所属消息的函数。这些属性是定义在一个 WNDCLASSEX 结构中,然后再把结构的地址当参数一次性传递给 RegisterClassEx,WNDCLASSEX 是 WNDCLASS 结构的扩展。

WNDCLASSEX 的结构定义为:

1-27.jpg

首先程序定义了一个 WNDCLASSEX 结构的变量 @stWndClass,用 RtlZeroMemory 将它填写全零(局部变量需要初始化),再填写结构的各字段,这样,没有赋值的部分保持为 0,结构各字段的含义如下:

1-28.jpg

  • hIcon——图标句柄,指定显示在窗口标题栏左上角的图标。windows 已经预定义了一些图标,同样,程序也可以使用资源文件中定义的图标,这些图标的句柄可以用 LoadIcon 函数获得。

  • hCursor——光标句柄,指定了鼠标在窗口中的光标形状。同样,windows 也预定义了一些光标,可以用 LocadCursor 获取它们的句柄,IDC_ARROW 是 Windows 预定义的箭头光标,如果想使用自定义的光标,也可以自己在资源文件中定义。

  • lpszMenuName——指定窗口上显示的默认菜单,它指向一个字符串,描述资源文件中菜单的名称,如果资源文件中菜单是用数值定义的,那么这里使用菜单资源的数值。窗口中的菜单也可以建立窗口函数 CreateWindowEx 的参数中指定。如果两个地方都没有指定,那么建立的窗口上就没有菜单。

  • hInstance——指定要注册的窗口类属于哪个模块,模块句柄在程序开始的地方用 GetModuleHandle 函数获得。

  • cbSize——指定 WNDCLASSEX 结构的长度,用 sizeof 伪操作来获取。主要是用来区分结构的版本,当以后新增了一个字段时,cbSize 就会相应增大,如果调用的时候 cbSize 还是旧的疮毒,表示运行的是基于旧的结构程序,这样可以防止使用无效的字段。

  • style——窗口风格。CS_HREDRAW 和 CS_VREDRAW 表示窗口的宽度或高度改变时是否重画窗口。比较重要的是 CS_DBLCLKS 风格,指定了它,windows 才会把在窗口中快速两次单机鼠标的行为翻译成双击消息 VM_LBUTTONDBLCLK 发给窗口过程。

  • hbrBackground——窗口客户区的背景色。前面的 hbr 表示它是一个刷子(Brush)的句柄,“刷子” 一词形象地表示了填充一个区域的着色模式。windows 预定义了一些刷子,如 BLACK_BRUSH 和 WHITE_BRUSH 等,可以用 下列语句来得到它们的句柄:

1
invoke GetStockObject, WHITE_BRUSH

但在这里也可以使用颜色值,windows 已经预定义了一些颜色值,分别对应窗口各部分的颜色。使用颜色值得时候,windows 规定必须在颜色值上加1,所以程序中的指令是:

1
mov    @stWndClass.hbrBackground,COLOR_WINDOW + 1
  • szClassName——指定程序员要建立的类命名,以便以后用这个名称来引用它。

  • cbWndExtra 和 cbClsExtra——分别是在 windows 内部保存的窗口结构和类型结构给程序员预留的空间大小,用来存放自定义数据,单位是字节。不适用自定义数据的话,两个字段就是0 。

  • lpfnWndProc——最重要的参数,它指定了基于这个类建立的窗口的窗口过程地址。通过这个参数,Windows 就知道了在 DiskpatchMessage 函数中吧窗口消息发到哪里去,一个窗口过程可以为多个窗口服务,只要这些窗口是基于同一个窗口建立的。 Windows 中不同应用程序的按钮和文本框的行为都是一样的,因为他们是基于相同的 Windows 预定义类建立的,所以它们背后的窗口过程其实都是同一段代码。

1-29.jpg

  1. 创建窗口
    接下来是在已注册的窗口类的基础上建立窗口。与注册窗口类时使用一个结构传递所有参数不同,建立窗口时所有的属性都是用单个参数的方式传递的,建立窗口的函数是 CreateWindowEx,同样,它是 Win 16 中 CreateWindow 函数的扩展,主要表现在多了一个 dwExStyle(扩展风格)参数。CreateWindowEx 函数的使用方法是:
    1
    2
    invoke CreateWindowEx,dwExStyle,lpClassName,lpWindowName,dwStyle,\
    x,y,nWidth,nHeight,hWndParent,hMenu,hInstance,lpParam
  • lpClassName——建立窗口使用的类名字符串指针,在程序中指向 “MyClass” 字符串,表示使用 “MyClass” 类建立窗口。

  • lpWindowName——指向表示窗口的字符串,该名称会显示在标题栏上。如果该参数空白,则标题栏上什么都没有。

  • hMenu——窗口上要出现的菜单句柄。在注册窗口类的时候也定义了一个菜单,那是窗口的默认菜单,则使用窗口类中定义的菜单;如果这里指定了菜单句柄,则不管窗口类中有没有定义都将使用这里定义的菜单;两个地方都没有定义菜单,则窗口上没有菜单。另外,当监理的窗口是子窗口时(dwStyle 中指定了 WS_CHILD),这时 hMenu 参数指定的是子窗口的 ID 号。

  • lpParam——这是一个指针,指向一个欲传给窗口的参数,这个参数在 WM_CREATE 消息中可以被获取,一般情况下用不到这个字段。

  • hInstance——模块句柄,和注册窗口类时一样,指定了窗口所属的程序模块。

  • hWndParent——窗口所属的父窗口,对于普通的窗口(相对于子窗口),主要是用来在父窗口销毁时一同将其 “子” 窗口销毁,并不会把窗口位置限制在父窗口的客户区范围内,但如果要建立的是真正的子窗口(dwStyle 中指定了 WS_CHILD 的时候),这时窗口位置会被限制在父窗口的客户区范围内,同时窗口的坐标(x,y)也是以父窗口的左上角为基准的。

  • x,y——指定窗口左上角位置,单位是像素。默认时可指定为 CW_USEDFAULT,这样 windows 会自动为窗口指定最合适的位置,当建立子窗口时,位置是以父窗口的左上角为基准的,否则,以屏幕左上角为基准。

  • nWidth,nHeight——窗口的宽度和高度,也就是窗口的大小,同样是以像素为单位的。默认时可指定为 CW_USEDEFAULT,这样 windows 会自动为窗口指定最合适的大小。

窗口的两个参数 dwStyle 和 dwExStyle 决定了窗口的外形和行为,下表列出了一些常见的 dwStyle 定义。

1-30.jpg

1-31.jpg

1-32.jpg

程序中建立窗口的相关代码如下:

1
2
3
4
5
6
7
8
9
; 建立窗口
invoke CreateWindowEx,WS_EX_CLIENTEDGE,\
offset szClassName,offset szCaptionMain,\
WS_OVERLAPPEDWINDOW,\
100,100,600,400,\
NULL,NULL,hInstance,NULL
mov hWinMain,eax
invoke ShowWindow,hWinMain,SW_SHOWNORMAL
invoke UpdateWindow,hWinMain

建立窗口以后,eax 中传回来的是窗口句柄,要把它保存起来以备后用。这个时候,窗口虽然已经建立,但是还没有在屏幕上显示出来,要用 ShowWindow 把它显示出来,ShowWindow 也可以用在别的地方,主要用来控制窗口的显示状态(显示或隐藏),大小控制(最大化、最小化或原始大小)和是否激活(当前窗口还是背后的窗口),它用窗口句柄做第一个参数,第二个参数则是显示方式。下表给出了显示方式预定义值。

1-33.jpg

窗口显示以后,用 UpdateWindow 绘制客户区,他实际上就是向窗口发送了一条 WM_PAINT 消息。

CreateWindowEx 也可以用来建立子窗口,如按钮、文本框。下面举例说明建立一个按钮的方法:

1-34.jpg

需要注意的是:风格一定要指定 WS_CHILD,建立的按钮才会在主窗口上。WS_VISIBLE 也要同时指定,否则按钮不会显示出来,hMenu 参数在这里用做表示最窗口 ID,将它设置为 1,在建立多个子窗口的时候,ID 应该有所区别。

  1. 消息循环

消息循环的一般结构:

1
2
3
4
5
6
7
; 消息循环
.while TRUE
invoke GetMessage,addr @stMsg,NULL,0,0
.break .if eax == 0
invoke TranslateMessage,addr @stMsg
invoke DispatchMessage,addr @stMsg
.endw

消息循环中的几个函数要用 MSG 结构,用来消息传递:

1-35.jpg

它的各个字段的含义是:

  • hwnd——消息要法向的窗口句柄
  • message——消息标识符,在文件中以 WM_ 开头的预定义值(意思是 windows Message)
  • wParam——消息的参数之一
  • lParam——消息的参数之二
  • time——消息放入消息队列的时间
  • pt——这是一个 POINT 数据结构,表示消息放入消息队列时的鼠标坐标。

这个结构定义了消息的所有属性,GetMessage 函数就是从消息对劣质取出这样的一条消息:

1
invoke GetMessage,lpMsg,hWnd,wMsgFilterMin,wMsgFilterMax

函数的 lpMsg 指向一个 MSG 结构,函数会在这里返回取到消息,hWnd 参数指定要获取哪个窗口的消息,例子中指定为 NULL,表示获取的是所有本程序所属窗口的消息。例子中指定为 NULL,表示获取的是所有本程序所属窗口的消息,wMsgFilterMin 和 wMsgFilterMax 为 0 表示所有编号的消息。

GetMessage 函数从消息队列里取得消息,填写好 MSG 结构并返回,如果获取的消息是 WM_QUIT 消息,那么 eax 中的返回值是 0,否则 eax 返回非零值,所以用 .break .if eax==0 来检查返回值,如果消息队列中有 WM_QUIT 则退出循环。

TranslateMessage 将 MSG 结构传给 Windows 进行一些键盘消息的转换,当有键盘按下和放开时,windows 产生 WM_KEYDOWN 和 WM_KEYUP 或 WM_SYSKEYDOWN 和 WM_SYSKEYUP 消息,但这些消息的参数中包含的是按键的扫描码,转换成常用 ASCII 码要经过查表,很不方便。TranslateMessage 遇到键盘消息则将扫描码转换成 ASCII 码并在消息队列中插入 WM_CHAR 或 WM_SYSCHAR 消息,参数就是转换好的 ASCII 码,如此一来,要处理键盘消息的话只要处理 WM_CHAR 消息就好了。遇到非见哦按消息则 TranslateMessage 不做处理。

最后由 DispatchMessage 将消息发送到窗口对应的窗口过程处理。窗口过程返回后 DispatchMessage 函数才返回,然后开始新一轮的循环

  1. 窗口过程

窗口之间的通信

  • 本文标题:80386汇编-第一个窗口程序
  • 本文作者:9unk
  • 创建时间:2020-10-26 23:57:41
  • 本文链接:https://9unkk.github.io/2020/10/26/80386-hui-bian-di-yi-ge-chuang-kou-cheng-xu/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!