简介 《Inetel 汇编语言程序设计》这本书中涉及了很多知识点,但大多都是简单地提了一下,知识点写的并不深入。这篇 win32 汇编算是提前预习了解,后续会更加深入学习这些知识点。
什么是windows程序 用汇编写 windows 程序,这个就是 win32 汇编。windows程序是面向对象的程序,我们之前学习的内容更多的侧重于算法,按照代码的顺序执行(面向过程的程序)。windows程序是一种消息驱动的程序,根据消息传递的顺序来执行的,而不是按写代码的顺序执行的。
背景知识 控制台程序
补充: 在学习8086汇编的过程中,写过创建一个窗口,显示显存中的内容。控制台显示字符的原理就是:将窗口定位到显存的某个地址,再输出显示该线性地址的内容。只不过在win32中这个窗口和具体显存地址是由windows系统自动创建分配。
Win32 API 的参考信息
高级操作和底层操作
windows 的数据类型
SmallWin.inc 包含文件
控制台句柄
Win32控制台函数
显示消息框
程序清单: 由于 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
控制台输入 本书中的 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
错误检查
单字符的输入
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
获取键盘状态 调用 GetKeyState API 函数可以测试单个按键的状态,查看其是否正被按下。函数原型如下:
1 GetKeyState PROTO,nVirtKey:DWORD
调用 GetKeyState 时,应传递一个要检查按键的虚拟键码,在函数返回后,应测试EAX中的相应位是否置位了。一些虚拟键码的值以及应检查的对应数据位如下表:
下面的例子程序演示了 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 函数
Irvine32 库的文件 I/O 过程
测试文件 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
控制台窗口的操作
下面的 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
光标的控制
文本颜色的控制
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
严格意义上来说颜色和字符复制的缓冲区不在同一个位置,只是函数忽略了这个细节,看上去是同一个位置。学过 8086 汇编的都应该能理解。
时间和日期函数 下面的例子程序 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
编写windows 图形界面应用程序
必须了解的数据结构 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 函数
WinMain 过程
ErrorHandler 过程
程序清单 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 的堆空间,同时也可以手动分配堆的大小。
堆栈
注:上面两块空白的区域都是堆栈。
栈:由系统自动分配的内存空间叫栈
堆:由用户手动分配的内存空间叫堆
栈用于存储小块的数据,例如:push、call指令存储的数据和局部变量;堆用于大块的数据存储。
内存分配的使用 在编写程序时,所需的内存空间是确定的,就使用静态内存分配。如果需要使用的内存空间是不确定的,那就需要使用动态内存分配。堆空间会根据所需数据的大小分配堆空间,但不能超过堆空间的最大值。堆空间的自动分配是由操作系统自动调用 malloc 动态分配堆空间。
动态内存分配的流程
使用 GetProcessHeap(获取默认堆的句柄)或 HeapCreate(创建新的堆并获取堆句柄)
使用 HeapAlloc 在堆中分配一块内存
使用 HeapReAlloc 调整堆中内存块的大小
使用 HeapSize 返回 HeapAlloc 或 HeapReAlloc 分配内存块的大小
使用 HeapFree 释放堆中的内存
使用 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 都有内建地运行时堆栈管理,用于处理程序地存储分配和存储释放请求,堆管理器通常在程序启动时请求操作系统分配一个大块地内存,堆管理器创建一个空闲存储块指针链表,在接到分配请求时,吧一个合适地内存块标记为保留并返回指向该内存块地指针,其后在接到针对同一内存块地释放请求时,堆管理器把该内存块放回空闲存储块的指针链表中(或释放该内存块)。每次新的分配请求到达时,堆管理器都会先扫描空闲存储块链表,查找第一个足够大的内存块以满足分配请求。
汇编语言可通过多种方式进行动态内存分配:第一种方式是通过系统调用让操作系统为其分配内存块。第二种方式是实现字节堆管理器以处理小对象的内存分配请求。本节只讲述第一种方法。
GetProcessHeap GetProcessHeap:如果对使用当前程序拥有的默认堆,可以使用 GetProcessHeap 函数,该函数无参数,在 eax 中返回默认的堆句柄。函数原型如下:
调用示例:
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
堆测试程序 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 为偏移地址和段的基址相加就得到了线性地址。
线性地址:线性地址是一个介于0~FFFFFFFF的32位整数,它代表内存中的一个位置。如果分页机制没有打开的话,那么线性地址实际上就是数据的物理地址。
分页机制是 IA-32 系列处理器的一个重要特征,它使得计算机同时在内存中运行原本无法装入的一堆程序成为可能。
在一开始处理器仅仅装入程序的一部分,剩余的部分保留在磁盘上面。程序要用到的内存被划分称为页的小块,通常每块的大小为 4 KB。运行每个程序的时候,处理器有选择的在内存中释放不用的页面,然后装入其他马上要被用到的页面。
操作系统使用一个页目录和一系列的页表来追踪内存中所有程序的页面使用情况。当一个程序尝试访问线性地址空间中某个地址的时候,处理器自动把线性地址转换成物理地址,这个转换称为页面转换。如果需要的页面尚未在内存中,处理器打断程序的执行并引发一个页错误,操作系统捕获到这个错误并在程序恢复运行前把所需的页面从磁盘复制到内存中。从应用程序的角度看,页错误和页面转换是自动发生的。
举例来说,下面的 winxp 虚拟机,分配了 1GB 的内存空间,但是系统显示的最大内存是2GB。
描述符表 段描述符存在于两种类型的表中:全局描述符表(GDT)和局部变量描述符表(LDT)
全局描述符表(GDT): 系统中只存在一个全局描述符表,系统在处理器切换到保护模式时创建全局描述符表,表的基址存放在 GDTR(全局描述符表寄存器)里面。表中的项目(称为段描述符)指向各个段。操作系统可以把所有程序都要使用的段存放在 GDT 中。
局部描述符表(LDT): 在一个多任务的操作系统中,每个程序或任务都有它自己的段描述符表,这个表称为局部描述符表(LDT)。当前程序的 LDT 的基址存放在 LDTR(局部描述符表寄存器)中。每个段描述符都包含了段在线性地址空间中的基址。如下图所示:
一个段和其他段通常是不同的。图中显示了三个不同的逻辑地址,每个地址分别对应于 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)中。如下图所示,当线性地址被转换到物理地址的时候,处理器执行了以下步骤:
线性地址代表线性地址空间的一个位置
以线性地址中 10 位的页目录域作为索引,从页目录表中得到页表入口项,页表入口项中包含了页表地址。
以线性地址中 10 位的页表域作为索引,从页表入口项中得到页在物理内存中的基址
线性地址中 12 位的偏移地址域加上页的基址,就得到了操作数确切的物理地址
页机制本质上是为了节省内存空间,程序运行时会生成多个页存储在exe程序所在的磁盘中,当某个页中的数据需要加载到内存时,系统才会把这个页加载到内存;当这个页的数据用不到的时候,就会交换页,把其他需要的页加载到内存中,而之前加载的页会重新存储到磁盘中。 页机制还有整理碎片化内存块的能力,它可将多个碎片化的物理内存地址,连续地写到页中,当对整个页进行读写时,就能达到整理碎片化内存块的能力。