《汇编语言:基于X86处理器》第5章 过程(2)
本章介绍过程,也称为子程序或函数。任何具有一定规模的程序都需要被划分为几个部分,其中某些部分要被使用多次。读者会发现寄存器可以传递参数,也将了解为了追踪过程的调用位置,CPU 使用的运行时堆栈。最后,本章会介绍本书提供的两个代码库,分别称为 Irvine 32 和 Irvine 64,其中包含了有用的工具来简化输入输出。
5.4 Irvine32链接库
5.4.1 创建库的动机
汇编语言编程没有 Microsoft 认可的标准库。在20 世纪 80年代早期,程序员第一次开始为 x86 处理器编写汇编语言时,MS-DOS 是常用的操作系统。这些 16 位程序可以调用 MS-DOS 函数(即INT 21h服务)来实现简单的输入输出。即使是在那个时代,如果想在控制台上显示一个整数,也需要编写一个相当复杂的程序,将整数的内部二进制表示转换为可以在屏幕上显示的 ASCII 字符序列。这个过程被称为 WriteInt,下面是其抽象为伪代码的逻辑:
初始化:
let n equal the binary value
let buffer be an array of char[size]
算法:
i = size - 1 ;缓冲区最后一个位置
repeatr = n mod 10 ;余数n = n / 10 ;整数除法digit = r or 30h ;将r转换为ASCII数字buffer[i--] = digit ;保存到缓冲区
until n = 0
if n is negativebuffer[i] = "-" ;插入负号
while i > 0printf buffer[i]i++
注意,数字是按照逆序生成,插入缓冲区,从后往前移动。然后,数字按照正序写到控制台。虽然这段代码简单到足以用 C/C++实现,但是如果是在汇编语言中,它还需要一些高级技巧。
专业程序员通常更愿意自己建立库,这是一种很好的学习经验。在Windows 的 32 位模式下,输入输出库必须能直接调用操作系统的内容。这个学习曲线相当陡峭,对编程初学者提出了一些挑战。因此,lrvine32 链接库被设计成给初学者提供简单的输入输出接口。随着对本书学习的推进,读者将能获得自己创建库的知识和技术。只要成为库的创建者,就能自由地修改和重用库。第 13 章将讨论另一种方法,即从汇编语言程序中调用标准C库函数。同样,这种方法也需要一些其他的背景知识。
表5-1列出了Irvine32链接库的全部过程。
续表
5.4.2 概述
控制台窗口控制台窗口(console window)(或命令窗口command window)是显示命令提示符时,由MS-Windows生成的一个纯文本窗口。
要想Microsoft Windows 中显示一个控制台窗口,在桌面上单击Start 按钮,并在 StartSearch 框中输入cmd,然后单击回车。打开控制台窗口后,右键点击窗口左上角的系统菜单,就可以重新定义控制台窗口缓冲区的大小,从弹出菜单中选择 Properties,然后修改数值,如图 5-10 所示。
还可以选择不同的字体大小和颜色。默认的控制台窗口为 25 行x80 列,使用 mode 命令可以修改它的行数和列数。例如,在命令提示符下键入下述内容,则将控制台窗口设置为30 行x40列:
mode con cols = 40 lines = 30
文件句柄(file handle)是一个32 位整数,Windows 操作系统用它来标识当前打开的文件。当用户程序调用一个Windows服务来打开或创建文件时,操作系统就创建一个新的文件句柄,并使其对用户程序可用。每当程序调用 OS 服务方法来读写该文件时,就必须将这个文件句柄作为参数传递给服务方法。
注意: 如果用户程序调用Irvine32链接库中的过程,就必须总是将这个32位数值压人运行时堆栈;如果不这样做,被库调用的 Win32 控制台函数就不能正常工作。
图5-10 修改控制台窗口属性
5.4.3 过程详细说明
本节将逐一介绍Irvine32链接库中的过程是如何使用的,同时也会忽略一些更高级的过程,它们将在后续章节中进行解释。
CloseFile CloseFile过程关闭之前已经创建或打开的文件(参见CreateOutputFile 和OpenInputFile)。该文件用一个32位整数的句柄来标识,句柄由EAX传递。如果文件成功关闭,EAX中的返回值就是非零的。示例如下:
mov eax, fileHandle
call CloseFile
Clrscr其他时间调用这个过程,就需要先调用WaitMsg来暂停程序,这样就可以让用户在屏幕被清除之前,阅读屏幕上的信息。调用示例如下:
call WaitMsq:"Press any key..."
call Clrscr
完整代码测试笔记
; 5.4.3_1.asm WaitMsg来暂停程序, Clrscr清屏调用INCLUDE Irvine32.inc.code
main PROCcall WaitMsg ; "Press any key..."call ClrscrINVOKE ExitProcess,0
main ENDP
END main
运行调试:
CreateOutputFile CreateOutputFile 过程创建并打开一个新的磁盘文件,进行写操作。调用该过程时,将文件名的偏移量送人EDX。过程返回后,如果文件创建成功则EAX将包含一个有效文件句柄(32位整数),否则,EAX将等于INVAL_IDHANDLE_VALUE(一个预定义的常数)。调用示例如下:
.data
filename BYTE "newfile.txt",0
.code
mov edx, OFFSET filename
call CreateOutputFile
完整代码测试笔记
; 5.4.3_2.asm CreateOutputFile过程创建并打开一个新的磁盘文件INCLUDE Irvine32.inc.data
filename BYTE "newfile.txt",0.code
main PROCmov edx, OFFSET filenamecall CreateOutputFileINVOKE ExitProcess,0
main ENDP
END main
运行调试:
下面的伪代码描述的是调用 CreateOutputFile 之后,可能会出现的结果:
if EAX =INVALID_HANDLE_VALUEthe file was not created successfully
elseEAX = handle for the open file
endif
Crif Crlf过程将光标定位在控制台窗口下一行的开始位置。它写的字符串包含了ASCI 字符代码 0Dh 和 0Ah。调用示例如下:
call Crlf
Delay Delay 过程按照特定毫秒数暂停程序。在调用 Delay 之前,将预定时间间隔送人 EAX。调用示例如下:
mov eax,1000 ;1 秒
call Delay
完整代码测试笔记
; 5.4.3_3.asm Delay函数示例INCLUDE Irvine32.inc.code
main PROCmov eax, 1000 ;1秒call DelayINVOKE ExitProcess,0
main ENDP
END main
运行调试:
DumpMen DumpMen 过程在控制台窗口中用十六进制的形式显示一段内存区域。ESI中存放的是内存区域首地址;ECX 中存放的是单元个数;EBX中存放的是单元大小(1=字节,2= 字,4=双字)。下述调用示例用十六进制形式显示了包含11 个双字的数组:
完整代码测试笔记
; 5.4.3_4.asm INCLUDE Irvine32.inc.data
array DWORD 1,2,3,4,5,6,7,8,9,0Ah,0Bh.code
main PROCmov esi, OFFSET array ;首地址偏移量mov ecx, LENGTHOF array ;单元个数mov ebx, TYPE array ;双字格式call DumpMemINVOKE ExitProcess,0
main ENDP
END main
产生的输出如下所示:
0000 0001 0000 0002 0000 0003 0000 0004 0000 0005 0000 0006
0000 0007 0000 0008 0000 0009 0000 000A 0000 000B
DumpRegs DumpRegs过程用十六进制形式显示EAXEBX、ECX、EDX、ESI、EDI、EBP、ESP、EIP和EFL(EFLAGS)的内容,以及进位标志位、符号标志位、零标志位、溢出标志位、辅助进位标志位和奇偶标志位的值。调用示例如下:
call DumpRegs
完整代码测试笔记
; 5.4.3_5.asm INCLUDE Irvine32.inc.code
main PROCcall DumpRegsINVOKE ExitProcess,0
main ENDP
END main
示例输出如下所示:
EIP 显示的数值是调用DumpRegs的下一条指令的偏移量。DumpRegs 在调试程序时很有用,因为它显示了 CPU 快照。该过程没有输入参数和返回值。
GetCommandTail GetCommandTail过程将程序命令行复制到一个空字节结束的字符串。如果命令行是空,则进位标志位置1;否则进位标志位清零。该过程的作用在于能让程序用户通过命令行传递参数。假设有一程序Encrypt.exe读取输入文件filel.txt,并产生输出文件file2.txt。程序运行时,用户可以通过命令行传递这两个文件名:
Encrypt file1.txt file2.txt
当Encrypt程序启动时,它可以调用GetCommandTail,检索这两个文件名。调用GetCommandTail 时,EDX 必须包含一个数组的偏移量,该数组至少要有.129 个字节。调用示例如下:
.data
cmdTail BYTE 129 DUP(0) ;空缓冲区
.code
mov edx, OFFSET cmdTail
call GetCommandTail ;填充缓冲区
在Visual Studio 中运行应用程序时,有一种方法可以传递命令行参数。在 Project 菜单中,选择<projectname>Properties。在PropertyPages 窗口,展开 Configuration Properties 选项,选择Debugging。然后,在右边Command Arguments面板的编辑行中输入程序的命令参数。
完整代码测试笔记:
; 5.4.3_6.asm GetCommandTail过程 调用示例INCLUDE Irvine32.inc.data
cmdTail BYTE 129 DUP(0) ;空缓冲区.code
main PROCmov edx, OFFSET cmdTailcall GetCommandTail ;填充缓冲区INVOKE ExitProcess,0
main ENDP
END main
设置命令行参数:
运行调试,
GetMaxXY GetMaxXY过程获取控制台窗口缓冲区的大小。如果控制台窗口缓冲区大于可视窗口尺寸,则自动显示滚动条。GetMaxXY没有输人参数。当过程返回时,DX 寄存器包含了缓冲区的列数,AX寄存器包含了缓冲区的行数。每个数值的可能范围都不超过255,这也许会小于实际窗口缓冲区的大小。调用示例如下:
; 5.4.3_7.asm INCLUDE Irvine32.inc.data
rows BYTE ?
cols BYTE ?.code
main PROCcall GetMaxXYmov rows,almov cols,dlINVOKE ExitProcess,0
main ENDP
END main
运行调试:
GetMseconds GetMseconds 过程获取主机从午夜开始经过的毫秒数,并用 EAX 返回该值。在计算事件间隔时间时,这个过程是非常有用的。过程不需要输人参数。下面的例子调用了GetMseconds,并保存了返回值。执行循环之后,代码第二次调用GetMseconds,并将两次返回的时间值相减,结果就是执行循环的大致时间:
; 5.4.3_8.asm GetMseconds 过程获取主机从午夜开始经过的毫秒数,并用 EAX 返回该值。INCLUDE Irvine32.inc.data
startTime DWORD ?.code
main PROCcall GetMsecondsmov startTime, eax
L1:;(loop body)loop L1call GetMsecondssub eax, startTime ;EAX=循环时间,按毫秒计INVOKE ExitProcess,0
main ENDP
END main
运行调试:
GetTextColor GetTextColor过程获取控制台窗口当前的前景色和背景色,它没有输入参数。返回时,AL 中的高四位是背景色,低四位是前景色。调用示例如下:
; 5.4.3_9.asm GetTextColor过程获取控制台窗口当前的前景色和背景色,
;它没有输入参数。返回时,AL 中的高四位是背景色,低四位是前景色。INCLUDE Irvine32.inc.data
color BYTE ?.code
main PROCcall GetTextColormov color, ALINVOKE ExitProcess,0
main ENDP
END main
运行结果:
Gotoxy Gotoxy过程将光标定位到控制台窗口的指定位置。默认情况下,控制台窗口的X轴范围为0~79,Y轴范围为0~24。调用Gotoxy时,将Y轴(行数)传递到DH寄存器,X 轴(列数)传递到 DL 寄存器。调用示例如下:
; 5.4.3_10.asm Gotoxy过程将光标定位到控制台窗口的指定位置。INCLUDE Irvine32.inc.code
main PROCmov dh, 10 ;第 10 行mov dl, 20 ;第 20 列call Gotoxy ;定位光标INVOKE ExitProcess,0
main ENDP
END main
运行调试:
用户可能会修改控制台窗口大小,因此可以调用 GetMaxXY获取当前窗口的行列数。
lsDigit IsDigit 过程确定 AL 中的数值是否是一个有效十进制数的ASCII 码。过程被调用时,将一个 ASCII 字符传递到 AL。如果 AL 包含的是一个有效十进制数,则过程将零标志位置 1;否则,清除零标志位。调用示例如下:
; 5.4.3_11.asm IsDigit 过程确定 AL 中的数值是否是一个有效十进制数的ASCII 码。INCLUDE Irvine32.inc.data
somechar BYTE 50.code
main PROCmov AL, somecharcall IsDigitINVOKE ExitProcess,0
main ENDP
END main
运行调试:
不是10进制数的ACSII码,零标志置0
MsgBox MsgBox过程显示一个带选择项的图形界面弹出消息框。(当程序运行于控制台窗口时有效。)过程用EDX 传递一个字符串的偏移量,该字符串将显示在消息框中。还可以用EBX传递消息框标题字符串的偏移量,如果标题为空,则 EBX 为 0。调用示例如下:
; 5.4.3_12.asm MsgBox过程显示一个带选择项的图形界面弹出消息框。INCLUDE Irvine32.inc.data
caption BYTE "Dialog itle",0
HelloMsg BYTE "This is apop-up message box.",0dh, 0ahBYTE "Click OK to continue...",0.code
main PROCmov ebx, OFFSET captionmov edx, OFFSET HelloMsgcall MsgBoxINVOKE ExitProcess,0
main ENDP
END main
示例输出如下:
MsgBoxAsk MsgBoxAsk 过程显示带有 Yes 和 No 按钮的图形弹出消息框。(当程序运行于控制台窗口时有效。)过程用EDX传递问题字符串的偏移量,该问题字符串将显示在消息框中。还可以用EBX 传递消息框标题字符串的偏移量,如果标题为空,则 EBX 为0。MsgBoxAsk 用 EAX中的返回值表示用户选择的是哪个按钮,返回值有两个选择,都是预先定义的Windows常数:IDYES(值为6)或IDNO(值为7)。调用示例如下:
; 5.4.3_13.asm INCLUDE Irvine32.inc.data
caption BYTE "Survey Completed", 0
question BYTE "Thank you for completing the survey."BYTE 0dh, 0ahBYTE "Would you like to receive the results?", 0.code
main PROCmov ebx, OFFSET captionmov edx, OFFSET questioncall MsgBoxAsk;查看EAX中的返回值INVOKE ExitProcess,0
main ENDP
END main
示例输出如下:
eax寄存器中的值
OpenInputFile OpenlnputFile 过程打开一个已存在的文件进行输人。过程用EDX传递文件名的偏移量。当从过程返回时,如果文件成功打开,则EAX就包含有效的文件句柄。否则,EAX等于INVALID_HANDLE_VALUE(一个预定义的常数)。
调用示例如下:
; 5.4.3_14.asm OpenlnputFile 过程打开一个已存在的文件进行输人。INCLUDE Irvine32.inc.data
filename BYTE "myfile.txt", 0.code
main PROCmov edx, OFFSET filenamecall OpenInputFileINVOKE ExitProcess,0
main ENDP
END main
运行调试:
文件不存在:
下述伪代码显示了调用OpenlnputFile后可能的结果:
if EAX =INVALID_HANDLE_VALUEthe file was not opened successfully
elseEAX = handle for the open file
endif
ParseDecimal32 ParseDecimal32过程将一个无符号十进制整数字符串转换为 32位二进制数。非数字符号之前所有的有效数字都要转,前导空格要忽略。过程用EDX传递字符串的偏移量,用ECX传递字符串的长度,用EAX返回二进制数值。调用示例如下:
; 5.4.3_15.asm ParseDecimal32过程将一个无符号十进制整数字符串转换为 32位二进制数。INCLUDE Irvine32.inc.data
buffer BYTE "8193"
bufSize = ($ - buffer).code
main PROCmov edx, OFFSET buffermov ecx, bufSizecall ParseDecimal32 ;返回EAXINVOKE ExitProcess,0
main ENDP
END main
运行调试:
●如果整数为空,则EAX=0且CF=1
●如果整数只有空格,则EAX-0且CF=1。
●如果整数大于(-1 ),则EAX-0且CF=1。
●否则,EAX为转换后的数,且CF=0
参阅 ReadDec 过程的说明,详细了解进位标志位是如何受到影响的。
Parselnteger32 ParseInteger32过程将一个有符号十进制整数字符串转换为32位二进制数。字符串开始到第一个非数字符号之间所有的有效数字都要转,前导空格要忽略。过程用 EDX 传递字符串的偏移量,用 ECX 传递字符串的长度,用 EAX 返回二进制数值。调用示例如下:
; 5.4.3_16.asm ParseInteger32过程将一个有符号十进制整数字符串转换为32位二进制数。INCLUDE Irvine32.inc.data
buffer BYTE "-8193"
bufSize = ($ - buffer).code
main PROCmov edx, OFFSET buffermov ecx, bufSizecall ParseInteger32 ;返回EAXINVOKE ExitProcess,0
main ENDP
END main
运行调试:
字符串可能包含一个前导加号或减号,但其后只能跟十进制数字。如果数值不能表示为32位有符号整数(范围:-2 147483648到+2147483 647),则溢出标志位置1,且在控制台显示一个错误信息。
Random32 Random32 过程生成一个32 位随机整数并用 EAX 返回该数。当被反复调用时,Random32 就会生成一个模拟的随机数序列,这些数由一个简单的函数产生,该函数有一个输人称为种子(seed)。函数利用公式里的种子生成一个随机数值,并且每次都使用前次生成的随机数作为种子,来生成后续随机数。下述代码段展示了一个调用Random32的例子:
; 5.4.3_17.asm Random32 过程生成一个32 位随机整数并用 EAX 返回该数。INCLUDE Irvine32.inc.data
randVal DWORD ?.code
main PROCcall Random32mov randVal, eaxINVOKE ExitProcess,0
main ENDP
END main
运行调试:
Randomize Randomize过程对Random32和RandomRange过程的第一个种子进行初始化。种子等于一天中的时间,精度为 1/100 秒。每当调用 Random32 和 RandomRange 的程序运行时,生成的随机数序列都不相同。而 Randomize过程只需要在程序开头调用一次。下面的例子生成了10个随机整数:
; 5.4.3_18.asm Randomize过程对Random32和RandomRange过程的第一个种子进行初始化。INCLUDE Irvine32.inc.data
arrayD DWORD 10 DUP(?).code
main PROCmov esi, OFFSET arrayDcall Randomizemov ecx, 10
L1:call Random32;在此使用或显示EAX 中的随机数mov [esi], eaxadd esi, 4loop L1INVOKE ExitProcess,0
main ENDP
END main
运行调试:
RandomRange RandomRange过程在范围0~n-1内生成一个随机整数,其中n是用EAX 寄存器传递的输入参数。生成的随机数也用EAX 返回。下面的例子在 0到 4999 之间生成一个随机整数,并将其放在变量randVal 中。
; 5.4.3_19.asm INCLUDE Irvine32.inc.data
randVal DWORD ?.code
main PROCmov eax, 5000call RandomRangemov randVal, eaxINVOKE ExitProcess,0
main ENDP
END main
运行调试:
ReadChar ReadChar 过程从键盘读取一个字符,并用 AL 寄存器返回,字符不在控制台窗口中回显。调用示例如下:
; 5.4.3_20.asm ReadChar 过程从键盘读取一个字符,INCLUDE Irvine32.inc.data
charB BYTE ?.code
main PROCcall ReadCharmov charB, alINVOKE ExitProcess,0
main ENDP
END main
运行调试:
如果用户按下的是扩展键,如功能键、方向键、Ins 键或 Del 键,则过程就把AL 清零,而 AH 包含的是键盘扫描码。本书文前给出了扫描码列表。EAX 的高字节没有使用。下述伪代码描述了调用ReadChar 之后可能产生的结果;
if an extended key was pressedAL = 0AH = keyboard scan code
elseAL = ASCII key value
endif
ReadDec ReadDec过程从键盘读取一个32位无符号十进制整数,并用EAX 返回该值,前导空格要忽略。返回值为遇到第一个非数字字符之前的所有有效数字。比如,如果用户输入 123ABC,则EAX 中的返回值为123。下面是一个调用示例:
; 5.4.3_21.asm ReadDec过程从键盘读取一个32位无符号十进制整数,并用EAX 返回该值,
INCLUDE Irvine32.inc.data
intVal DWORD ?.code
main PROCcall ReadDecmov intVal, eaxINVOKE ExitProcess,0
main ENDP
END main
运行调试:
ReadDec 会影响进位标志位:
●如果整数为空,则EAX-0且CF=1。
●如果整数只有空格,则EAX-0 且 CF=1。
●如果整数大于(-1 ),则EAX-0 且 CF=1。
●否则,EAX 为转换后的数,且CF=0
ReadFromFile ReadFromFile过程读取存储缓冲区中的一个输入磁盘文件。当调用ReadFromFile时,用EAX传递打开文件的句柄,用EDX传递缓冲区的偏移量,用 ECX传递读取的最大字节数。ReadFromFile 返回时要查看进位标志位的值:如果 CF 清零,则EAX包含了从文件中读取的字节数;如果 CF 置1,则EAX 包含了数字系统错误代码。调用WriteWindowsMsg 过程就可以获得该错误的文本。在下面的例子中,从文件读取的 5000个字节复制到了缓冲区变量:
.data
BUFFER_SIZE = 5000
buffer BYTE BUFFER_SIZE DUP(?)
bytesRead DWORD ?
.code
mov edx, OFFSET buffer ;指向缓冲区
mov ecx, BUFFER_SIZE ;读取的最大字节数
call ReadFromFile ;读文件
如果此时进位标志位清零,则可以执行如下指令:
mov bytesRead,eax ;实际读取的字节数
但是,如果此时进位标志位置1,就可以调用 WriteWindowsMsg 过程,显示错误代码以及该应用最近产生错误的说明:
call WriteWindowsMng
完整的测试代码笔记:
; 5.4.3_22.asm ReadFromFile过程读取存储缓冲区中的一个输入磁盘文件。
;当调用ReadFromFile时,用EAX传递打开文件的句柄,
;用EDX传递缓冲区的偏移量,用 ECX传递读取的最大字节数。INCLUDE Irvine32.inc.data
filename BYTE "file1.txt",0
BUFFER_SIZE = 5000
buffer BYTE BUFFER_SIZE DUP(?)
bytesRead DWORD ?.code
main PROC;打开文件mov edx, OFFSET filenamecall OpenInputFile;读取文件mov edx, OFFSET buffer ;指向缓冲区mov ecx, BUFFER_SIZE ;读取的最大字节数call ReadFromFile ;读文件mov bytesRead, eax ;实际读取的字节数call WriteWindowsMsgINVOKE ExitProcess,0
main ENDP
END main
运行调试:
打开文件:
读取文件内容
实际读取字节数:
显示调用信息错误:
ReadHex ReadHex过程从键盘读取一个32位十六进制整数,并用EAX 返回相应的二进制数。对无效字符不进行任何错误检查。字母A到F的大小写都可以使用。最多能够输入8个数字(超出的字符将被忽略),前导空格将被忽略。调用示例如下:
; 5.4.3_23.asm ReadHex过程从键盘读取一个32位十六进制整数,并用EAX 返回相应的二进制数。INCLUDE Irvine32.inc.data
hexVal DWORD ?.code
main PROCcall ReadHexmov hexVal, eaxINVOKE ExitProcess,0
main ENDP
END main
运行调试:
ReadInt ReadInt 过程从键盘读取一个32位有符号整数,并用 EAX 返回该值。用户可以键入前置加号或减号,而其后跟的只能是数字。ReadInt 设置溢出标志位,如果输人数值无法表示为32位有符号数(范围:-2147483648至+2147483647),则显示一个错误信息。返回值包括所有的有效数字,直到遇见第一个非数字字符。例如,如果用户输入+123ABC,则返回值为+123。调用示例如下:
; 5.4.3_24.asm ReadInt 过程从键盘读取一个32位有符号整数,并用 EAX 返回该值。INCLUDE Irvine32.inc.data
intVal SDWORD ?.code
main PROCcall ReadIntmov intVal, eaxINVOKE ExitProcess,0
main ENDP
END main
运行调试:
ReadKey ReadKey过程执行无等待键盘检查。换句话说,它检查键盘输入缓冲区以查看用户是否有按键操作。如果没有发现键盘数据,则零标志位置1。如果ReadKey发现有按键,则清除零标志位,且向 AL 送人 0或 ASCII 码。若 AL 为 0,表示用户可能按下了一个特殊键(功能键、方向键等)。AH 寄存器为虚拟扫描码,DX 为虚拟键码,EBX 为键盘标志位。下述伪代码说明了调用ReadKey时的各种结果:
if no_keyboard_data thenZF = 1
elseZF = 0if AL = 0 thenextended key was pressed, and AH = scan code, DX = virtualkey code, and EBX = keyboard flag bitselseAL = the key's ASCII codeendi f
endif
当调用ReadKey时,EAX和EDX 的高16 位会被覆盖。
ReadString ReadString过程从键盘读取一个字符串,直到用户键入回车键。过程用EDX 传递缓冲区的偏移量,用ECX传递用户能键入的最大字符数加1(保留给终止空字节),用EAX返回用户键人的字符数。示例调用如下:
; 5.4.3_25.asm ReadString过程从键盘读取一个字符串,直到用户键入回车键。INCLUDE Irvine32.inc.data
buffer BYTE 21 DUP(0) ;输入缓冲区
byteCount DWORD ? ;定义计数器.code
main PROCmov edx, OFFSET buffer ;指向缓冲区mov ecx, SIZEOF buffer ;定义最大字符数call ReadString ;输入字符串mov byteCount, eax ;字符数INVOKE ExitProcess,0
main ENDP
END main
运行调试:
ReadString在内存中字符串的末尾自动插入一个null 终止符。用户输入“ABCDEFG”后,buffer 中前8个字节的十六进制形式和ASCII 形式如下所示:
变量byteCount等于7。
SetTextColor SetTextColor 过程(仅在Irvine32链接库中)设置输出文本的前景色和背景色。调用 SetTextColor 时,给EAX分配一个颜色属性。下列预定义的颜色常数都可以用于前景色和背景色;
颜色常量在 Irvine32.inc 文件中进行定义。要获得完整的颜色字节数值,就将背景色乘以 16 再加上前景色。例如,下述常量表示在蓝色背景上输出黄色字符:
yellow +(blue·16)
下列语句设置为蓝色背景上输出白色字符:
mov eax,white-(blue-16) ;蓝底白字
call SetTextColor
另一种表示颜色常量的方法是使用 SHL 运算符,将背景色左移4 位再加上前景色。
yellow +(blue SHL 4)
位移是在汇编时执行的,因此它只能用常数作操作数。第 7章将会学习如何在执行时进行整数移位。16.3.2 节的视频属性还有详细说明。
; 5.4.3_26.asm Str_length 过程返回空字节结束的字符串的长度。
;过程用EDX 传递字符串的偏移量,用 EAX 返回字符串的长度。INCLUDE Irvine32.inc.data
buffer BYTE "abcdefghijklmnopqrstuvwxyz", 0
bufLength DWORD ?.code
main PROCmov edx, OFFSET buffer ;指向字符串call Str_length ;EAX = 5mov bufLength, eax ;保存长度 INVOKE ExitProcess, 0
main ENDP
END main
运行调试:
这个结果有问题,无论buffer是多长字符串,eax的结果都是20个长度(014h)???
WaitMsg WaitMsg 过程显示“Press any key to continue…”消息,并等待用户按键。当用户想在数据滚动和消失之前暂停屏幕显示时,这个过程就很有用。过程没有输入参数。调用示例如下:
; 5.4.3_27.asm WaitMsg 过程显示“Press any key to continue…”消息,并等待用户按键。
;当用户想在数据滚动和消失之前暂停屏幕显示时,这个过程就很有用。INCLUDE Irvine32.inc.code
main PROCcall WaitMsgINVOKE ExitProcess,0
main ENDP
END main
运行结果:
WriteBin WriteBin过程以ASCII二进制格式向控制台窗口输出一个整数。过程用EAX 传递该整数。为了便于阅读,二进制位以四位一组的形式进行显示。调用示例如下:
; 5.4.3_28.asm WriteBin过程以ASCII二进制格式向控制台窗口输出一个整数。
;过程用EAX 传递该整数。为了便于阅读,二进制位以四位一组的形式进行显示。INCLUDE Irvine32.inc.code
main PROCmov eax, 12346AF9hcall WriteBinINVOKE ExitProcess,0
main ENDP
END main
示例代码显示如下:
0001 0010 0011 0100 0110 1010 1111 1001
WriteBinB WriteBinB过程以ASCHI二进制格式向控制台窗口输出一个32 位整数。过程用 EAX 寄存器传递该整数,用 EDX 表示以字节为单位的显示大小(1、2,或4)。为了便于阅读,二进制位以四位一组的形式进行显示。调用示例如下:
; 5.4.3_29.asm WriteBinB过程以ASCHI二进制格式向控制台窗口输出一个32 位整数。
;过程用 EAX 寄存器传递该整数,用 EDX 表示以字节为单位的显示大小(1、2,或4)。
;为了便于阅读,二进制位以四位一组的形式进行显示。INCLUDE Irvine32.inc.code
main PROCmov eax, 00001234hmov ebx, TYPE WORD ;两个字节call WriteBinB ;显示0001 0010 0011 0100INVOKE ExitProcess,0
main ENDP
END main
运行调试:
WriteChar WriteChar过程向控制台窗口写一个字符。过程用AL传递字符(或其ASCII 码)。调用示例如下:
; 5.4.3_30.asm WriteChar过程向控制台窗口写一个字符。过程用AL传递字符(或其ASCII 码)。INCLUDE Irvine32.inc.code
main PROCmov al, 'A'call WriteChar ;显示:"AINVOKE ExitProcess,0
main ENDP
END main
运行调试:
WriteDec WriteDec 过程以十进制格式向控制台窗口输出一个32位无符号整数,且没有前置 0。过程用 EAX 寄存器传递该整数。调用示例如下:
; 5.4.3_31.asm WriteDec 过程以十进制格式向控制台窗口输出一个32位无符号整数,
;且没有前置 0。过程用 EAX 寄存器传递该整数。INCLUDE Irvine32.inc.code
main PROCmov eax, 295call WriteDec ;显示:"295”INVOKE ExitProcess,0
main ENDP
END main
运行调试:
WriteHex WriteHex过程以8位十六进制格式向控制台窗口输出一个32位无符号整数,如果需要,应插人前置 0。过程用 EAX 传递整数。调用示例如下;
; 5.4.3_32.asm WriteHex过程以8位十六进制格式向控制台窗口输出一个32位无符号整数,
;如果需要,应插人前置 0。过程用 EAX 传递整数。INCLUDE Irvine32.inc.code
main PROCmov eax, 7FFFhcall WriteHex ;显示:"00007FFF"INVOKE ExitProcess,0
main ENDP
END main
运行调试:
WriteHexB WriteHexB过程以十六进制格式向控制台窗口输出一个32位无符号整数,如果需要,应插入前置 0。过程用 EAX 传递整数,用 EBX表示显示格式的字节数(1、2,或 4)。调用示例如下:
; 5.4.3_33.asm WriteHexB过程以十六进制格式向控制台窗口输出一个32位无符号整数,如果需要,应插入前置 0。
;过程用 EAX 传递整数,用 EBX表示显示格式的字节数(1、2,或 4)。INCLUDE Irvine32.inc.code
main PROCmov eax, 7FFFhmov ebx, TYPE WORD ;两个字节call WriteHexB ;显示:"7FFF"INVOKE ExitProcess,0
main ENDP
END main
运行调试:
WriteInt WriteInt过程以十进制向控制台窗口输出一个32位有符号整数,有前置符号,但没有前置 0。过程用 EAX 传递整数。调用示例如下:
; 5.4.3_34.asm WriteInt过程以十进制向控制台窗口输出一个32位有符号整数,
;有前置符号,但没有前置 0。过程用 EAX 传递整数。INCLUDE Irvine32.inc.code
main PROCmov eax, 216543call WriteInt ;显示:“+216543"INVOKE ExitProcess,0
main ENDP
END main
运行调试:
WriteString WriteString过程向操作台窗口输出一个空字节结束的字符串。过程用EDX 传递字符串的偏移量。调用示例如下:
; 5.4.3_35.asm WriteString过程向操作台窗口输出一个空字节结束的字符串。过程用EDX 传递字符串的偏移量。INCLUDE Irvine32.inc.data
prompt BYTE "Enter your name: ",0.code
main PROCmov edx, OFFSET promptcall WriteStringINVOKE ExitProcess,0
main ENDP
END main
运行结果:
WriteToFile WriteToFile过程向一个输出文件写入缓冲区内容。过程用 EAX 传递有效的文件句柄,用 EDX 传递缓冲区偏移量,用ECX 传递写人的字节数。当过程返回时,如果EAX 大于 0,则其包含的是写入的字节数;否则,发生错误。下述代码调用了WriteToFile:
; 5.4.3_36.asm WriteToFile过程向一个输出文件写入缓冲区内容。INCLUDE Irvine32.incBUFFER_SIZE = 5000
.data
fileHandle DWORD ?
buffer BYTE BUFFER_SIZE DUP(?).code
main PROCmov eax, fileHandlemov edx, OFFSET buffermov ecx, BUFFER_SIZEcall WriteToFileINVOKE ExitProcess,0
main ENDP
END main
运行调试:
下面的伪代码说明了调用WriteToFile之后对EAX返回值的处理:
if EAX =0 thenerror occurred when writing to filecall WriteWindowsMessage to see the error
elseEAX =number of bytes written to the file
endif
WriteWindowsMsg WriteWindowsMsg过程向控制台窗口输出应用程序在调用系统函数时最近产生的错误信息。调用示例如下:
; 5.4.3_37.asm WriteWindowsMsg过程向控制台窗口输出应用程序在调用系统函数时最近产生的错误信息。INCLUDE Irvine32.inc.code
main PROCcall WriteWindowsMsgINVOKE ExitProcess,0
main ENDP
END main
运行调试:
下面的例子展示了一个消息字符串:
Error 2: The system cannot find the file specified.
5.4.4库测试程序
教程:库测试#1
本实践教程编写一个程序,演示用屏幕颜色输入/输出整数。
步骤 1:用标准头部开始程序:
;库测试#1: 整数I/O(InputLoop.asm)
;测试Clrscr,Crlf,DumpMem,ReadInt,SetTextColor,
;WaitMsg,WriteBin,WriteHex和WriteString 过程。
步骤 2:声明 COUNT 常量,以便之后确定程序循环重复的次数。然后再定义两个常量BlueTextOnGray 和 DefaultColor,当需要修改控制台窗口颜色时可以使用它们。背景色存放在颜色字节的高4位,前景色(文本)存放在颜色字节的低4位。虽然还没有讨论位移指令,但是可以通过将背景色移动到颜色属性字节的高 4位来实现背景色乘16:
。data
COUNT = 4
BlueTextOnGray = blue + (lightGray*16)
DefaultColor = lightGray + (black*16)
步骤3:用十六进制常数声明一个有符号双字整数数组。此外,还要定义一个字符串,在程序需要用户输人整数时作为提示:
arrayD SDWORD 12345678h, 1A4B2000h, 3434h, 7AB9h
prompt BYTE "Enter a 32-bit signed integer:", 0
步骤 4:在代码段定义主程序,编写代码将EAX初始化为浅灰色背景和蓝色文本。在程序执行时,SetTextColor过程将从其被调用开始改变窗口中所有输出文本的前景色和背景色:
.code
main PROC
mov eax,BlueTextOnGray
call SetTextColor
如果要把控制台窗口背景色设置为新的颜色,就必须使用Clrscr过程来清屏:
call Clrscr ;清屏
接下来,程序将显示由变量 arrayD 定义的内存中的一组双字数值。DumpMem 过程需要用ESI、EBX和ECX寄存器传递参数。
步骤5:将arrayD的偏移量赋给ESI,用于标识显示数据区的起始位置:
mov esi,oFFsET arrayD
步骤6:EBX的值是一个整数,指定每个数组元素的大小。由于要显示的是双字数组因此EBX等于4。该值由表达式TYPEarrayD返回:
mov ebx, TYPE arrayD ;双字=4字节
步骤7:利用LENGTHOF运算符,ECX设置为被显示单元的个数。然后,当调用DumpMem 时,过程需要的所有信息就都已经准备好了:
mov ecx, LENGTHOF arrayD ;arrayD中的单元数
call DumpMem ;显示内存区
下图展示了DumpMem产生的输出:
接下来,将请求用户输入4个有符号整数。每输入一个整数,该数将以有符号十进制、十六进制和二进制的形式显示出来。
步骤8:调用Crf过程输出一个空行。接着,将ECX初始化为常数COUNT,使之成为后续执行的循环计数器:
call Crlf
mov ecx, COUNT
步骤 9:程序需要显示一个字符申来请求用户输入一个整数。将字符串偏移量赋给 EDX并调用 WriteString 过程。然后,调用 ReadInt 过程接收用户输入,该数将自动保存到 EAX;
L1: mov edx, OFFSET promptcallWriteString callReadInt ;输入数据存入 EAXcall Crlf ;显示一个新空自行
步骤 10: 调用 WriteInt 过程,将EAX中的整数显示为有符号十进制形式。再调用 Crlf将光标移动到下一个输出行;
call WriteInt ;显示为有符号十进制
call Crlf
步骤 11 :分别调用 WriteHex 和 WriteBin 过程,将同样的数(仍保存在EAX中)显示为十六进制和二进制形式:
call WriteHex ;显示为十六进制
call Crlf
call WriteBin ;显示为二进制
call Crlf
call Crlf
步骤12:插入一条Loop指令,使程序从标号L1处开始循环。该指令先将ECX减1,当且仅当ECX不等于0时,跳转到标号L1:
LOOP L1 ;重复循环
步骤13:循环结束后,想显示一条“Press anykey…”消息,然后暂停输出,等待用户按键。要实现这个功能,调用 WaitMsg 过程:
call WaitMsg ;"Press any key..."
步骤14:在程序结束之前,控制台窗口属性返回为默认颜色(黑色背景浅灰字符)。
mov eax, DefaultColor
call SetTextColor
call cirser
下述代码结束程序:
exit
main ENDP
END main
按照用户输入的4个整数,程序余下的输出如下图所示:
完整的程序清单如下,其中添加了一些注释行:
;库测试 #1: 整数 I/O (InputLoop.asm); 测试 Clrscr, Crlf, DumpMem, ReadInt, SetTextColor,
; WaitMsg, WriteBin, WriteHex, 和 WriteString 过程.INCLUDE Irvine32.inc.data
COUNT = 4
BlueTextOnGray = blue + (lightGray * 16)
DefaultColor = lightGray + (black * 16)
arrayD SDWORD 12345678h,1A4B2000h,3434h,7AB9h
prompt BYTE "Enter a 32-bit signed integer: ",0.code
main PROC;选择浅灰背景蓝色文本mov eax,BlueTextOnGraycall SetTextColorcall Clrscr ; 清屏;用DumpMem 显示数组.mov esi,OFFSET arrayD ; 开始位置的OFESETmov ebx,TYPE arrayD ; 双字=4字节mov ecx,LENGTHOF arrayD ; arrayD中的单元数call DumpMem ; 显示内存在;请求用户输入一组有符号整数 call Crlf ; 显示一个新空白行mov ecx,COUNT
L1: mov edx,OFFSET promptcall WriteStringcall ReadInt ; 输入数据存入EAXcall Crlf ; 显示一个新空白行;用十进制,十六进制和二进制显示整数call WriteInt ; 显示为有符号十进制call Crlfcall WriteHex ; 显示为十六进制call Crlfcall WriteBin ; 显示为二进制call Crlfcall CrlfLoop L1 ; 重复循环; Return console window to default colors.call WaitMsg ; "Press any key..."mov eax,DefaultColorcall SetTextColorcall Clrscrexit
main ENDP
END main
运行调试:
库测试 #2:随机整数
现在来看第二个库测试程序,演示链接库的随机数生成功能,并引人 Call 指令(详细说明参见5.5节)。第一步,程序在0到4 294 967294 之间,随机生成 10个无符号整数。第二步,程序在-50到+49之间生成10个有符号整数:
; 链接库测试#2 TestLib2.asm
; 测试Irvine32链接库的过程,INCLUDE Irvine32.incTAB = 9 ;Tab的ASCII码
.code
main PROCcall Randomize ;初始化随机生成器call Rand1call Rand2exit
main ENDPRand1 PROC;生成10个伪随机整数mov ecx, 10 ;循环 10次
L1: call Random32 ;生成随机整数call WriteDec ;用无符号十进制形式输出mov al, TAB ;水平制表符call WriteChar ;输出制表符loop L1call Crlfret
Rand1 ENDPRand2 PROC;在-50到+49之间生成10个伪随机整数mov ecx, 10 ;循环 10次
L1: mov eax, 100 ;数值范围0~99call RandomRange ;生成随机整数sub eax, 50 ;数值范围-50到+49call WriteInt ;用有符号十进制形式输出mov al, TAB ;水平制表符call WriteChar ;输出制表符loop L1call Crlfret
Rand2 ENDP
END main
程序示例输出如下所示:
库测试 #3:性能计时
汇编语言常常用于优化那些被视为对程序性能非常关键的代码。本书链接库中的 GetMseconds过程能返回从午夜之后经过的毫秒数。在第3个库测试程序中,调用GetMseconds,执行一个嵌套循环,然后再一次调用 GetMSeconds。两次过程调用返回不同的值,给出了嵌套循环花费的时间:
; 链接库测试#3 (TestLib3.asm)
; 计算嵌套循环的执行时间INCLUDE Irvine32.inc.data
OUTER_LOOP_COUNT = 3
startTime DWORD ?
msg1 byte "Please wait...", 0dh, 0ah, 0
msg2 byte "Elapsed milliseconds: ", 0.code
main PROCmov edx, OFFSET msg1 ;"Please wait..."call WriteString;保存开始时间call GetMSecondsmov startTime, eax;开始外层循环mov ecx, OUTER_LOOP_COUNT
L1: call innerLooploop L1;计算执行时间call GetMSecondssub eax, startTime;显示执行时间mov edx,OFFSET msg2 ;"Elapsed milliseconds: "call WriteStringcall WriteDec ;输出毫秒数call Crlfexit
main ENDP
innerLoop PROCpush ecx ;保存当前ECX的值mov ecx, 0FFFFFFFFh ;设置循环计数器
L1: mul eax ;使用了一些周期mul eaxmul eaxloop L1 ;重复内循环pop ecx ;恢复ECX被保存的值ret
innerLoop ENDP
END main
在Intel Core Duo处理器上运行该程序的示例输出如下:
程序的详细分析
现在来仔细研究一下库测试#3 程序。main 过程在控制台窗口中显示字符串“ Please wait…”:
main PROCmov edx, OFFSET msg1 ; "Please wait…"call WriteString
调用 GetMSeconds 时,过程用 EAX 寄存器返回从午夜开始经过的毫秒数。为了后续的使用,该数值被保存到一个变量里:
call GetMSeconds
mov startTime, eax
接下来,以OUTER_LOOP_COUNT 的值为基础创建一个循环。该值被送入 ECX,用于之后出现的LOOP 指令:
mov ecx, OUTE_ LOOP_COUNT
循环在标号L1 处开始,调用innerLoop 过程。这条CALL 指令将一直重复,直到 ECX递减到 0 为止:
L1: call innerLooploop L1
innerLoop 过程用指令 PUSH 将 ECX 保存到堆栈,再对其赋新值。(PUSH 和 POP 指令已经在5.1.2 节讨论过了。)然后,循环本身有一些指令,设计用来使用一些时钟周期:
innerLoop PROCpush ecx; ;保存当前 ECX 的值mov ecx, 0FFFFFFFh ;设置循环计数器
L1: mul eax ;使用一些周期mul eaxmul eaxloop L1 ;重复内循环
这条LOOP 指令会把ECX 递减到 0,因此我们将被保留的ECX 值弹出堆栈。所以在结束过程时,ECX 中的值与进入过程时相同。PUSH 和 POP 指令序列很重要,因为 main 过程在调用 innerLoop 时是用ECX 作为循环计数器的。innerLoop的最后几行如下所示:
pop ecx ;恢复ECX被保存的值ret
innerLOOP ENDP
循环结束后回到 main 过程,调用 GetMSeconds,用 EAX 返回其结果。现在程序要做的就是用该值减去开始时间,从而获得两次调用GetMSeconds之间经过的毫秒数:
call GetMseconds
sub eax,startTime
程序显示一个新的字符串信息,然后输出EAX中的数值,以显示经过的毫秒数:
mov edx, OFFSET msq2 ;"Elapsed milliseconds:"call Writestringcall WriteDeccall Crlfexit
main ENDP
5.4.5 本节回顾
1.链接库中的哪个过程在指定范围内生成随机整数?
答:RandomRange过程
2.链接库中的哪个过程显示“Press[Enter]to continue…”并等待用户按下Enter键?
答:WaitMsg过程
3.编写语句使程序暂停 700毫秒。
答:mov eax, 700
call Delay
4.链接库中的哪个过程以十进制形式向控制台窗口输出无符号整数?
答:WriteDec过程
5.链接库中的哪个过程将光标定位到控制台窗口的指定位置?
答:GotoXY过程
6.编写使用 Irvine32 链接库时所需的INCLUDE 伪指令。
答:INCLUDE Irvine32.inc
7.Irvine32.inc文件中包含哪些类型的语句?
答:PROTO语句(过程原型)和常量定义。(还有读文本宏,但是其内容不包含在本章中)
8.DumpMem过程需要哪些输入参数?
答:ESI为数据被始地址,ECX为数据单元个数,EBX为数据单元承压小(字节,字或双字)
9.ReadString过程需要哪些输入参数?
答:EDX为字节数组的偏移量,ECX为读取字符的最大个数。
10.DumpRegs过程显示哪些处理器状态标志位?
答:进位标志位、符号标志位、零标志位、溢出标志位、辅助进位标志位、奇偶标志位。
11.挑战:编写语句提示用户输入标识号,并向字节数组输入一串数字。
; 5.4.5_11.asm INCLUDE Irvine32.inc.data
str1 BYTE "Enter identification number: ", 0
idStr BYTE 15 DUP(?).code
main PROCmov edx, OFFSET str1call WriteStringmov edx, OFFSET idStrmov ecx, (SIZEOF idStr) - 1call ReadStringINVOKE ExitProcess,0
main ENDP
END main
运行调试:
5.5 64位汇编编程
现在编写一段小程序,使用 Microsoft x64 调用规范来调用子程序AddFour。这个子程序将四个参数寄存器(RCX、RDX、R8和R9)的内容相加,并将和数保存到 RAX。由于过程通常使用 RAX 返回结果,因此,当从子程序返回时,调用程序也期望返回值在这个寄存器中。这样就可以说这个子程序是一个函数,因为,它接收了四个输入并(确切地说)产生了一个输出。
; 在64模式下调用子程序(Callproc 64.asm)
;第5章示例ExitProcess PROTO
WriteInt64 PROTO ;Irvine64链接库
Crlf PROTO ;Irvine64链接库.code
main PROCsub rsp, 8 ;对准堆栈指针sub rsp, 20h ;为影子参数保留32个字节mov rcx, 1 ;依序传递参数mov rdx, 2 mov r8, 3mov r9, 4call AddFour ;在RAX中查找返回值call WriteInt64 ;显示数字call Crlf ;输出回车换行符mov ecx, 0call ExitProcess
main ENDPAddFour PROCmov rax, rcxadd rax, rdxadd rax, r8add rax, r9 ;ret
AddFour ENDPEND
调试运行:
现在来看看本例中的其他细节:第10 行将堆栈指针对齐到16字节的偶数边界。为什么要这样做?在 OS 调用主程序之前,假设堆栈指针是对齐 16 字节边界的。然后,当 OS 调用主程序时,CALL 指令将8 字节的返回地址压入堆栈。将堆栈指针再减去 8,使其减少成一个16 的倍数。
可以在VisualStudio调试器中运行该程序,并查看RSP寄存器(堆栈指针)改变数值通过这个方法,能够看到用图形方式在图5-11中展示的十六进制数值。该图只展示了每个地址的低32位,因为高32位为全零:
1)执行第10行前,RSP=01AFE48。这表示在OS调用本程序之前,RSP等于01AFE50。(CALL指令使得堆栈指针减8。)
2)执行第10行后,RSP=01AFE40,表示堆栈正好对齐到16字节边界。
3)执行第11行后,RSP=01AFE20,表示32个字节的影子空间位置从01AFE20到01AFE3F。
4)在AddFour过程中,RSP=0IAFE18,表示调用者的返回地址已经压人堆栈。
5)从 AddFour返回后,RSP再一次等于01AFE20,与调用AddFour之前的值相同。
与调用 ExitProcess 来结束程序相比,本程序选择的是执行 RET 指令,这将返回到启动本程序的过程。但是,这也就要求能将堆栈指针恢复到其在 main 过程开始执行时的位置。下面的代码行能替代CallProc_64 程序的第 21 和 22 行:
21: add rsp, 28 ;恢复堆栈指针
22 : mov ecx,0 ;过程返回码
23: ret ;返回 OS
提示 要使用Irvine64链接库,将Irvine64.obj文件添加到用户的Visual Studio 项目中。VisualStudio中的操作步骤如下:在Solution Explorer 窗口中右键点击项目名称选择 Add,选择ExistingItem,再选择Irvine64.obj 文件名。
5.6 本章小结
这一章介绍了本书的链接库,使读者在汇编语言应用程序中更便于进行输入输出的处理。
表5-1列出了lrvine32链接库中的绝大多数过程。本书网站(www.asmirvine.com)上可以获取所有过程最新更新的列表。
5.4.4 节中的库测试程序演示了若干Irvine32库的输入输出函数。它生成并显示了一组随机数、寄存器的内容和内存区域的内容。它还显示了各种格式的整数并演示了字符串的输人输出。
运行时堆栈是一种特殊的数组,用于暂时保存地址和数据。ESP 寄存器保存了一个 32位偏移量,指向栈中某个位置。由于堆栈中的最后一个数是第一个出栈的,因此,堆栈被称为LIFO(后进先出)结构。入栈操作将一个数复制到堆栈。出栈操作将一个数从堆栈中取出并将其复制到寄存器或变量。堆栈通常存放过程返回地址、过程参数、局部变量和过程内使用的寄存器。
PUSH指令首先减少堆栈指针,然后把源操作数复制到堆栈中。POP 指令首先把 ESP指向的堆栈内容复制到目标操作数中,然后增加 ESP 的值。
PUSHAD 指令把 32 位通用寄存器都压入堆栈,PUSHA 指令把 16位通用寄存器都压人堆栈。POPAD指令把堆栈中的数据弹出到32位通用寄存器中,POPA指令把堆栈中的数据弹出到16 位通用寄存器中。PUSHA 和 POPA 只能用于16 位编程。
PUSHFD 指令将 32 位EFLAGS 寄存器压人堆栈,POPFD 将堆栈数据弹出到 EFLAGS寄存器。PUSHF 和POPF 对16 位FLAGS 寄存器进行同样的操作。
RevStr 程序(5.1.2节)用堆栈颠倒字符串顺序。
过程是用PROC和ENDP伪指令声明的、已命名的代码段,用RET指令结束其执行。5.2.1 节中给出的 SumOf过程,计算了三个整数之和。CALL 指令通过将过程地址插人指令指针寄存器来执行这个过程。当过程执行结束时,RET(从过程返回)指令又将处理器带回到程序中过程被调用的位置。过程嵌套调用是指,一个被调用过程在其返回前又调用了另一个过程。
带单个冒号的代码标号只在包含它的过程中可见。带::的代码标号则是全局标号,其所在源程序文件中的任何一条语句都可以访问它。
5.2.5 节给出的ArraySum过程计算并返回了数组元素之和。
与 PROC 伪指令一起使用的USES运算符,列出了过程修改的全部寄存器。汇编器产生代码,在程序开始时将寄存器的内容压人堆栈,并在过程返回前弹出恢复寄存器。
5.7 关键术语
5.7.1 术语
arguments(参数) | nested procedure call(嵌套过程调用) |
console window(控制台窗口) | precondition(前提) |
fle handle(文件句柄) | pop operation(出栈操作) |
globallabel(全局标号) | push operation(入栈操作) |
input parameter(输入参数) | runtime stack(运行时堆栈) |
label(标号) | stack abstract data type(堆栈抽象数据类型) |
last-in,first-out(LIFO)(后进先出) | stack data structure(堆栈数据结构) |
link library(链接库) | stack pointer register(堆栈指针寄存器) |