《汇编语言:基于X86处理器》第8章 高级过程(2)
8.4 INVOKE、ADDR、PROC和PROTO
在32位模式中,INVOKE、PROC和PROTO伪指令是过程定义和调用的强大工具。ADDR运算符与这些伪指令一起使用,是定义过程参数的重要工具。在很多方面,这些伪指令都接近于高级编程语言提供的便利。但是,从教学的角度来看,它们的使用是有争议的.因为它们屏蔽了运行时堆栈的底层结构。因此,在使用这些伪指令之前,详细了解子程序调用的底层机制是非常明智的。
在某种情况下,使用高级过程伪指令会得到更好的编程效果--即程序要在多个模块之间调用过程的时候。此时,PROTO伪指令对照过程声明检查参数列表,以帮助汇编器验证过程调用。这个特性鼓励经验丰富的汇编语言程序员去利用高级MASM伪指令提供的便利。
8.4.1 INVOKE伪指令
INVOKE 伪指令,只用于 32位模式,将参数入栈(按照MODEL 伪指令的语言说明符所指定的顺序)并调用过程。INVOKE 是 CALL 指令一个方便的替代品,因为,它用一行代码就能传递多个参数。常见语法如下:
INVOKE procedureName [, argumentList]
ArgumentList是可选项,它用逗号分隔传递给过程的参数。例如,执行若干 PUSH指令后调用DumpArray过程,使用CALL指令的形式如下:
push TYPE array
push LENGTHOF array
push OFFSET array
call DumpArray
使用等效的INVOKE则将代码减少为一行,列表中的参数逆序排列(假设遵循STDCALL 规范);
INVOKE DumpArray, OFFSET array, LENGTHOF array, TYPE array
INVOKE 对参数数量几乎没有限制,每个参数也可以独立成行。下面的 INVOKE 语句包含了有用的注释:
INVOKE DumpArray ;显示数组OFFSET array :指向数组LENGTHOF array ;数组长度TYPE array ;数组元素的大小类型
参数类型如表 8-2所示。
表8-2 INVOKE的参数类型 | |||
类型 | 例子 | 类型 | 例子 |
立即数 | 10, 3000h, OFFSET mylist, TYPE array | 寄存器 | eax,bl,edi |
整数表达式 | (10*20), COUNT | ADDR name | ADDR myList |
变量 | myList, array, myWord, myDWord | OFFSET name | OFFSET myList |
地址表达式 | [myList+2], [ebx+esi] |
覆盖EAX和EDX 如果向过程传递的参数小于32位,那么在将参数人栈之前,INVOKE为了扩展参数常常会使得汇编器覆盖EAX和EDX的内容。有两种方法可以避免这种情况:其一,传递给 INVOKE 的参数总是32 位的;其二,在过程调用之前保存 EAX 和 EDX,在过程调用之后再恢复它们的值。
完整代码测试笔记
;8.4.1.asm 8.4.1 INVOKE伪指令
;INVOKE 伪指令,只用于 32位模式,将参数入栈(按照MODEL 伪指令的语言说明符所指定的顺序)并调用过程。
;INVOKE 是 CALL 指令一个方便的替代品,因为,它用一行代码就能传递多个参数。常见语法如下:
;INVOKE procedureName [, argumentList]
;ArgumentList是可选项,它用逗号分隔传递给过程的参数。INCLUDE Irvine32.inc;.386
;.model flat,stdcall
;.stack 4096
;ExitProcess PROTO, dwExitCode:DWORD;DumpArray PROTO, dwOffset:DWORD, dwLengthof:DWORD, dwType:DWORD.data
array DWORD 1010h, 2020h, 3030h, 4040h.code
;PROC后面一定要声明参数列表,否则调用出错
DumpArray PROC, dwOffset:DWORD, dwLength:DWORD, dwType:DWORDmov ebx, dwType mov ecx, dwLengthmov esi, dwOffsetcall DumpMemret
DumpArray ENDPmain PROCmov esi, OFFSET array ;首地址偏移量mov ecx, LENGTHOF array ;单元个数mov ebx, TYPE array ;双字格式call DumpMemcall CrlfINVOKE DumpArray, OFFSET array, LENGTHOF array, TYPE arraycall CrlfINVOKE ExitProcess,0
main ENDPEND main
运行调试:
运行结果:
8.4.2 ADDR运算符
ADDR 运算符同样可用于32 位模式,在使用INVOKE 调用过程时,它可以传递指针参数。比如,下面的INVOKE语句给FillArray过程传递了myArray的地址:
INVOKE FillArray, ADDR myArray
传递给 ADDR 的参数必须是汇编时常数。下面的例子就是错误的:
INVOKE mySub, ADDR[ebp+12] ;错 误
ADDR运算符只能与INVOKE一起使用,下面的例子也是错误的:
mov esi, ADDR myArray ;错误
示例 下例中的INVOKE伪指令调用Swap,并向其传递了一个双字数组前两个元素的地址:
.data
Array DWORD 20 DUP(?)
.code
...
INVOKE Swap,ADDR Array,ADDR [Array+4]
假设使用 STDCALL 规范,那么汇编器生成的相应代码如下所示:
push OFFSET Array+4
push OFFSET Array
call Swap
ADDR 运行符 用于获取变量或标号的地址。
完整代码测试笔记
;8.4.2.asm 8.4.2 ADDR运算符
;ADDR 运算符同样可用于32 位模式,在使用INVOKE 调用过程时,它可以传递指针参数。INCLUDE Irvine32.inc.data
count = 10
array WORD count DUP(?)
arrayD DWORD 11112222h, 33334444h.code
;过程定义要放在调用的前面
ArrayFill PROC, dwOoffset:DWORDpushad ;保存寄存器mov esi, dwOoffset ;数组偏移量mov ecx, LENGTHOF array ;数据长度cmp ecx, 0 ;ECX==0?je L2 ;是:跳过循环
L1:mov eax, 10000h ;随机范围0~FFFFhcall RandomRange ;从链接库生成随机数mov [esi], ax ;在数组中插入值add esi, TYPE WORD ;指向下一个元素loop L1
L2: popad ;恢复寄存器ret
ArrayFill ENDP;数据交换过程
Swap PROC, wVal1:DWORD, wVal2:DWORDmov esi, wVal1 ;取偏移地址mov ebx, [esi] ;获取地址里的值mov edi, wVal2 ;取第2个数的偏移地址xchg ebx, [edi] ;交换数据mov [esi], ebxret
Swap ENDpmain PROC;调用测试mov esi, OFFSET arrayINVOKE ArrayFill, ADDR array ;添加参数INVOKE Swap, ADDR arrayD, ADDR arrayD[4]INVOKE ExitProcess,0
main ENDPEND main
运行调试:
填充后:
交换后的数据
8.4.3 PROC伪指令
1.PROC伪指令的语法
32 位模式中,PROC伪指令基本语法如下所示:
labe1 PROC [attributes] [USES reglist], parameter_list
Label 是按照第3章说明的标识符规则、由用户定义的标号。Attributes是指下述任一内容:
[distanco] [langtype][visibility] prologueargl
表8-3对这些属性进行了说明。
表 8-3 PROC伪指令的属性域 | |
属性 | 说明 |
distance | NEAR或FAR。指定汇编器生成的RET指令(RET或RETF)类型 |
langtype | 指定调用规范(参数传递规范),如C、PASCAL或STDCALL。能覆盖由.MODEL伪指令指定的语言 |
visibility | 指明本过程对其他模块的可见性。选项包括PRIVATE、PUBLIC(默认项)和EXPORT。若可见性为EXPORT,则链接器把过程名放入分段可执行文件的导出表。EXPORT也使之具有了PUBLIC可见性 |
prologuearg | 指定会影响开始和结尾代码生成的参数 |
2 参数列表
PROC 伪指令允许在声明过程时,添加上用逗号分隔的参数名列表。代码实现可以用名称来引用参数,而不是计算堆栈偏移量,如 [ebp+8]:
label PROC [attributes] [USES reglist],parameter_1,parameter_2,......parameter_n
如果参数列表与PROC 在同一行,则PROC后面的逗号可以省略:
label PROC [ateributes],parameter1,parameter_2,....parameter_n
每个参数的语法如下:
paramName: type
ParamName 是分配给参数的任意名称,其范围只限于当前过程(称为局部作用域(localscope))。同样的参数名可以用于多个过程,但却不能作为全局变量或代码标号的名称。Type可以在这些类型中选择:BYTE、SBYTE、WORD、SWORD、DWORD、SDWORD、FWORD、QWORD或TBYTE。此外,type还可以是限定类型(qualified type),如指向现有类型的指针。下面是限定类型的例子:
PTR BYTE PTR SBYTE
PTR WORD PTR DWORD
PTR SWORD PTR SDWORD
PTR QWORD PTR TBYTE
虽然可以在这些表达式中添加NEAR和FAR属性,但它们只与更加专用的应用程序相关。限定类型还能够用TYPEDEF和STRUCT伪指令创建,具体内容参见第10章。
示例1 AddTwo 过程接收两个双字数值,用EAX返回它们的和数:
AddTwo PROC,val1:DWORD,val2:DWORDmov eax, val1add eax, val2ret
AddTwo ENDP
AddTwo汇编时,MASM生成的汇编代码显示了参数名是如何被转换为EBP 偏移量的。由于使用的是STDCALL,因此RET指令附加了一个常量操作数:
AddTwo PROCpush ebpmov ebp, espmov eax, dword ptr [ebp+8]add eax, dword ptr [ebp+0ch]leaveret 8
AddTwo ENDP
注意:用指令 ENTER 0,0来代替下面的语句,AddTwo过程也一样正确:
push ebp
mov ebp, esp
提示 要详细查阅MASM生成的过程伪代码,用调试器打开程序,并查看Disassembly窗口。
完整代码测试笔记
;8.4.3_1.asm 8.4.3 PROC伪指令INCLUDE Irvine32.inc.code
;示例1 AddTwo 过程接收两个双字数值,用EAX返回它们的和数:
AddTwo PROC, val1:DWORD, val2:DWORDmov eax, val1add eax, val2ret
AddTwo ENDPmain PROC;调用测试INVOKE AddTwo, 1111h, 2222hINVOKE ExitProcess,0
main ENDPEND main
运行调试:
示例2 FillArray过程接收一个字节数组的指针:
FillArray PROC,pArray:PTR BYTE...FillArray ENDP
完整代码测试笔记
;8.4.3_2.asm 8.4.3 PROC伪指令INCLUDE Irvine32.inc.data
count = 10
array WORD count DUP(?)
arrayB BYTE "12345678",0
.code
;示例2 FillArray过程接收一个字节数组的指针:
ArrayFill PROC, dwOffset:DWORD, pArray:PTR BYTE, sizeArray:DWORDpushad ;保存寄存器mov ebx, dwOffset;mov esi, dwOffset ;数组偏移量mov esi, pArray ;数组偏移量mov ecx, sizeArray ;数据长度cmp ecx, 0 ;ECX==0?je L2 ;是:跳过循环
L1:mov eax, 10000h ;随机范围0~FFFFhcall RandomRange ;从链接库生成随机数mov [esi], ax ;在数组中插入值add esi, TYPE WORD ;指向下一个元素loop L1
L2: popad ;恢复寄存器ret
ArrayFill ENDPmain PROC;调用测试mov esi, OFFSET arrayINVOKE ArrayFill, ADDR array, ADDR array, LENGTHOF array ;添加参数INVOKE ExitProcess,0
main ENDPEND main
运行调试:
填充后
示例3 Swap过程接收两个双字指针:
Swap PROC,pValX:PTR DWORD,pValY:PTR DWORD...Swap ENDP
完整代码测试笔记
;8.4.3_3.asm 8.4.3 PROC伪指令INCLUDE Irvine32.inc.data
count = 10
array WORD count DUP(?)
arrayD DWORD 11112222h, 33334444h.code
;示例3 Swap过程接收两个双字指针:
Swap PROC, pValX:PTR DWORD, pValY:PTR DWORDmov esi, pValX ;取偏移地址mov ebx, [esi] ;获取地址里的值mov edi, pValY ;取第2个数的偏移地址xchg ebx, [edi] ;交换数据mov [esi], ebxret
Swap ENDpmain PROC;调用测试mov esi, OFFSET arrayINVOKE Swap, ADDR arrayD, ADDR arrayD[4]INVOKE ExitProcess,0
main ENDPEND main
运行调试:
交换后:
示例4 Read File过程接收一个字节指针pBuffer,有一个局部双字变量fileHandle,并把两个寄存器保存人栈(EAX和EBX):
Read_File PROC USES eax ebx,pBuffer:PTR BYTELOCAL fileHandle:DWORDmov esi, pBuffermov fileHandle, eax..ret
Read_File ENDP
完整代码测试笔记
;8.4.3_4.asm 8.4.3 PROC伪指令INCLUDE Irvine32.inc.data
count = 10
array WORD count DUP(?)
arrayD DWORD 11112222h, 33334444h.code
;示例4 Read_File过程接收一个字节指针pBuffer,有一个局部双字变量fileHandle,并把两个寄存器保存人栈(EAX和EBX):
Read_File PROC USES eax ebx, pBuffer:PTR BYTELOCAL fileHandle:DWORDmov esi, pBuffer ;数组偏移量mov fileHandle, eaxmov ecx, LENGTHOF array ;数据长度cmp ecx, 0 ;ECX==0?je L2 ;是:跳过循环
L1:mov eax, 10000h ;随机范围0~FFFFhcall RandomRange ;从链接库生成随机数mov [esi], ax ;在数组中插入值add esi, TYPE WORD ;指向下一个元素loop L1
L2: ret
Read_File ENDPmain PROC;调用测试mov esi, OFFSET arrayINVOKE Read_File, ADDR array ;添加参数INVOKE ExitProcess,0
main ENDPEND main
运行调试:
MASM为Read_File生成的代码显示了在EAX和EBX入栈(由USES子句指定)前,如何为局部变量(fileHandle)预留堆栈空间:
Read_File PROCpush ebpmov ebp, espadd esp, 0FFFFFFFCh ;创建fileHandlepush eax ;保存EAXpush ebx ;保存EBXmov esi, dword ptr [ebp+8] ;pBuffermov dword ptr [ebp-4], eax ;fileHandlepop ebxpop eaxleaveret 4
Read_File ENDP
注意:尽管 Microsoft 没有采用这种方法,但 Read_File生成代码的开始部分还可以是这样的:
Read_File PROCenter 4, 0push eax(etc.)
ENTER 指令首先保存EBP,再将它设置为堆栈指针的值,并为局部变量保留空间。
由PROC修改的RET指令 当PROC有一个或多个参数时,STDCALL 是默认调用规范。假设PROC 有 n 个参数,MASM 将生成如下入口和出口代码:
push ebp
mov ebp, esp
...
...
leave
ret (n*4)
RET指令中的常数是参数个数乘以4(因为每个参数都是一个双字)。若使用了INCLUDE Irvine32.inc,则 STDCALL 是默认规范,它是所有 Windows API函数调用使用的调用规范。
3.指定参数传递协议
一个程序可以调用Irvine32链接库过程,反之,也可以包含能被C++程序调用的过程。为了提供这样的灵活性,PROC 伪指令的属性域允许程序指定传递参数的语言规范,并且能覆盖MODEL伪指令指定的默认语言规范。下例声明的过程采用了C调用规范:
Example PROC C,parm1:DWORD, param2:DWORD
若用INVOKE执行Examplel,汇编器将生成符合C调用规范的代码。同样,如果用STDCALL声明Example,INVOKE的生成代码也会符合这个语言规范:
Example1 PROC STDCALL,parm1:DWORD, parm2:DWORD
8.4.4 PROTO伪指令
64模式中,PROTO 伪指令指定程序的外部过程,示例如下:
ExitProcess PROTO
.code
mov ecx, 0
call ExitProcess
然而在 32 位模式中,PROTO 是一个更有用的工具,因为它可以包含过程参数列表。可以说,PROTO伪指令为现有过程创建了原型(prototype)。原型声明了过程的名称和参数列表。它还允许在定义过程之前对其进行调用,并验证参数的数量和类型是否与过程的定义相匹配。
MASM要求INVOKE调用的每个过程都有原型。PROTO必须在INVOKE之前首先出现。换句话说,这些伪指令的标准顺序为:
MySub PROTO ;过程原型
INVOKE MySub ;过程调用
MySub PROC ;过程实现
MySub ENDP ;过程结束
还有一种情况也是可能的:过程实现可以出现在程序的前面,先于调用该过程的INVOKE 语句。在这种情况下,
PROC 就是它自己的原型
MySub PROC ;过程定义
MySub ENDP
INVOKE MySub ;过程调用
假设已经编写了一个特定的过程,创建其原型也是很容易的,即复制 PROC 语句并做如下修改:
●将关键字PROC 改为PROTO。
●如有USES运算符,则把该运算符连同其寄存器列表一起删除。
比如,假设已经创建了 ArraySum 过程:
ArraySum PROC USES esi ecx,ptrArray:PTR DWORD, ;指向数线szArray:DWORD ;数线大小
;省略其余代码行.......
ArraySum ENDP
下面是与之对应的 PROTO 声明:
ArraySum PROTOptrArray:PTR DWORD, ;指向数线szArray:DWORD ;数线大小
PROTO 伪指令可以覆盖.MODEL 伪指令中的参数传递协议。但它必须与过程的 PROC声明一致:
Examplel PROTO C,parml: DWORD,parm2:DWORD
1.汇编时参数检查
PROTO 伪指令帮助汇编器比较过程调用和过程定义的参数列表。但是这个错误检查没有如 C 和 C++语言中那样重要。相反,MASM 检查参数正确的数量,并在某些情况下,匹配实际参数和形式参数的类型。比如,假设 Sub1 的原型声明如下:
Sub1 PROTO,p1:BYTE,p2:WORD, P3:PTR BYTE
现在定义变量:
.data
byte_1 BYTE 10h
word_1 WORD 2000h
word_2 WORD 3000h
dword_1 DWORD 12345678h
那么,下面是 Sub1 的一个有效调用:
INVOKE Subl, byte_1, word_1 ADDR byte_1
MASM 为这个INVOKE生成的代码显示了参数按逆序压人堆栈:
push 404000h ;指向 byte_1的指针
sub esp, 2 ;在栈项填充两个字节
push word ptr ds:[00404001h] ;word_l的值
mov al, byte ptr ds:[00404000h] ;byte_1的值
push eax
call 00401071
EAX 被覆盖,sub esp,2 指令填充接下来的两个堆栈单元,以扩展到 32 位。
MASM 会检测的错误 如果实际参数超过了形式参数声明的大小,MASM 就会产生一个错误:
INVOKE Sub1, word_1, word_2, ADDR byte_1 ;参数1错误
如果调用 Sub1 时参数个数太少或太多,则MASM 会产生错误:
INVOKE Subl, byte_1, word_2 ;错误:参数个数太少
INVOKE Subl, byte_1, ;错误:参数个数太多word_2,
word_2, ADDR byte_1, word_2
MASM 不会检测的错误
如果实际参数的类型小于形式参数的声明,那么 MASM 不会检测出错误:
INVOKE Subl, byte_1, byte_1, ADDR byte_1
相反,MASM 会把实际参数扩展为形式参数声明的类型大小。下面是INVOKE 示例生成的代码,其中第二个实际参数(byte_1)入栈之前,在 EAX 中进行了扩展:
push 404000h ;byte_1的地址
mov al, byte ptr ds:[00404001h] ;byte 1的值
movzx eax, al ;在EAX中扩展
push eax ;入栈
mov al, byte ptr ds:[00404000h] ;byte_1的值
push eax ;入栈
call 00401071 ;调用Sub1
如果在想要传递指针时传递了一个双字,则不会检测出任何错误。当子程序试图把这个堆栈参数用作指针时,这种情况通常会导致一个运行时错误:
INVOKE Sub1, byte_1, word_2, dword_1 ;无错误检出
完整代码测试笔记
;8.4.4.asm 8.4.4 PROTO伪指令
;PROTO伪指令为现有过程创建了原型(prototype)。原型声明了过程的名称和参数列表。INCLUDE Irvine32.incSub1 PROTO, p1:BYTE, p2:WORD, P3:PTR BYTE.data
byte_1 BYTE 10h
word_1 WORD 2000h
word_2 WORD 3000h
dword_1 DWORD 12345678h.code
main PROC;调用测试mov esi, OFFSET byte_1mov eax, 11223344hINVOKE Sub1, byte_1, word_1, ADDR byte_1 ;参数从右到左入栈INVOKE ExitProcess,0
main ENDP;00401025 push offset byte_1 (0404000h) ;4个字节
;0040102A sub esp,2
;0040102D push word ptr [word_1 (0404001h)] ;前面加了2个字节,这个入栈2个字节,合计4个字节
;00401034 mov al,byte ptr [byte_1 (0404000h)]
;00401039 push eax ;入栈4个字节
;0040103A call Sub1 (0401046h) ;调用测试 p3虽然是BYTE型指针,但这个参数本身是32位的地址
Sub1 PROC, p1:BYTE, p2:WORD, p3:PTR BYTEmov esi, p3 ;esi = 0404000hmov al, BYTE PTR [esi] ;al = 10hmov bx, p2 ;bx = 2000hmov al, p1 ;al = 10hret
Sub1 ENDPEND main
运行调试:
2. ArraySum 示例
现在再来看看第 5 章的 ArraySum 过程,它对一个双字数组求和。之前,过程用寄存器传递参数,现在,可以用PROC伪指令来声明堆栈参数:
;ArraySum.asm 8.4.4 PROTO伪指令
;PROTO伪指令为现有过程创建了原型(prototype)。原型声明了过程的名称和参数列表。INCLUDE Irvine32.incArraySum PROTO, ptrArray: PTR DWORD, szArray:DWORD.data
array DWORD 10000h, 20000h, 30000h, 40000h, 50000h
theSum DWORD ?.code
main PROC;INVOKE语句调用ArraySum,传递数组地址和元素个数:INVOKE ArraySum, ADDR array, ;数组地址LENGTHOF array ;元素个数mov theSum, eax ;保存和数INVOKE ExitProcess,0
main ENDP;两个32位数示和
ArraySum PROC USES esi ecx,ptrArray:PTR DWORD, ;指向数组szArray:DWORD ;数组大小mov esi, ptrArray ;数组地址mov ecx, szArray ;数组大小mov eax, 0 ;和数清零cmp ecx, 0 ;数组长度=0?je L2 ;是:退出
L1: add eax, [esi] ;将每个整数加到和数中add esi, 4 ;指向下一个整数loop L1 ;按数组大小重复
L2: ret ;和数保存在EAX中
ArraySum ENDPEND main
运行调试:
8.4.5 参数类别
过程参数一般按照数据在主调程序和被调用过程之间传递的方向来分类:
●输入类: 输入参数是指从主调程序传递给过程的数据。被调用过程不会被要求修改相应的参数变量,即使修改了,其范围也只能局限在自身过程中。
●输出类: 当主调程序向过程传递变量地址,就会产生输出参数。过程用地址来定位变量,并为其分配数据。比如,Win32 控制台库中的 ReadConsole函数,其功能为从键盘读入一个字符串。用户键入的字符由ReadConsole保存到缓冲区中,而主调程序传递的就是这个字符串缓冲区的指针:
.data
buffer BYTE 80 DUP(?)
inputHandle DWORD ?
.code
INVOKE ReadConsole, inputHandle, ADDR buffer(etc.)
●输入输出类: 输入输出参数与输出参数相同,只有一个例外:被调用过程预期参数引用的变量中会包含一些数据,并且能通过指针来修改这些变量。
8.4.6 示例:交换两个整数
下面的例子实现两个32位整数的交换。Swap过程有两个输人输出参数 pValX 和pValY,它们是交换数据的地址:
;Swap.asm 8.4.6 示例:交换两个整数
;下面的例子实现两个32位整数的交换。
;Swap过程有两个输人输出参数 pValX 和pValY,它们是交换数据的地址:INCLUDE Irvine32.incSwap PROTO, pValX:PTR DWORD, pValY:PTR DWORD.data
Array DWORD 10000h, 20000h.code
main PROC;显示交换前的数组mov esi, OFFSET Arraymov ecx, 2 ;计数值=2mov ebx, TYPE Arraycall DumpMem ;显示数组INVOKE Swap, ADDR Array, ADDR[Array+4] ;最数组的第1个和第2个数;显示交换后的数组call DumpMemexit
main ENDP
;------------------------------------------------
Swap PROC USES eax esi edi,pValX:PTR DWORD, ;第一个整数的指针pValY:PTR DWORD ;第二个整数的指针
;交换两个32位整数的值
;返回:无
;------------------------------------------------mov esi, pValX ;获得指针mov edi, pValY ;获得指针mov eax, [esi] ;取第一个整数xchg eax, [edi] ;与第二个数交换mov [esi], eax ;替换第一个整数ret ;PROC在这里生成 RET 8
Swap ENDP
END main
运行结果:
Swap过程的两个参数pValX和pValY都是输入输出参数。它们的当前值要输入到过程,而它们的新值也会从过程输出。由于使用的 PROC 带有参数,汇编器把 Swap 过程末尾的RET指令改为RET8(假设调用规范是STDCALL)。
8.4.7 调试提示
本节提醒编程者要注意的一些常见错误是汇编语言在传递过程参数时会遇到的,希望编程者永远不要犯这些错误。
1.参数大小不匹配
数组地址以其元素的大小为基础。比如,一个双字数组第二个元素的地址就是其起始地址加 4。假设调用8.4.6 节的 Swap 过程,并传递 DoubleArray 前两个元素的指针。如果错误地把第二个元素的地址计算为 DoubleArray+1,那么调用 Swap 后,DoubleArray 中的十六进制结果值也不正确:
.data
DoubleArray DWORD 10000h, 20000h
.code
INVOKE Swap, ADDR[DoubleArray+0], ADDR [DoubleArray+1]
2.传递错误类型的指针
在使用INVOKE时,要记住汇编器不会验证传递给过程的指针类型。例如,8.4.6 节的Swap 过程期望接收到两个双字指针,假若不小心传递的是指向字节的指针:
.data
ByteArray BYTE 10h, 20h, 30h, 40h, 50h, 60h, 70h, 80h
.code
INVOKE Swap, ADDR [ByteArray+0], ADDR [ByteArray+1]
程序可以汇编运行,但是当ESI 和 EDI 解引用时,就会交换两个 32 位数值。
3.传递立即数
如果过程有一个引用参数,就不要向其传递立即数参数。考虑下面的过程,它只有一个引用参数:
Sub2 PROC, dataPtr:PTR WORDmov esi, dataPtr ;获得地址mov WORD PTR [esi], 0 ;解引用,分配零ret
Sub2 ENDP
汇编下面的INVOKE 语句将导致一个运行时错误。Sub2 过程接收 1000h 作为指针的值,并解引用到内存地址1000h:
INVOKE Sub2,1000h
上例很可能会导致一般性保护故障,因为内存地址 1000h不大可能在该程序的数据段中。
8.4.8 WriteStackFrame过程
Irvine32链接库有个很有用的过程WriteStackFrame,用于显示当前过程堆栈帧的内容,其中包括过程的堆栈参数、返回地址、局部变量和被保存的寄存器。该过程由太平洋路德大学(Pacific Lutheran University)的詹姆斯·布林克(James Brink)教授慷慨提供,原型如下:
WriteStackFrame PROTO,numParam:DWORD, ;传递参数的数量numLocalVal:DWORD ;双字局部变量的数量numSavedReg:DWORD ;被保存寄存器的数量
下面的代码节选自 WriteStackFrame的演示程序:
;WriteStackFrame.asm INCLUDE Irvine32.incmyProc PROTO, x:DWORD, y:DWORD.data
Array DWORD 10000h, 20000h.code
main PROCmov eax, 0EAEAEAEAhmov ebx, 0EBEBEBEBhINVOKE myProc, 1111h, 2222h ;传递两个整数参数exit
main ENDPmyProc PROC USES eax ebx,x:DWORD, y:DWORDLOCAL a:DWORD, b:DWORDmov eax, xmov ebx, yPARAMS = 2LOCALS = 2SAVED_REGS = 2mov a, 0AAAAhmov b, 0BBBBhINVOKE WriteStackFrame, PARAMS, LOCALS, SAVED_REGSret
myProc ENDP
END main
该调用生成的输出如下所示:
还有一个过程名为WriteStackFrameName,增加了一个参数,保存拥有该堆栈帧的过程名:
WriteStackFrameName PROTO,numParam:DWORD, ;传递参数的数量numLocalVal:DWORD ;双字局部变量的数量numSavedReg:DWORD ;被保存寄存器的数量procName:PTR BYTE ;空字节结束的字符串
Irvine32链接库的源代码保存在本书安装目录(通常为C:\Irvine)的\Examples\Lib32子目录下,文件名为Irvine32.asm。
8.4.9 本节回顾
1.(真/假):CALL 指令不能包含过程参数。
答:真
2.(真/假):INVOKE伪指令最多能包含3个参数。
答:假
3.(真/假):INVOKE 伪指令只能传递内存操作数,不能传递寄存器值。
答:假
4.(真/假):PROC 伪指令可以包含USES 运算符,但PROTO 伪指令不可以。
答:真
8.5 新建多模块程序
大型源文件难于管理且汇编速度慢,可以把单个文件拆分为多个子文件,但是,对其中任何子文件的修改仍需对所有的文件进行整体汇编。更好的方法是把一个程序按模块(module)(汇编单位)分割。每个模块可以单独汇编,因此,对一个模块源代码的修改就只需要重汇编这个模块。链接器将所有汇编好的模块(OBJ文件)组合为一个可执行文件的速度是相当快的,链接大量目标模块比汇编同样数量的源代码文件花费的时间要少得多。
新建多模块程序有两种常用方法:其一是传统方法,使用 EXTERN 伪指令,基本上它在不同的x86 汇编器之间都可以进行移植。其二是使用Microsoft的高级伪指令INVOKE和PROTO,这能够简化过程调用,并隐藏一些底层细节。本节将对这两种方法进行说明,由编程者决定使用哪一种。
8.5.1隐藏和导出过程名
默认情况下,MASM使所有的过程都是public属性,即允许它们能被同一程序中任何其他模块调用。使用限定词 PRIVATE 可以覆盖这个属性:
mySub PROC PRIVATE
使过程为 private属性,可以利用封装原则将过程隐藏在模块中,如果其他模块有相同过程名,就还需避免潜在的重名冲突。
OPTION PROC: PRIVATE 伪指令 在源模块中隐藏过程的另一个方法是,把OPTION PROC:PRIVATE伪指令放在文件顶部。则所有的过程都默认为private,然后用PUBLIC 伪指令指明那些希望其可见的过程:
OPTION PROC: PRIVATE
PUBLIC mySub
PUBLIC 伪指令用逗号分隔过程名:
PUBLIC subi, sub2, sub3
或者,也可以单独指定过程为 public 属性:
mySub PROC PUBLIC
mySub ENDP
如果程序的启动模块使用了 OPTION PROC:PRIVATE,那么就应该将它(通常为main)指定为 PUBLIC,否则操作系统加载器无法发现该启动模块。比如:
main PROC PUBLIC
8.5.2 调用外部过程
调用当前模块之外的过程时使用 EXTERN 伪指令,它确定过程名和堆栈帧大小。下面的示例程序调用了subl,它在一个外部模块中:
INCLUDE Irvine32.inc
EXTERN sub1@0:PROC
.code
main PROCcall sub1@0exit
main ENDP
END main
当汇编器在源文件中发现一个缺失的过程时(由CALL指令指定),默认情况下它会产生错误消息。但是,EXTERN伪指令告诉汇编器为该过程新建一个空地址。在链接器生成程序的可执行文件时再来确定这个空地址。
过程名的后缀@n确定了已声明参数占用的堆栈空间总量(参见8.4节扩展PROC伪指令)。如果使用的是基本PROC伪指令,没有声明参数,那么EXTERN中的每个过程名后缀都为@0。若用扩展PROC伪指令声明一个过程,则每个参数占用4字节。假设现在声明的AddTwo带有两个双字参数:
AddTwo PROC,val1:DWORD,val2:DWORD...
AddTwo ENDP
则相应的EXTERN伪指令为EXTERN AddTwo@8:PROC。或者,也可以用PROTO伪指令来代替EXTERN:
AddTwo PROTO,val1:DWORD,val2:DWORD
完整代码测试笔记
;Sub1.asm 子过程INCLUDE Irvine32.inc.code
;调用测试 p3虽然是BYTE型指针,但这个参数本身是32位的地址
Sub1 PROC, p1:BYTE, p2:WORD, p3:PTR BYTEmov esi, p3 ;esi = 0404000hmov al, BYTE PTR [esi] ;al = 10hmov bx, p2 ;bx = 2000hmov al, p1 ;al = 10hret
Sub1 ENDP
END ;代码段结束
AddTwo.asm文件
;AddTwo.asm 子过程INCLUDE Irvine32.inc.code
AddTwo PROC, val1:DWORD, val2:DWORDmov eax, val1add eax, val2ret
AddTwo ENDP
END ;代码段结束
MyProc.asm文件
;MyProc.asm 文件.386
.model flat, stdcall
.stack 4096
;option casemap:none.data
value DWORD 20.code
add_two PROCmov eax, valueadd eax, 2ret
add_two ENDPAddNumbers PROC, val1:DWORD, val2:DWORDmov eax, val1 ; 第一个参数add eax, val2 ; 加第二个参数ret ; 清理栈
AddNumbers ENDPEND
调试测试文件
;8.5.2.asm 8.5.2 调用外部过程
;调用当前模块之外的过程时使用 EXTERN 伪指令,它确定过程名和堆栈帧大小。
;下面的示例程序调用了subl,它在一个外部模块中:
;MyProc.asm, Sub1.asm, AddTwo.asm和主文件要放在同一工程下编译 INCLUDE Irvine32.inc
;PROTO方式声明过程
Sub1 PROTO, p1:BYTE, p2:WORD, P3:PTR BYTE
AddTwo PROTO, val1:DWORD, val2:DWORD
;EXTERN声明过程
EXTERN add_two@0:PROC
EXTERN AddNumbers@8:PROC ; stdcall, 两个DWORD参数(4+4=8).data
byte_1 BYTE 10h
word_1 WORD 2000h
word_2 WORD 3000h
dword_1 DWORD 12345678h
result DWORD ?.code
main PROC;调用测试mov esi, OFFSET byte_1mov eax, 11223344hINVOKE Sub1, byte_1, word_1, ADDR byte_1 ;参数从右到左入栈INVOKE AddTwo, 11223344h, 55667788h;调用无参的add_two过程call add_two@0mov result, eax;调用有参的Addnumbers过程 push 10hpush 20hcall AddNumbers@8 ; 调用外部加法函数; eax现在包含30hINVOKE ExitProcess,0
main ENDPEND main
运行调试:
经过对比发现,PROTO方式声明过程比EXTERN方式声明过程要方便些,尤其是在有参数的情况下。
项目文件结构:
8.5.3 跨模块使用变量和标号
1.导出变量和符号
默认情况下,变量和符号对其包含模块是私有的(private)。可以使用 PUBLIC 伪指令输出指定过程名,如下所示:
PUBLIC count, SYM1
SYM1 = 10
.data
count DWORD 0
2.访问外部变量和符号
使用EXTERN伪指令可以访问在外部过程中定义的变量和符号:
EXTERN name : type
对符号(由EQU和=定义)而言,type应为ABS。对变量而言,type 是数据定义属性,如BYTE、WORD、DWORD和SDWORD,可以包含 PTR。例子如下:
EXTERN one:WORD, two:SDWORD, three:PTR BYTE, four:ABS
3.使用带EXTERNDEF 的INCLUDE 文件
MASM 中一个很有用的伪指令EXTERNDEF可以代替PUBLIC和EXTERN。它可以放在文本文件中,并用INCLUDE伪指令复制到每个程序模块。比如,现在用如下声明定义文件vars.inc:
;vars.inc
EXTERNDEF count: DWORD, SYM1:ABS
接着,新建名为sub1.asm的源文件,其中包含了count和SYM1,以及一条用于把vars.inc 复制到编译流中的INCLUDE 语句。
;sub2.asm
.386
.model flat,STDCALL
INCLUDE vars.inc
SYM1 = 10
.data
count DWORD 0
END
因为不是程序启动模块,因此 END 伪指令省略了程序入口标号,并且不用声明运行时堆栈。
现在再新建一个启动模块main.asm,其中包含vars.inc,并使用了count和SYM1:
;main.asm
.386
.model flat,stdcall
.stack 4096
ExitProcess proto, dwExitCode:DWORD
INCLUDE vars.inc
.code
main PROCmov count, 2000hmov eax, SYM1INVOKE ExitProcess, 0
main ENDP
END main
运行调试:
8.5.4示例:ArraySum程序
ArraySum 程序,第一次出现在第5章,是一个容易划分为模块的程序。现在通过其结构图(图8-5)来快速回顾一下它的设计。带阴影的矩形表示本书链接库中的过程。main 过程调用 PromptForIntegers, PromptForIntegers再调用WriteString和ReadInt。通常、为多模块程序的各种文件创建单独的磁盘目录最容易跟踪这些文件。这也是下一节将要展示的ArraySum 程序的做法。
8.5.5用Extern新建模块
多模块 ArraySum 程序有两个版本。本节展示的版本使用传统的 EXTERN 伪指令引用位于不同模块中的函数。稍后,8.5.6 节将用 INVOKE、PROTO 和 PROC 的高级功能来实现同样的程序。
PromptForintegers prompt.asm是PromptForIntegers过程的源代码文件。它显示提示要求用户输人三个整数,调用 ReadInt 获取数值,并将它们插人数组:
;_prompt.asm 8.5.5用Extern新建模块
;_prompt.asm是PromptForIntegers过程的源代码文件。
;它显示提示要求用户输人三个整数,调用 ReadInt 获取数值,并将它们插人数组:INCLUDE Irvine32.inc.code
;------------------------------------------
;提示用户为数组输入整数,
;并用用户输入填充该数组。
;接收:
; ptrPrompt:PTR BYTE ;提示信息字符串
; ptrArray:PTR DWORD ;数组指针
; arraySize:DWORD ;数组大小
;返回:无
;--------------------------------------------
PromptForIntegers PROCarraySize EQU [ebp+16]ptrArray EQU [ebp+12]ptrPrompt EQU [ebp+8]enter 0, 0 ;push ebp; mov ebp, esp; sub esp, 0pushad ;保存全部寄存器mov ecx, arraySizecmp ecx, 0 ;数据大小≤0?jle L2 ;是:退出mov edx, ptrPrompt ;提示信息的地址mov esi, ptrArray
L1: call WriteString ;显示字符串call ReadInt ;将整数读入EAXcall Crlf ;换行mov [esi], eax ;保存入数组add esi, 4 ;下一个整数loop L1
L2: popad ;恢复全部寄存器leave ;mov esp, ebp; pop ebp;ret 12 ;恢复堆栈
PromptForIntegers ENDP
END
ArraySum _arraysum.asm模块为ArraySum过程,计算数组元素之和,并用EAX返回计算结果:
;_arraysum.asm 8.5.5用Extern新建模块
;_arraysum.asm模块为ArraySum过程,计算数组元素之和,并用EAX返回计算结果:INCLUDE Irvine32.inc.code
;------------------------------------------
;计算32位整数数组之和
;接收:
; ptrArray:PTR DWORD ;数组指针
; arraySize:DWORD ;数组大小
;返回:EAX=和数
;--------------------------------------------
ArraySum PROCptrArray EQU [ebp+8]arraySize EQU [ebp+12]enter 0, 0 ;push ebp; mov ebp, esp; sub esp, 0push ecx ;EAX不入栈push esimov eax, 0 ;和数清零mov esi, ptrArraymov ecx, arraySizecmp ecx, 0 ;数据大小≤0?jle L2 ;是:退出
L1: add eax, [esi] ;将每个整数加到和数中add esi, 4 ;指向下一个整数loop L1
L2: pop esipop ecx ;恢复寄存器leave ;mov esp, ebp; pop ebp;ret 12 ;恢复堆栈
ArraySum ENDP
END
DisplaySum _display.asm模块为DisplaySum过程,显示标号和和数的结果
;_display.asm 8.5.5 用Extern新建模块
;_display.asm模块为DisplaySum过程,显示标号和和数的结果INCLUDE Irvine32.inc.code
;------------------------------------------
;在控制台显示和数。
;接收:
; ptrPrompt:PTR BYTE ;提示字符串的偏移量
; theSum:DWORD ;数组和数
;返回:无
;--------------------------------------------
DisplaySum PROCtheSum EQU [ebp+12]ptrPrompt EQU [ebp+8]enter 0, 0 ;push ebp; mov ebp, esp; sub esp, 0push eaxpush edxmov edx, ptrPrompt ;提示字符串的指针call WriteStringmov eax, theSumcall WriteInt ;显示 EAXcall Crlfpop edxpop eaxleave ;mov esp, ebp; pop ebp;ret 8 ;恢复堆栈
DisplaySum ENDP
END
Startup模块 Summain.asm模块为启动过程(main)。其中的EXTERN伪指令指定了三个外部过程。为了使源代码更加友好,用EQU伪指令再次定义了过程名:
ArraySum EQU ArraySum@0
PromptForIntegers EQU PromptForIntegers@0
DisplaySum EQU DisplaySum@0
每次过程调用之前,用注释说明了参数顺序。该过程使用STDCALL参数传递规范
;Sum_main.asm 8.5.5用Extern新建模块
;多模块示例
;本程序由用户输入多个整数,
;将它们存入数组,计算数组之和,
;并显示和数。INCLUDE Irvine32.inc
EXTERN PromptForIntegers@0:PROC
EXTERN ArraySum@0:PROC, DisplaySum@0:PROC;为方便起见,重新定义外部符号
ArraySum EQU ArraySum@0
PromptForIntegers EQU PromptForIntegers@0
DisplaySum EQU DisplaySum@0
;修改Count来改变数组大小:
Count = 3
.data
prompt1 BYTE "Enter a signed integer: ", 0
prompt2 BYTE "The sum of the integers is: ", 0
array DWORD Count DUP(?)
sum DWORD ?.code
main PROCcall Clrscr ;清屏;PromptForIntegers(addr prompt1, addr array, Count)push Countpush OFFSET arraypush OFFSET prompt1call PromptForIntegers;sum=ArraySum(addr array, Count)push Countpush OFFSET arraycall ArraySummov sum, eax;DisplaySum(addr prompt2, sum)push sumpush OFFSET prompt2call DisplaySumcall Crlfexit
main ENDP
END main
运行结果:
本程序的源文件保存在示例程序目录下的ch08\ModSum32 traditional 文件夹中。
接下来将了解如果使用 Microsoft 的 INVOKE 和 PROTO 伪指令,上述程序会发生怎样的变化。
8.5.6用INVOKE和PROTO新建模块
32位模式中,可以用Microsoft 的INVOKE、PROTO和扩展PROC伪指令(8.4 节)新建多模块程序。与更加传统的CALL和EXTERN相比,它们的主要优势在于:能够将INVOKE 传递的参数列表与PROC 声明的相应列表进行匹配。
现在用 INVOKE、PROTO 和高级 PROC 伪指令重新编写ArraySum。为每个外部过程创建含有PROTO伪指令的头文件是很好的开始。每个模块都会包含这个文件(用INCLUDE伪指令)且不会增加任何代码量或运行时开销。如果一个模块不调用特定过程,汇编器就会忽略相应的PROTO伪指令。本程序源代码位于\ch08\ModSum32_advanced foleder。
sum.inc头文件本程序的sum.inc头文件如下所示:
INCLUDE Irvine32.inc
PromptForIntegers PROTO,ptrPrompt:PTR BYTE, ;提示字符串ptrArray:PTR DWORD, ;数组指针arraySize:DWORD ;数组大小
ArraySum PROTO,ptrArray:PTR DWORD, ;数组指针arraySize:DWORD ;数组大小
DisplaySum PROTO,ptrPrompt:PTR BYTE, ;提示字符串theSum:DWORD ;数组之和
_prompt模块 prompt.asm文件用PROC伪指令为PromptForIntegers 过程声明参数,用INCLUDE 将sum.inc 复制到本文件:
;_prompt.asm 8.5.6 用INVOKE和PROTO新建模块INCLUDE sum.inc ;获得过程原型
.code
;--------------------------------------------
;提示用户输入数组元素值,并用用户输入
;填充数组。
;返回:无
;--------------------------------------------
PromptForIntegers PROC,ptrPrompt:PTR BYTE, ;提示字符串ptrArray:PTR DWORD, ;数组指针arraySize:DWORD ;数组大小pushad ;保存所有寄存器mov ecx, arraySizecmp ecx, 0 ;数组大小≤0?jle L2 ;是:退出mov edx, ptrPrompt ;提示信息的地址mov esi, ptrArray
L1: call WriteString ;显示字符串call ReadInt ;把整数读入EAXcall Crlf ;换行mov [esi], eax ;保存入数组add esi, 4 ;下一个整数loop L1
L2: popad ;恢复所有寄存器ret
PromptForIntegers ENDP
END
与前面的PromptForIntegers版本比较,语句enter0,0和leave不见了,这是因为当MASM 遇到PROC伪指令及其声明的参数时,会自动生成这两条语句。同样,RET指令也不需要自带常数参数了(PROC会处理好)。
_arraysum模块 接下来,arraysum.asm文件包含了ArraySum 过程。
;_arraysum.asm 8.5.6 用INVOKE和PROTO新建模块
;_arraysum.asm文件包含了ArraySum 过程。INCLUDE sum.inc ;获得过程原型
.code
;--------------------------------------------
;计算32位整数数组之和
;返回:EAX=和数
;--------------------------------------------
ArraySum PROC,ptrArray:PTR DWORD, ;数组指针arraySize:DWORD ;数组大小push ecx ;EAX不入栈push esimov eax, 0 ;和数清零mov esi, ptrArraymov ecx, arraySizecmp ecx, 0 ;数组大小≤0?jle L2 ;是:退出
L1: add eax, [esi] ;将每个整数加到和数中add esi, 4 ;指向下一个整数loop L1 ;按数组大小重复
L2: pop esipop ecx ;用EAX返回和数ret
ArraySum ENDP
END
_display模块 _display.asm 文件包含了DisplaySum 过程:
;_display.asm 8.5.6 用INVOKE和PROTO新建模块
;_display.asm 文件包含了DisplaySum 过程:INCLUDE sum.inc ;获得过程原型
.code
;--------------------------------------------
;在控制台显示和数。
;返回:无
;--------------------------------------------
DisplaySum PROC,ptrPrompt:PTR BYTE, ;提示字符串theSum:DWORD ;数组之和push eaxpush edxmov edx, ptrPrompt ;提示信息的指针call WriteStringmov eax, theSumcall WriteInt ;显示 EAXcall Crlfpop edxpop eaxret
DisplaySum ENDP
END
Sum_main模块 Sum main.asm(启动模块)包含主程序并调用所有其他的过程。它使用INCLUDE从sum.inc复制过程原型:
;Sum_main.asm 8.5.6 用INVOKE和PROTO新建模块
;整数求和程序(Sum main.asm)INCLUDE sum.inc ;获得过程原型
Count = 3
.data
prompt1 BYTE "Enter a signed integer: ", 0
prompt2 BYTE "The sum of the integers is: ", 0
array DWORD Count DUP(?)
sum DWORD ?
.code
main PROCcall Clrscr ;清屏INVOKE PromptForIntegers, ADDR prompt1, ADDR array, CountINVOKE ArraySum, ADDR array, Countmov sum, eaxINVOKE DisplaySum, ADDR prompt2, sumcall Crlfexit
main ENDP
END main
运行结果:
小结本节与上一节展示了在32位模式中新建多模块程序的两种方法--第一-种使用的是更传统的EXTERN伪指令,第二种使用的是INVOKEPROTO和 PROC 的高级功能。后一种中的伪指令简化了很多细节,并为WindowsAPI函数调用进行了优化。此外,它们还隐藏了一些细节,因此,编程者可能更愿意使用显式的堆栈参数和 CALL 及 EXTERN 伪指令。
8.5.7 本节回顾
1.(真/假):链接OBJ模块比汇编ASM 源文件快得多。
答:真
2.(真/假):将一个大型程序分割为多个短模块使得该程序更难维护。
答:假
3.(真/假):多模块程序中,带标号的 END 语句只在启动模块中出现一次。
答:真
4.(真/假):PROTO 伪指令会占用内存,因此,编程者必须注意过程中不包含 PROTO 伪指令,除非该过程确实会被调用。
答:假