win32汇编-windows程序设计(预习篇)
9unk Lv5

简介

《Inetel 汇编语言程序设计》这本书中涉及了很多知识点,但大多都是简单地提了一下,知识点写的并不深入。这篇 win32 汇编算是提前预习了解,后续会更加深入学习这些知识点。

什么是windows程序

用汇编写 windows 程序,这个就是 win32 汇编。windows程序是面向对象的程序,我们之前学习的内容更多的侧重于算法,按照代码的顺序执行(面向过程的程序)。windows程序是一种消息驱动的程序,根据消息传递的顺序来执行的,而不是按写代码的顺序执行的。

背景知识

控制台程序

1

补充: 在学习8086汇编的过程中,写过创建一个窗口,显示显存中的内容。控制台显示字符的原理就是:将窗口定位到显存的某个地址,再输出显示该线性地址的内容。只不过在win32中这个窗口和具体显存地址是由windows系统自动创建分配。

Win32 API 的参考信息

2
3

高级操作和底层操作

4

windows 的数据类型

5
6

SmallWin.inc 包含文件

7

控制台句柄

8

Win32控制台函数

9
10

显示消息框

11
12

程序清单: 由于 MessageBox 是 MessageBoxA 的别名,因此程序中使用了前者

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
;---------------------------
;程序名:MessageBox.asm
;功能:演示 MessageBox API
;作者:9unk
;编写时间:2023-4-10
;----------------------------
INCLUDE Irvine32.inc
;INCLUDE windows.inc

.data
captionW BYTE "Attempt to Divide by Zero",0
warningMsg BYTE "Please check your denominator.",0

captionQ BYTE "Question",0
questionMsg BYTE "Do you want to know my name?",0

showMyName BYTE "My Name is MASM",0dh,0ah,0

captionC BYTE "Information",0
infoMsg BYTE "Your file was erased.",0dh,0ah
BYTE "Notify system admin, or restore backup?",0

.code
main PROC
;显示一条警告信息
INVOKE MessageBox,NULL,ADDR warningMsg,
ADDR captionW,
MB_OK + MB_ICONEXCLAMATION

;询问一个问题,等待回应
INVOKE MessageBox,NULL,ADDR questionMsg,
ADDR captionQ,MB_YESNO + MB_ICONQUESTION

cmp eax,IDYES
jne L2
;向控制台窗口写名称
mov edx,OFFSET showMyName
call WriteString
;更复杂的按钮,可能会让用户迷惑
L2:
INVOKE MessageBox,NULL,ADDR infoMsg,ADDR captionC,MB_YESNOCANCEL + MB_ICONEXCLAMATION + MB_DEFBUTTON2
exit
main ENDP
END main

13

控制台输入

本书中的 ReadString 和 ReadChar 过程,都是 Win32 API 函数 ReadConsole 的封装。
控制台输入缓冲区: Win32 控制台有一个输入缓冲区,其中包含一个输入动作记录的队列,每个输入动作,如键盘敲击、鼠标移动、按下鼠标等,都会在缓冲区中产生一条记录。高级操作函数如 ReadConsole 等过滤并处理这些输入数据,只返回字符流。

ReadConsole 函数

ReadConsole 函数提供了一种把文本输入到读取到一个缓冲区中的便捷方法,函数原型如下:

1
2
3
4
5
6
ReadConsole PROTO,
hConsoleInput:HANDLE, ;输入句柄
lpBuffer:PTR BYTE, ;缓冲区地址指针
nNumberOfCharsToRead:DWORD, ;要读取的字符数量
lpNumberOfCharsRead:PTR DWORD, ;指向返回实际读取数量大小的指针
lpReserved:DWORD ;(保留)
  • hConsoleInput 参数是 GetStdHandle 函数返回的有效输入句柄
  • lpBuffer 参数指向一个字符缓冲区
  • nNumberOfCharsToRead 参数是一个 32 位整数,指定了要读取字符的最大数量
  • lpNumberOfCharsRead 参数是指向一个双字变量的指针,函数运行时会填写该变量,它返回实际读取到缓冲区中的字符数量。
  • 最后一个参数未使用,使用时要传递一个数值(比如:0)

除了用户输入以外,调用 ReadConsole 读入输入缓冲区中的文本还包含两个额外的字符————行结束符(回车和换行符)。欲使输入缓冲区中的文本以 0 结尾,那么应该把包含 0Dh(回车)的字节替换为 0,ReadString 就是这样做的。

例子程序: 写一个程序读取用户输入的字符

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
;---------------------------
;程序名:ReadConsole.asm
;功能:读取用户输入的字符
;作者:9unk
;编写时间:2023-4-11
;----------------------------
INCLUDE Irvine32.inc
BufSize = 80

.data
buffer BYTE BufSize DUP(?),0,0
stdInHandle HANDLE ?
bytesRead DWORD ?

.code
main PROC
;获取标准输入句柄
INVOKE GetStdHandle,STD_INPUT_HANDLE
mov stdInHandle,eax

;等待用户输入
INVOKE ReadConsole,stdInHandle,ADDR buffer,BufSize - 2,ADDR bytesRead,0

;显示缓冲区内容
mov esi,OFFSET buffer
mov ecx,bytesRead
mov ebx,TYPE buffer
call DumpMem

exit
main ENDP
END main

14

错误检查

15
16
17

单字符的输入

18
19

ReadKey测试程序: 下面的程序测试 ReadKey,程序使用一个循环延时等待按键输入,然后输出是否按下了 CapsLock 键。

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
;---------------------------
;程序名:TestReadkey.asm
;功能:使用一个循环延时等待按键输入,然后输出是否按下了 CapsLock 键。
;作者:9unk
;编写时间:2023-4-11
;----------------------------
INCLUDE Irvine32.inc
INCLUDE Macros.inc

.code
main PROC
L1:
mov eax,10 ;延时等待键盘输入
call Delay
call ReadKey
jz L1

test ebx,CAPSLOCK_ON
jz L2
mWrite <"CapsLock is ON",0dh,0ah>
jmp L3
L2:
mWrite <"CapsLock is OFF",0dh,0ah>
L3:
exit
main ENDP
END main

20

获取键盘状态

调用 GetKeyState API 函数可以测试单个按键的状态,查看其是否正被按下。函数原型如下:

1
GetKeyState PROTO,nVirtKey:DWORD

调用 GetKeyState 时,应传递一个要检查按键的虚拟键码,在函数返回后,应测试EAX中的相应位是否置位了。一些虚拟键码的值以及应检查的对应数据位如下表:
21

下面的例子程序演示了 GetKeyState 的用法,程序检查了NumLock 和 左Shift按键状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;---------------------------
;程序名:Keybd.asm
;功能:检查了NumLock 和 左Shift按键状态
;作者:9unk
;编写时间:2023-4-11
;----------------------------
INCLUDE Irvine32.inc
INCLUDE Macros.inc

.code
main PROC
INVOKE GetKeyState,VK_NUMLOCK
test al,1
.IF !Zero?
mWrite <"The NumLock ke is ON",0dh,0ah>
.ENDIF
INVOKE GetKeyState,VK_LSHIFT
test al,80h
.IF !Zero?
mWrite <"The Left Shift ke is currently DOWN",0dh,0ah>
.ENDIF
main ENDP
END main

控制台输出

在第5章介绍的 Irvine32 链接库中的 WriteSring 过程只要一个参数:通过 EDX 传递的字符串地址。事实上,WriteString 过程是对 win32 函数 WriteConsole 的封装,调用后者时处理的细节要更多一些。

相关的数据结构

一些 win32 控制台函数使用预定义的数据结构,如 COORD 和 SMALL_Rect 结构。COORD 结构用于存放字符在控制台屏幕缓冲区中的坐标,坐标系的远点(0,0)在屏幕的 左上角:

1
2
3
4
COORD STRUCT
X WORD ?
Y WORD ?
COORD ENDS

SMALL_RECT 结构用于存放矩形区域的左上角和右小角坐标,它制定了控制台窗口中的一块矩形区域:

1
2
3
4
5
6
SMALL_RECT STRUCT
Left WORD ?
Top WORD ?
Right WORD ?
Bottom WORD ?
SMALL_RECT ENDS

WriteConsole 函数

WriteConsole 函数在控制台窗口中的当前光标位置显示一个字符串并前进光标,支持标准的 ASCII 控制符。要显示字符串不必以0结尾。函数原型如下:

1
2
3
4
5
6
WriteConsole PROTO,
hConsoleOutput:HANDLE,
lpBuffer:PTR BYTE,
nNumberOfCharsToWrite:DWORD,
lpNumberOfCharsWritten:PTR DWORD,
lpReserved:DWORD
  • 第一个参数 hConsoleOutput:控制台输出句柄
  • 第二个参数 lpBuffer:指向要显示的字符串的指针
  • 第三个参数 nNumberOfCharsToWrite 指定了要显示的字符串长度
  • 第四个参数 lpNumberOfCharsWritten 指向一个整数变量,函数通过该变量返回实际输出的字符数量
  • 最后一个参数保留未用,在使用的时候把它置为0

例子程序:Console

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
;-----------------------------------------------------
;程序名:Console.asm
;功能:在控制台窗口显示一个字符串,以此示范 GetStdHandle,
;ExitProcess 和 WriteConsole 函数的用法。
;作者:9unk
;编写时间:2023-2-13
;--------------------------------------------------------
INCLUDE Irvine32.inc

.data
end1 EQU <0dh,0ah>

Message LABEL BYTE
BYTE "This is a simple demonstration of"
BYTE "Console mode output,using the GetStdHandle"
BYTE "and WriteConsole functions.",end1
messageSize DWORD ($-Message)

consoleHandle HANDLE 0 ;标准输出设备句柄
bytesWritten DWORD ? ;一输出的字符数量

.code
main PROC
;获取句柄
INVOKE GetStdHandle,STD_OUTPUT_HANDLE
mov consoleHandle,eax
;在控制台显示一个字符串
INVOKE WriteConsole,
consoleHandle,
ADDR Message,
messageSize,
bytesWritten,
0
exit
main ENDP
END main

WriteConsoleOutputCharacter 函数

WriteConsoleOutputCharacter 函数将一定数量的字符复制到屏幕缓冲区从指定位置开始的连续空间中。函数原型如下:

1
2
3
4
5
6
WriteConsoleOutputCharacter PROTO,
hConsoleOutput:HANDLE, ;控制台缓冲区句柄
lpcharacter:PTR BYTE, ;字符串缓冲区地址
nLength:DWORD, ;缓冲区大小
dwWriteCoord:COORD, ;首字符的坐标
lpNumberOfCharsWritten:PTR DWORD ;实际输出字符的数量

输出字符的时候,如果到达屏幕行的末尾,那么自动换行。该函数不影响控制台缓冲区中原有字符的属性值。如果函数无法输出字符,返回值为0,忽略字符串中的 ASCII 控制字符,如制表符、回车符和换行符。

文件的读写

CreateFile 函数

22
23
24
25
26
27
28
29

Irvine32 库的文件 I/O 过程

30
31
32

测试文件 I/O 过程

演示创建文件的例子程序

下面的程序创建了一个输出文件,要求用户输入一段文本,然后把文本写入输出文件并报告已写入字节数,最后关闭文件。

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
;-------------------------------------------------------
;程序名:CreateFile.asm
;功能:创建一个输出文件,要求用户输入一段文本,
;然后把文本写入输出文件并报告已写入字节数,最后关闭文件。
;作者:9unk
;编写时间:2023-4-15
;-------------------------------------------------------
INCLUDE Irvine32.inc
BUFFER_SIZE = 501

.data
buffer BYTE BUFFER_SIZE DUP(?)
filename BYTE "output.txt",0
fileHandle HANDLE ?
stringLength DWORD ?
bytesWritten DWORD ?
str1 BYTE "Cannot create file",0dh,0ah,0
str2 BYTE "Bytes written to file [output.txt]: ",0
str3 BYTE "Enter up to 500 characters and press"
BYTE "[Enter]: ",0dh,0ah,0

.code
main PROC
;创建一个新的文本文件
mov edx,OFFSET filename
call CreateOutputFile
mov fileHandle,eax

;检查错误
cmp eax,INVALID_HANDLE_VALUE
jne file_ok
mov edx,OFFSET str1
call WriteString
jmp quit
file_ok:
;要求用户输入一个字符串
mov edx,OFFSET str3
call WriteString
mov ecx,BUFFER_SIZE
mov edx,OFFSET buffer
call ReadString
mov StringLength,eax
;把缓冲区写入输出文件
mov eax,fileHandle
mov edx,OFFSET buffer
mov ecx,StringLength
call WriteToFile
mov bytesWritten,eax
call CloseFile
;显示返回值
mov edx,OFFSET str2
call WriteString
mov eax,bytesWritten
call WriteDec
call Crlf
quit:
exit
main ENDP
END main

演示读取文件的例子程序

下面的程序打开一个文件用于输入,把它的内容读入一个缓冲区,然后显示缓冲区的内容调用的所有过程都是 Irvine32 库中的过程:

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
;---------------------------
;程序名:ReadFile.asm
;功能:打开一个文件用于输入,把它的内容读入一个缓冲区,然后显示缓冲区的内容。
;作者:9unk
;编写时间:2023-4-15
;----------------------------
INCLUDE Irvine32.inc
INCLUDE macros.inc
BUFFER_SIZE = 5000

.data
buffer BYTE BUFFER_SIZE DUP(?)
filename BYTE 80 DUP(?)
fileHandle HANDLE ?

.code
main PROC
;允许用户输入一个文件名
mWrite "Enter an input filename: "
mov edx,OFFSET filename
mov ecx,SIZEOF filename
call ReadString
;打开文件用于输入
mov edx,OFFSET filename
call OpenInputFile
mov fileHandle,eax
;检查错误
cmp eax,INVALID_HANDLE_VALUE
jne file_ok
mWrite <"Cannot open file",0dh,0ah>
jmp quit
file_ok:
;把文件内容读入一个缓冲区
mov edx,OFFSET buffer
mov ecx,BUFFER_SIZE
call ReadFromFile
jnc check_buffer_size
mWrite "Error reading file."
call WriteWindowsMsg
jmp close_file
check_buffer_size:
cmp eax,BUFFER_SIZE
jb buf_size_ok
mWrite <"Error: Buffer too small for the file",0dh,0ah>
jmp quit
buf_size_ok:
mov buffer[eax],0
mWrite "File size: "
call WriteDec
call Crlf
;显示缓冲区内容
mWrite <"Buffer:",0dh,0ah,0dh,0ah>
mov edx,OFFSET buffer
call WriteString
call Crlf
close_file:
mov eax,fileHandle
call CloseFile
quit:
exit
main ENDP
END main

控制台窗口的操作

33
34
35
36

下面的 SCroll.asm 程序在屏幕缓冲区上显示 50 行文本,然后改变控制台窗口的大小和位置,达到有效地回滚文字地效果。

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
;----------------------------------------------------------
;程序名:Scroll.asm
;功能:在屏幕缓冲区上显示50行文本,然后改变控制台窗口的位置和大小,达到有效地回滚文字地效果。
;作者:9unk
;编写时间:2023-4-15
;----------------------------------------------------------
INCLUDE Irvine32.inc

.data
message BYTE ": This line of text was Written "
BYTE "to the screen buffer",0dh,0ah
messageSize DWORD ($-message)

outHandle HANDLE 0
bytesWritten DWORD ?
lineNum DWORD 0
;左上:0列,0行;右下:60列,11行
windowRect SMALL_RECT <0,0,60,11>

.code
main PROC
INVOKE GetStdHandle,STD_OUTPUT_HANDLE
mov outHandle,eax
.REPEAT
mov eax,lineNum
call WriteDec
INVOKE WriteConsole,
outHandle,
ADDR message,
messageSize,
ADDR bytesWritten,
0
inc lineNum
.UNTIL lineNum > 50
;调整控制台窗口相对于屏幕缓冲区的大小并重新定位
INVOKE SetConsoleWindowInfo,
outHandle,
TRUE,
ADDR windowRect ;窗口大小

call Readchar ;等待按键
call Clrscr ;清除屏幕缓冲区
call Readchar ;等待第二次按键

INVOKE ExitProcess,0
main ENDP
END main

37

光标的控制

38

文本颜色的控制

39
40

WriteColors

例子程序 WriteColors.asm 创建一个字符数组和一个属性数组,属性数组里面的每个属性对应字符数组里面的一个字符。程序调用 WriteConsoleOutputAttribute 函数把颜色属性复制到屏幕缓冲区中,然后调用 WriteConsoleOutputCharacter 函数把字符复制到屏幕缓冲区中。

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
;------------------------------------------------------------
;程序名:WriteColors.asm
;功能:创建一个字符数组和一个属性数组,属性数组里面的每个属性对应字符数组里面的一个字符。
;作者:9unk
;编写时间:2023-2-13
;------------------------------------------------------------
INCLUDE Irvine32.inc
.data
outHandle HANDLE ?
cellsWriten DWORD ?
xyPos COORD <10,2>

;字符代码数组
buffer BYTE 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20
BUFSIZE DWORD ($-buffer)
;字符颜色数组
attributes WORD 0Fh,0Eh,0Dh,0Ch,0Bh,0Ah,9,8,7,6
WORD 5,4,3,2,1,0F0h,0E0h,0D0h,0C0h,0B0h

.code
main PROC
;获取控制台标准输出的句柄
INVOKE GetStdHandle,STD_OUTPUT_HANDLE
mov outHandle,eax

;设置相邻连续字符的颜色
INVOKE WriteConsoleOutputAttribute,
outHandle,ADDR attributes,
BufSize,xyPos,
ADDR cellsWriten

;输出 1 到 20 的字符代码
INVOKE WriteConsoleOutputCharacter,
outHandle,ADDR buffer,BUFSIZE,
xyPos,ADDR cellsWriten

INVOKE ExitProcess,0
main ENDP
END main

41

严格意义上来说颜色和字符复制的缓冲区不在同一个位置,只是函数忽略了这个细节,看上去是同一个位置。学过 8086 汇编的都应该能理解。

时间和日期函数

42
43
44
下面的例子程序 Timer.asm 测量两次调用 GetTickCount 之间经过的时间,并检查时间计数器值是否发生回滚(超过49.7天)

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
;-------------------------------------------------
;程序名:Timer.asm
;功能:调用两次 GetTickCount 监控系统经过的时间,
;并检查时间计数器值是否发生了回滚。
;作者:9unk
;编写时间:2023-4-16
;--------------------------------------------------
INCLUDE Irvine32.inc
INCLUDE macros.inc

.data
startTime DWORD ?

.code
main PROC
INVOKE GetTickCount
mov startTime,eax

;创建一个无用的循环
mov ecx,10000100h
L1:
imul ebx
imul ebx
imul ebx
loop L1

INVOKE GetTickCount
cmp eax,startTime
jb error

sub eax,startTime
call WriteDec
mWrite <" milliseconds have elapased",0dh,0ah>
jmp quit
error:
mWrite "Error: GetTickCOunt invalid--system has"
mWrite <"been active for more than 49.7 days",0dh,0ah>
quit:
exit
main ENDP
END main

45
46

编写windows 图形界面应用程序

47

必须了解的数据结构

POINT 结构

POINT 结构定义了以像素为单位的屏幕上某个点的 X 和 Y 坐标,它可以用来定位屏幕上的某个对象的坐标,如图形对象、窗口、鼠标点击时的位置等:

1
2
3
4
POINT STRUCT
ptX DWORD ?
ptY DWORD ?
POINT ENDS

RECT 结构

RECT 结构定义了一个矩形边界,left 字段为矩形左边界 X 坐标,top 字段为矩形顶边的 Y 坐标。right 和 bottom 字段值定义了矩形右下角的坐标:

1
2
3
4
5
6
RECT STRUCT
left DWORD ?
top DWORD ?
right DWORD ?
bottom DWORD ?
RECT ENDS

MSGStruct 结构

MSGStruct 结构定义了 Windows 消息需要的相关数据:

1
2
3
4
5
6
7
8
MSGStruct STRUCT
msgWnd DWORD ?
msgMessage DWORD ?
msgWparam DWORD ?
msgLparam DWORD ?
msgTime DWORD ?
msgPt POINT <>
MSGStruct ENDS

WNDCLASS 结构

WNDCLASS 结构定义了一个窗口类,程序中的每个窗口必须属于一个窗口类,所以每个程序必须为它的主窗口创建窗口类。在能够显示主窗口之前,窗口类必须先在系统里面注册:

1
2
3
4
5
6
7
8
9
10
11
12
WNDCLESS STRUCT
style DWORD ? ;窗口风格
lpfnWndProc DWORD ? ;窗口过程地址
cbClsExtra DWORD ? ;共享内存
cbWndExtra DWORD ? ;额外定义的数据
hInstance DWORD ? ;当前程序的句柄
hIcon DWORD ? ;图标句柄
hCursor DWORD ? ;光标句柄
hbrBackground DWORD ? ;背景画刷句柄
lpszMeunName DWORD ? ;菜单名称的指针
lpszClassName DWORD ? ;类名名称的指针
WNDCLASS ENDS

以下是这些字段的简要介绍:

  • style 是一些不同风格选项的组合,如 WS_CAPTION 和 WS_BORDER,这个字段影响窗口的外观和行为。
  • lpfnWndProc 是指向一个子程序的指针,这个子程序在我们自己的程序中,用来接收由用户触发的事件消息。
  • cbWndExtra 参数为每个窗口实例分配一些额外内存
  • hInstance 参数用来保存当前运行程序的句柄
  • hIcon 和 hCursor 参数为当前程序原使用的图标和光标句柄
  • hbrBackground 参数为背景颜色画刷的句柄。
  • lpszMeunName 指向一个菜单名称字符串。
  • lpszClassName 指向一个 0 结尾的窗口类名称字符串。

MessageBox 函数

48

WinMain 过程

49
50

ErrorHandler 过程

51

程序清单

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
;-------------------------------------------------------
;程序名:WinApp.asm
;功能:这个程序会显示可调整大小的窗口,并且弹出若干个消息框,
;特别感谢 Tom Joyce 提供本程序的第一个版本。
;作者:9unk
;编写时间:2023-4-17
;--------------------------------------------------------
INCLUDE Irvine32.inc
INCLUDE GraphWin.inc

;================================DATA================================================
.data
AppLoadMsgTitle BYTE "Application Loaded",0
AppLoadMsgText BYTE "This window was activated by a "
BYTE "VM_LBUTTONDOWN message",0
PopupTitle BYTE "Popup Window",0
PopupText BYTE "Main Window was activated by a"
BYTE "WM_LBUTTONDOWN message",0
GreetTitle BYTE "Main window Active",0
GreetText BYTE "This window is shown immediately after "
BYTE "CreateWindow and UpdateWindow are called.",0

CloseMsg BYTE "WM_CLOSE message received",0

ErrorTitle BYTE "Error",0
WindowName BYTE "ASM Windows App",0
className BYTE "ASMWin",0

;定义应用程序的窗口类结构
MainWin WNDCLASS <NULL,WinProc,NULL,NULL,NULL,NULL,NULL,COLOR_WINDOW,NULL,className>

msg MSGStruct <>
winRect RECT <>
hMainWnd DWORD ?
hInstance DWORD ?

;=================================CODE================================================
.code
WinMain PROC
;获取当前进程的句柄
INVOKE GetModuleHandle,NULL
mov hInstance,eax
mov MainWin.hInstance,eax
;加载程序的光标和图标
INVOKE LoadIcon,NULL,IDI_APPLICATION
mov MainWin.hIcon,eax
INVOKE LoadCursor,NULL,IDC_ARROW
mov MainWin.hCursor,eax
;注册窗口类
INVOKE RegisterClass,ADDR MainWin
.IF eax==0
call ErrorHandler
jmp Exit_Program
.ENDIF
;创建应用程序的主窗口
INVOKE CreateWindowEx,0,ADDR className,ADDR WindowName,MAIN_WINDOW_STYLE,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
NULL,NULL,hInstance,NULL
;如果 CreateWindowEx 失败,显示一条消息并退出
.IF eax == 0
call ErrorHandler
jmp Exit_Program
.ENDIF
;保存窗口句柄,并显示绘制窗口
mov hMainWnd,eax
INVOKE ShowWindow,hMainWnd,SW_SHOW
INVOKE UpdateWindow,hMainWnd

;显示欢迎消息
INVOKE MessageBox,hMainWnd,ADDR GreetText,ADDR GreetTitle,MB_OK

;开始程序的持续消息处理循环
Message_Loop:
;从队列中获取下一条消息
INVOKE GetMessage,ADDR msg,NULL,NULL,NULL

;若无消息则退出
.IF eax == 0
jmp Exit_Program
.ENDIF

;把消息转发给程序的WinProc过程
INVOKE DispatchMessage,ADDR msg
jmp Message_Loop
Exit_Program:
INVOKE ExitProcess,0
WinMain ENDP

;---------------------------------------------------------------------------------------------
WinProc PROC,
hwnd:DWORD,localMsg:DWORD,wParam:DWORD,lParam:DWORD
;应用程序的信息处理程序,它处理特定于应用程序的信息。所有其他的消息都被转发给默认的Windows消息处理程序
;---------------------------------------------------------------------------------------------
mov eax,localMsg

.IF eax == WM_LBUTTONDOWN ;鼠标按键消息
INVOKE MessageBox,hWnd,ADDR PopupText,
ADDR PopupTitle,MB_OK
jmp WinProcExit
.ELSEIF eax == WM_CREATE ;创建窗口消息
INVOKE MessageBox,hWnd,ADDR AppLoadMsgText,
ADDR AppLoadMsgTitle,MB_OK
jmp WinProcExit
.ELSEIF eax == WM_CLOSE ;关闭消息窗口
INVOKE MessageBox,hWnd,ADDR CloseMsg,
ADDR WindowName,MB_OK
INVOKE PostQuitMessage,0
jmp WinProcExit
.ELSE ;其他消息
INVOKE DefWindowProc,hWnd,localMsg,wParam,lParam
jmp WinProcExit
.ENDIF

WinProcExit:
ret
WinProc ENDP

;------------------------
ErrorHandler PROC
;显示相应的系统错误信息
;------------------------
.data
pErrorMsg DWORD ? ;指向错误消息指针
messageID DWORD ?

.code
INVOKE GetLastError ;在 EAX 中返回错误消息的指针
mov messageID,eax

;显示错误消息
INVOKE MessageBox,NULL,pErrorMsg,ADDR ErrorTitle,
MB_ICONERROR+MB_OK

;释放消息字符串
INVOKE LocalFree,pErrorMsg
ret
ErrorHandler ENDP
END WinMain

动态内存分配

内存管理

静态内存分配

源码中数组、变量和结构体数据,程序在编译时会自动分配相应的内存空间,而这一块内存空间在程序运行后是无法变动的,所以称为静态内存分配。但是静态分配的缺点也很明显容易出现内存空间不够用或内存空间浪费的现象。
比如一个数组分配了 20 个字节,但是在执行程序的时候发现这 20 个字节的空间不够用,此时程序就会发生错误。那我给这个数组分配 1000 个字节,现在内存是够用了,但是我实际使用了时候用不完这些内存,这就会造成内存空间的浪费。为了解决这个问题,因此动态内存分配就产生了。

动态内存分配

在写汇编期间,我们可以使用自定分分配堆栈的大小,默认分配 128 字节。在现在的 windows 中堆默认分配 1M~2GGB 的堆空间,同时也可以手动分配堆的大小。

堆栈

54

注:上面两块空白的区域都是堆栈。

  1. 栈:由系统自动分配的内存空间叫栈
  2. 堆:由用户手动分配的内存空间叫堆

栈用于存储小块的数据,例如:push、call指令存储的数据和局部变量;堆用于大块的数据存储。

内存分配的使用

在编写程序时,所需的内存空间是确定的,就使用静态内存分配。如果需要使用的内存空间是不确定的,那就需要使用动态内存分配。堆空间会根据所需数据的大小分配堆空间,但不能超过堆空间的最大值。堆空间的自动分配是由操作系统自动调用 malloc 动态分配堆空间。
55

动态内存分配的流程

  1. 使用 GetProcessHeap(获取默认堆的句柄)或 HeapCreate(创建新的堆并获取堆句柄)
  2. 使用 HeapAlloc 在堆中分配一块内存
  3. 使用 HeapReAlloc 调整堆中内存块的大小
  4. 使用 HeapSize 返回 HeapAlloc 或 HeapReAlloc 分配内存块的大小
  5. 使用 HeapFree 释放堆中的内存
  6. 使用 HeapDestroy 销毁堆(公有的堆不能销毁,会导致程序错误)

实操

动态内存分配也称为堆(内存)分配(Heap Allocation),是程序设计语言提供的一种非常有用的工具,用户创建的对象、数组和其他结构保留内存。例如,在 JAVA 中,类似下面的语句会导致程序为创建的 String 对象保留内存:

1
String str = new String("abcde");

类似地,在 C++ 中,可能会需要为一个整数数组分配内存空间,其大小来自于一个变量:

1
2
3
int size;   
cin >> size;
int array[] = new int[size]

C/C++ 和 JAVA 都有内建地运行时堆栈管理,用于处理程序地存储分配和存储释放请求,堆管理器通常在程序启动时请求操作系统分配一个大块地内存,堆管理器创建一个空闲存储块指针链表,在接到分配请求时,吧一个合适地内存块标记为保留并返回指向该内存块地指针,其后在接到针对同一内存块地释放请求时,堆管理器把该内存块放回空闲存储块的指针链表中(或释放该内存块)。每次新的分配请求到达时,堆管理器都会先扫描空闲存储块链表,查找第一个足够大的内存块以满足分配请求。

汇编语言可通过多种方式进行动态内存分配:第一种方式是通过系统调用让操作系统为其分配内存块。第二种方式是实现字节堆管理器以处理小对象的内存分配请求。本节只讲述第一种方法。
52

GetProcessHeap

GetProcessHeap:如果对使用当前程序拥有的默认堆,可以使用 GetProcessHeap 函数,该函数无参数,在 eax 中返回默认的堆句柄。函数原型如下:

1
GetProcessHeap PROTO

调用示例:

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
;------------------------------------------
;程序名:GetHeap.asm
;功能:使用 GetProcessHeap 获取该程序的默认堆。
;作者:9unk
;编写时间:2023-4-22
;-------------------------------------------
INCLUDE Irvine32.inc
INCLUDE Macros.inc
.data
hHeap HANDLE ? ;存储默认堆的句柄

.code
main PROC
INVOKE GetProcessHeap
.IF eax == NULL
mWrite <"Not getting the default heap handle">
jmp quit
.ELSE
mov hHeap,eax
mWrite <"The defalut heap handle is stored in hHeap: 0x">
mov eax,hHeap
call WriteHex
.ENDIF
quit:
exit
main ENDP
END main

HeapCreate

HeapCreate: 允许为当前程序创建新的私有堆。函数原型如下:

1
2
3
4
HeapCreate PROTO,
flOptions:DWORD ;堆分配选项
dwInitialSize:DWORD, ;堆的初始大小,以字节为单位
dwMaximumSize:DWORD ;堆的最大尺寸值,以字节为单位

调用时把 flOptions 设为 NULL,把 dwInitialSize 设置为堆的初始大小,实际的初始大小是该值按边界向上舍入后的值。当调用 HeapAlloc 分配内存块时,如果堆的大小超过堆的初始大小,堆将自动增长,上限是 dwMaximumSize 参数(按页边界向上舍入)指定的值。在调用该函数之后,如果堆未成功创建,则在 EAX 中返回 NULL。

下面是调用示例:

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
;-------------------------------
;程序名:HeapCreate.asm
;功能:使用 HeapCreate 创建一个堆
;作者:9unk
;编写时间:2023-4-22
;--------------------------------
INCLUDE Irvine32.inc
INCLUDE Macros.inc

HEAP_START = 2000000 ;2M
HEAP_MAX = 400000000 ;400M

.data
hHeap HANDLE ?

.code
main PROC
INVOKE HeapCreate,0,HEAP_START,HEAP_MAX
.IF eax == NULL
call WriteWindowsMsg
jmp quit
.ELSE
mov hHeap,eax
mWrite <"The Create heap handle is stored in hHeap: 0x">
mov eax,hHeap
call WriteHex
.ENDIF
quit:
exit
main ENDP
END main

HeapDestroy

HeapDestroy 销毁一个显存的私有堆(通过调用 HeapCreate 创建的)。调用时传递要销毁的堆的句柄:

1
2
HeapDestroy PROTO,
hHeap:DWORD ;堆的句柄

如果销毁堆的失败,则 EAX 中返回 NULL。下面是 HeapDestroy 的调用示例:

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
;-------------------------------
;程序名:HeapDestroy.asm
;功能:使用 HeapDestroy 销毁一个堆
;作者:9unk
;编写时间:2023-4-22
;--------------------------------
INCLUDE Irvine32.inc
INCLUDE Macros.inc

HEAP_START = 2000000 ;2M
HEAP_MAX = 400000000 ;400M

.data
hHeap HANDLE ?

.code
main PROC
INVOKE HeapCreate,0,HEAP_START,HEAP_MAX
.IF eax == NULL
call WriteWindowsMsg
jmp quit
.ELSE
mov hHeap,eax
mWrite <"The Create heap handle is stored in hHeap: 0x">
mov eax,hHeap
call WriteHex
call Crlf
INVOKE HeapDestroy,hHeap
.IF eax == NULL
call WriteWindowsMsg
.ELSE
mWrite <"Successfully Destroy the created heap">
.ENDIF
.ENDIF
quit:
exit
main ENDP
END main

HeapAlloc:HeapAlloc 从堆中分配一块内存。函数原型如下:

1
2
3
4
HeapAlloc PROTO,
hHeap:HANDLE, ;堆的句柄
dwFlags:DWORD, ;堆分配控制标志
dwBytes:DWORD ;要分配的字节数

参数解释:

  • hHeap 是通过调用 GetProcessHeap 或 HeapCreate 获取的 32 位堆句柄。
  • dwFlags 是包含一个或多个标志值的双字,可以把该值设为 HEAP_ZERO_MEMORY,此时分配的内存块将以 0 初始化。
  • dwBytes 是表示要分配的内存块大小的双字,大小以字节为单位计算的。

如果调用成功,EAX 中返回分配的内存块的指针;如果调用失败,EAX 中的返回 NULL。下面的代码从 hHeap 表示的堆中分配 1000 个字节并以 0 初始化。

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
;------------------------------------------
;程序名:HeapAlloc.asm
;功能:使用 HeapAlloc 在默认堆中初始化一个内存空间。
;作者:9unk
;编写时间:2023-4-22
;-------------------------------------------
INCLUDE Irvine32.inc
INCLUDE Macros.inc

AllocBytes = 1000

.data
hHeap HANDLE ? ;存储默认堆的句柄

.code
main PROC
;获取默认堆
INVOKE GetProcessHeap
.IF eax == NULL
mWrite <"Not getting the default heap handle">
jmp quit
.ELSE
mov hHeap,eax
mWrite <"The defalut heap handle is stored in hHeap: 0x">
mov eax,hHeap
call WriteHex
call Crlf
;从默认堆中分配 1 MB 空间的内存,内存数据以 0 填充初始化
INVOKE HeapAlloc,hHeap,HEAP_ZERO_MEMORY,AllocBytes
.IF eax == NULL
mWrite <"HeapAlloc failed">
.ELSE
mWrite <"HeapAlloc success">
.ENDIF
.ENDIF
quit:
exit
main ENDP
END main

HeapFree

HeapFree 释放以前从堆中分配的内存块,内存块是以堆句柄和内存块的地址标识的:

1
2
3
4
HeapFree PROTO,
hHeap:HANDLE,
dwFlags:DWORD,
lpMem:DWORD
  • 第一个参乎上主要是包含要释放内存块的堆句柄
  • 第二个参数通常是 0
  • 第三个参数指的是指向要释放内存块的指针。

如果内存块成功释放,返回非 0 值;如果释放失败,则返回 0 。下面是调用示例:

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
;------------------------------------------
;程序名:HeapFree.asm
;功能:使用 HeapFree 在释放堆中分配的内存。
;作者:9unk
;编写时间:2023-4-22
;-------------------------------------------
INCLUDE Irvine32.inc
INCLUDE Macros.inc

AllocBytes = 1000

.data
hHeap HANDLE ? ;存储默认堆的句柄
mHeap HANDLE ? ;存储在堆中分配的内存句柄
.code
main PROC
;获取默认堆
INVOKE GetProcessHeap
.IF eax == NULL
mWrite <"Not getting the default heap handle">
jmp quit
.ELSE
mov hHeap,eax
mWrite <"The defalut heap handle is stored in hHeap: 0x">
mov eax,hHeap
call WriteHex
call Crlf
;从默认堆中分配 1 MB 空间的内存,内存数据以 0 填充初始化
INVOKE HeapAlloc,hHeap,HEAP_ZERO_MEMORY,AllocBytes
mov mHeap,eax
.IF eax == NULL
mWrite <"HeapAlloc failed",0dh,0ah>
.ELSE
mWrite <"HeapAlloc success",0dh,0ah>
.ENDIF
;从堆中释放分配的 1MB 空间的内存
INVOKE HeapFree,hHeap,0,mHeap
.IF eax == NULL
mWrite <"HeapFree failed",0dh,0ah>
.ELSE
mWrite <"HeapFree success",0dh,0ah>
.ENDIF
.ENDIF
quit:
exit
main ENDP
END main

53

堆测试程序

Heaptest1

下面的例子(Heaptest1.asm)使用动态内存分配的方法创建一个 1000 字节的数组,使用的是进程的默认堆:

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
;---------------------------
;程序名:Heaptest1.asm
;功能:这个程序使用动态内存分配来分配和填充一个字节数组
;作者:9unk
;编写时间:2023-4-22
;----------------------------
INCLUDE Irvine32.inc
.data
ARRAY_SIZE = 1000
FILL_VAL EQU 0FFh

hHeap HANDLE ? ;进程的堆句柄
pArray DWORD ? ;内存块的指针
newHeap DWORD ? ;新堆的句柄
str1 BYTE "Heap size is: ",0

.code
main PROC
INVOKE GetProcessHeap ;获取程序的默认堆的句柄
.IF eax == NULL
call WriteWindowsMsg
jmp quit
.ELSE
mov hHeap,eax
.ENDIF

call allocate_array
jnc arrayOK ;失败了(CF=1)
call WriteWindowsMsg
call Crlf
jmp quit
arrayOK:
call fill_array
call display_array
call Crlf

;free the array
INVOKE HeapFree,hHeap,0,pArray
quit:
exit
main ENDP

;----------------------------------------------
allocate_array PROC USES eax
;功能:为数组动态分配空格符
;入口参数:无
;出口参数:无
;----------------------------------------------
INVOKE HeapAlloc,hHeap,HEAP_ZERO_MEMORY,ARRAY_SIZE
.IF eax == NULL
stc
.ELSE
mov pArray,eax
clc
.ENDIF

ret
allocate_array ENDP

;------------------------------------------------
fill_array PROC USES eax ebx ecx esi
;功能:使用 FILL_VAL 填充数组
;入口参数:无
;出口参数:无
;-------------------------------------------------
mov ecx,ARRAY_SIZE
mov esi,pArray
L1:
mov BYTE PTR [esi],FILL_VAL ;填充每一字节
inc esi
loop L1

ret
fill_array ENDP

;------------------------------------------------
display_array PROC USES eax ebx ecx esi
;功能:显示数组
;入口参数:无
;出口参数:无
;-------------------------------------------------
mov ecx,ARRAY_SIZE ;循环计数器
mov esi,pArray ;指向数组
L1:
mov al,[esi] ;去一个字节
mov ebx,TYPE BYTE
call WriteHexB ;显示之
inc esi
loop L1

ret
display_array ENDP
END main

Heaptest2

下面的例子使用动态内存分配的方法循环分配 2000 个大约 0.5MB 的内存块:

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
;---------------------------
;程序名:Heaptest2.asm
;功能:使用动态内存分配的方法循环分配 2000 个大约 0.5MB 的内存块
;作者:9unk
;编写时间:2023-4-22
;----------------------------
INCLUDE Irvine32.inc
.data
HEAP_START = 2000000 ;2MB
HEAP_MAX = 400000000 ;400MB
BLOCK_SIZE = 500000 ;0.5MB

hHeap HANDLE ? ;堆的句柄
pData DWORD ? ;内存块的指针

str1 BYTE 0dh,0ah,"Memory allocation failed",0dh,0ah,0

.code
main PROC
INVOKE HeapCreate,0,HEAP_START,HEAP_MAX

.IF eax == NULL
call WriteWindowsMsg
call Crlf
jmp quit
.ELSE
mov hHeap,eax
.ENDIF

mov ecx,2000 ;循环计数器
L1:
call allocate_block ;每次循环分配一块内存
.IF Carry?
mov edx,OFFSET str1
call WriteString
jmp quit
.ELSE
mov al,'.'
call WriteChar
.ENDIF
;每次循环释放分配的内存块,注释后会发生报错
;因为每循环一次需要分配 0.5MB 的内存块
;循环 2000 次,需要 1000MB 的堆空间已超过 HEAP_MAX 的上线
;最后肯定是申请内存块失败
call free_block
loop L1
quit:
INVOKE HeapDestroy,hHeap ;销毁堆
.IF eax == NULL
call WriteWindowsMsg
call Crlf
.ENDIF

exit
main ENDP

allocate_block PROC USES ecx
;分配一块内存并以 0 填充
INVOKE HeapAlloc,hHeap,HEAP_ZERO_MEMORY,BLOCK_SIZE

.IF eax == NULL
stc ;返回时 CF = 1
.ELSE
mov pData,eax ;保存指针
clc ;返回时 CF = 0
.ENDIF
ret
allocate_block ENDP

free_block PROC USES ecx
INVOKE HeapFree,hHeap,0,pData
ret
free_block ENDP
END main

IA-32内存管理

本节的内容主要集中在内存管理的两个主要方面:

  • 从逻辑地址到线性地址的转换。
  • 从线性地址到物理地址的转换(分页)。

线性地址

逻辑地址到线性地址的转换

多任务操作系统允许多个程序(任务)同时在内存中运行,每个程序用用属于它自己的唯一的数据空间。假设有三个程序,每个程序都在偏移地址 200h 处有一个变量,这三个变量是如何互相隔离呢?答案是:IA-32 处理器使用了一个经过一步或两个步骤的过程把每个变量的地址转换到另一个唯一的地址上去。

第一步把变量的段和偏移地址合成一个线性地址,线性地址有可能是变量的物理地址,但是有些操作系统(如windows或者linux)使用了称为 IA-32 的分页技术,使程序能够使用比计算机中实际物理内存更多的线性地址空间。如果情况是这样,就要经过第二个步骤,使用页面转换的方法把线性地址转换到物理地址。

下图展示了处理器如何使用段和偏移地址来确定一个变量的线性地址。每个段选择子指向描述符表里面的段描述符,段描述符包含了段的基地址(起始地址),逻辑地址中的 32 为偏移地址和段的基址相加就得到了线性地址。

56

线性地址:线性地址是一个介于0~FFFFFFFF的32位整数,它代表内存中的一个位置。如果分页机制没有打开的话,那么线性地址实际上就是数据的物理地址。

分页机制是 IA-32 系列处理器的一个重要特征,它使得计算机同时在内存中运行原本无法装入的一堆程序成为可能。

在一开始处理器仅仅装入程序的一部分,剩余的部分保留在磁盘上面。程序要用到的内存被划分称为页的小块,通常每块的大小为 4 KB。运行每个程序的时候,处理器有选择的在内存中释放不用的页面,然后装入其他马上要被用到的页面。

操作系统使用一个页目录和一系列的页表来追踪内存中所有程序的页面使用情况。当一个程序尝试访问线性地址空间中某个地址的时候,处理器自动把线性地址转换成物理地址,这个转换称为页面转换。如果需要的页面尚未在内存中,处理器打断程序的执行并引发一个页错误,操作系统捕获到这个错误并在程序恢复运行前把所需的页面从磁盘复制到内存中。从应用程序的角度看,页错误和页面转换是自动发生的。

举例来说,下面的 winxp 虚拟机,分配了 1GB 的内存空间,但是系统显示的最大内存是2GB。
57

描述符表

段描述符存在于两种类型的表中:全局描述符表(GDT)和局部变量描述符表(LDT)

全局描述符表(GDT): 系统中只存在一个全局描述符表,系统在处理器切换到保护模式时创建全局描述符表,表的基址存放在 GDTR(全局描述符表寄存器)里面。表中的项目(称为段描述符)指向各个段。操作系统可以把所有程序都要使用的段存放在 GDT 中。

局部描述符表(LDT): 在一个多任务的操作系统中,每个程序或任务都有它自己的段描述符表,这个表称为局部描述符表(LDT)。当前程序的 LDT 的基址存放在 LDTR(局部描述符表寄存器)中。每个段描述符都包含了段在线性地址空间中的基址。如下图所示:
58

一个段和其他段通常是不同的。图中显示了三个不同的逻辑地址,每个地址分别对应于 LDT 中的不同表项。在这个例子中,假设分页机制是关闭的,所以线性地址空间也就是物理地址空间。

段描述符的细节

段描述符中除了包含段的基址以外,有些数据位定义了段的限长和段类型。代码段是一个只读段的例子,如果程序尝试修改代码段的内容,那么处理器会产生一个页异常。段描述符中页包含保护级别,这样可以预防应用程序访问操作系统使用的数据。下面是段描述符各个域的含义:

  • 基地址: 是一个 32 位的整数,定义了段在 4GB 的线性地址空间中的起始地址。
  • 特权级: 每个段都有一个0~3级之间的权限等级,其中0级是最高级,通常被操作系统的核心代码所使用。如果低优先级(优先级数字大)的程序尝试去存取高优先级(优先级数字小)的段,那么处理器会产生一个异常。
  • 段类型: 用来指明段的类型以及可以对这个段进行的访问方式,还有段的扩展方向(向上或向下)。数据段(包括堆栈段)可以是只读或者可读写的,可以是向上或向下扩展。代码段可以仅仅是可执行的或者是可执行/可读的。
  • 段存在标志: 这个数据位指明段当前是否在物理内存中存在。
  • 粒度标志: 用来决定如何解释段限长域的数值,如果标志位清零,那么段长的单位是字节。如果该标志位置位,那么段限长是 4096 字节。
  • 段限长: 是一个 20 位的整数,表示段的长度,它根据粒度标志的值按下面的两种方式解释:
    • 1 字节到 1MB 字节的段长度。
    • 4096字节到 4GB 字节的段长度。

在8086汇编中段寄存器的值是可以修改段的,但是到了 80386 系统中,为了保护内存中的系统数据不被修改,因此设计了段选择器这套方案。这样用户不能修改段寄存器,从而使得高 2GB 空间(系统数据存储空间)的数据不会被任意修改。

页面地址转换

当分页机制被允许的时候,处理器必须把 32 位的线性地址转换到 32 位的物理地址,在这个过程中要使用一下三个数据结构:

  • 页目录:一个最多包含 1024 个 32 位表项的页表地址表。
  • 页表:一个最多包含 1024 个 32 位表项的页地址表。
  • 页:一个 4KB 或者 4MB 的地址空间。

为了简单起见,下面讨论中假设使用 4KB 的页。
一个线性地址可以被分为三个部分:指向页目录的指针、指向页表的指针和在页中的偏移地址。页目录的起始地址存放在控制寄存器(CR3)中。如下图所示,当线性地址被转换到物理地址的时候,处理器执行了以下步骤:
59

  1. 线性地址代表线性地址空间的一个位置
  2. 以线性地址中 10 位的页目录域作为索引,从页目录表中得到页表入口项,页表入口项中包含了页表地址。
  3. 以线性地址中 10 位的页表域作为索引,从页表入口项中得到页在物理内存中的基址
  4. 线性地址中 12 位的偏移地址域加上页的基址,就得到了操作数确切的物理地址

页机制本质上是为了节省内存空间,程序运行时会生成多个页存储在exe程序所在的磁盘中,当某个页中的数据需要加载到内存时,系统才会把这个页加载到内存;当这个页的数据用不到的时候,就会交换页,把其他需要的页加载到内存中,而之前加载的页会重新存储到磁盘中。
页机制还有整理碎片化内存块的能力,它可将多个碎片化的物理内存地址,连续地写到页中,当对整个页进行读写时,就能达到整理碎片化内存块的能力。

60

  • 本文标题:win32汇编-windows程序设计(预习篇)
  • 本文作者:9unk
  • 创建时间:2023-04-10 14:06:00
  • 本文链接:https://9unkk.github.io/2023/04/10/win32-hui-bian-windows-cheng-xu-she-ji-yu-xi-pian/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!