windows环境下32位汇编语言程序设计-第6章
按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
将某个函数导出的时候可以这样使用。默认的设置是PUBLIC。
● USES寄存器列表——表示由编译器在子程序指令开始前自动安排push这些寄存器的指令,并且在ret前自动安排pop指令,用于保存执行环境,但笔者认为不如自己在开头和结尾用pushad和popad指令一次保存和恢复所有寄存器来得方便。
● 参数和类型——参数指参数的名称,在定义参数名的时候不能跟全局变量和子程序中的局部变量重名。对于类型,由于Win32中的参数类型只有32位(dword)一种类型,所以可以省略。在参数定义的最后还可以跟VARARG,表示在已确定的参数后还可以跟多个数量不确定的参数,在Win32汇编中惟一使用VARARG的API就是wsprintf,类似于C语言中的printf,其参数的个数取决于要显示的字符串中指定的变量个数。
完成了定义之后,可以用invoke伪指令来调用子程序,当invoke伪指令位于子程序代码之前的时候,处理到invoke语句的时候编译器还没有扫描到子程序定义信息的记录,所以会有以下错误信息:
error A2006: undefined symbol : _ProcWinMain
这并不是说子程序的编写有错误,而是invoke伪指令无法得知子程序的定义情况,所以无法进行参数的检测。在这种情况下,为了让invoke指令能正常使用,必须在程序的头部用proto伪操作定义子程序的信息,“提前”告诉invoke语句关于子程序的信息,proto的用法见3。2。2节。当然,如果子程序定义在前的话,用proto的定义就可以省略了。
由于程序的调试过程中可能常常对一些子程序的参数个数进行调整,为了使它们保持一致,就需要同时修改proc语句和proto语句。在写源程序的时候有意识地把子程序的位置提到invoke语句的前面,省略掉proto语句,可以简化程序和避免出错。
3。4。2 参数传递和堆栈平衡
了解了子程序的定义方法后,让我们继续深入了解子程序的使用细节。在调用子程序时,参数的传递是通过堆栈进行的,也就是说,调用者把要传递给子程序的参数压入堆栈,子程序在堆栈中取出相应的值再使用,比如,如果要调用:
SubRouting(Var1;Var2;Var3)
经过编译后的最终代码可能是(注意只是“可能”):
push Var3
push Var2
push Var1
call SubRouting
add esp;12
也就是说,调用者首先把参数压入堆栈,然后调用子程序,在完成后,由于堆栈中先前压入的数不再有用,调用者或者被调用者必须有一方把堆栈指针修正到调用前的状态,即堆栈的平衡。参数是最右边的先入堆栈还是最左边的先入堆栈、还有由调用者还是被调用者来修正堆栈都必须有个约定,不然就会产生错误的结果,这就是在上述文字中使用“可能”这两个字的原因。各种语言中调用子程序的约定是不同的,所以在proc以及proto语句的语言属性中确定语言类型后,编译器才可能将invoke伪指令翻译成正确的样子,不同语言的不同点如表3。4所示。
表3。4 不同语言调用方式的差别
C SysCall StdCall BASIC FORTRAN PASCAL
最先入栈参数
右
右
右
左
左
左
清除堆栈者
调用者
子程序
子程序
子程序
子程序
子程序
允许使用VARARG
是
是
是注
否
否
否
注:VARARG 表示参数的个数可以是不确定的,如wsprintf函数,本表中特殊的地方是StdCall 的堆栈清除平时是由子程序完成的,但使用VARARG 时是由调用者清除的。
为了了解编译器对不同类型子程序的处理方式,先来看一段源程序:
;》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
Sub1 proc C _Var1;_Var2
mov eax;_Var1
mov ebx;_Var2
ret
Sub1 endp
;》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
Sub2 proc PASCAL _Var1;_Var2
mov eax;_Var1
mov ebx;_Var2
ret
Sub2 endp
;》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
Sub3 proc _Var1;_Var2
mov eax;_Var1
mov ebx;_Var2
ret
b3 endp
;》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》
…
invoke Sub1;1;2
invoke Sub2;1;2
invoke Sub3;1;2
编译后再进行反汇编,看编译器是如何转换处理不同类型的子程序的:
; 这里是Sub1 - C类型
:00401000 55 push ebp
:00401001 8BEC mov ebp; esp
:00401003 8B4508 mov eax; dword ptr 'ebp+08'
:00401006 8B5D0C mov ebx; dword ptr 'ebp+0C'
:00401009 C9 leave
:0040100A C3 ret
; 这里是Sub2 - PASCAL类型
:0040100B 55 push ebp
:0040100C 8BEC mov ebp; esp
:0040100E 8B450C mov eax; dword ptr 'ebp+0C'
:00401011 8B5D08 mov ebx; dword ptr 'ebp+08'
:00401014 C9 leave
:00401015 C20800 ret 0008
; 这里是Sub3 — StdCall类型
:00401018 55 push ebp
:00401019 8BEC mov ebp; esp
:0040101B 8B4508 mov eax; dword ptr 'ebp+08'
:0040101E 8B5D0C mov ebx; dword ptr 'ebp+0C'
:00401021 C9 leave
:00401022 C20800 ret 0008
…
; 这里是invoke Sub1;1;2 — C类型
:00401025 6A02 push 00000002
:00401027 6A01 push 00000001
:00401029 E8D2FFFFFF call 00401000
:0040102E 83C408 add esp; 00000008
; 这里是invoke Sub2;1;2 — PASCAL类型
:00401031 6A01 push 00000001
:00401033 6A02 push 00000002
:00401035 E8D1FFFFFF call 0040100B
; 这里是invoke Sub3;1;2 — StdCall类型
:0040103A 6A02 push 00000002
:0040103C 6A01 push 00000001
:0040103E E8D5FFFFFF call 00401018
可以清楚地看到,在参数入栈顺序上,C类型和StdCall类型是先把右边的参数先压入堆栈,而PASCAL类型是先把左边的参数压入堆栈。在堆栈平衡上,C类型是在调用者在使用call指令完成后,自行用add esp;8指令把8个字节的参数空间清除,而PASCAL和StdCall的调用者则不管这个事情,堆栈平衡的事情是由子程序用ret 8来实现的,ret指令后面加一个操作数表示在ret后把堆栈指针esp加上操作数,完成的是同样的功能。
Win32约定的类型是StdCall,所以在程序中调用子程序或系统API后,不必自己来平衡堆栈,免去了很多麻烦。
存取参数和局部变量都是通过堆栈来定义的,所以参数的存取也是通过ebp做指针来完成的。在探讨局部变量的时候,已经就没有参数的情况下ebp指针和局部变量的对应关系做了分析,现在来分析一下ebp指针和参数之间的对应关系,注意,这里是以Win32中的StdCall为例,不同的语言类型,指针的顺序可能是不同的。
假定在一个子程序中有两个参数,主程序调用时在 push 第一个参数前的堆栈指针esp为X,那么压入两个参数后的esp为X…8,程序开始执行call指令,call指令把返回地址压入堆栈,这时候eps为X…C,接下去是子程序中用push ebp来保存ebp的值,esp变为X…10,再执行一句mov ebp;esp,就可以开始用ebp存取参数和局部变量了,图3。4说明了这个过程。
图3。4 ebp指针、参数和局部变量的关系
在源程序中,由于参数、局部变量和ebp的关系是由编译器自动维护的,所以读者不必关心它们的具体关系,但到了用Soft…ICE等工具来分析其他软件的时候,遇到调用子程序的时候一定要先看清楚它们之间的类型差别。
在子程序中使用参数,可以使用与存取局部变量同样的方法,因为这两者的构造原理几乎一模一样,所以,在子程序中有invoke语句时,如果要用到输入参数的地址当做invoke的参数,同样要遵循局部变量的使用方式,不能用offset伪操作符,只能用addr来完成。同样,所有对局部变量使用的限制几乎都可以适用于参数。
来源:电子工业出版社 作者:罗云彬 上一页 回书目 下一页
上一页 回书目 下一页
第3章 使用MASM
3。5 高 级 语 法(1)
以前高级语言和汇编的最大差别就是条件测试、分支和循环等高级语法。高级语言中,程序员可以方便地用类似于if,case,loop和while等语句来构成程序的结构流程,不仅条理清楚、一目了然,而且维护性相当好。而汇编程序员呢?只能在cmp指令后面绞尽脑汁地想究竟用几十种跳转语句中的哪一种,这里就能列出近三十个条件跳转指令来:ja,jae,jb,jbe,jc,je,jg,jge,jl,jle,jna,jnae,jnb,jnbe,jnc,jne,jng,jnge,jnl,jnle,jno,jnp,jns,jnz,jo,jp,jpe,jpo和jz等。虽然其中的很多指令我们一辈子也不会用到,但就是这些指令和一些loop,loopnz以及被loop涉及的ecx等寄存器纠缠在一起,使在汇编中书写结构清晰、可读性好的代码变得相当困难,这也是很多人视汇编为畏途的一个原因。
现在好了,MASM中新引入了一系列的伪指令,涉及条件测试、分支和循环语句,利用它们,汇编语言有了和高级语言一样的结构,配合对局部变量和调用参数等高级语言中常见元素的支持,为使用Win32汇编编写大规模的应用程序奠定了基础。
3。5。1 条件测试语句
在高级语言中,所有的分支和循环语句首先要涉及条件测试,也就是涉及一个表达式的结果是“真”还是“假”的问题,表达式中往往有用来做比较和计算的操作符,MASM也不例外,这就是条件测试语句。
MASM条件测试的基本表达式是:
寄存器或变量 操作符 操作数
两个以上的表达式可以用逻辑运算符连接:
(表达式1)逻辑运算符(表达式2) 逻辑运算符(表达式3)…
允许的操作符和逻辑运算符如表3。5所示。
表3。5 条件测试中的操作符
操作符和逻辑运算 操 作 用 途
等于
变量和操作数之间的比较
!=
不等于
变量和操作数之间的比较
》
大于
变量和操作数之间的比较
》=
大于等于
变量和操作数之间的比较
续表
操作符和逻辑运算 操 作 用 途
《
小于
变量和操作数之间的比较
=3)&&ebx ;y大于等于3且ebx为非零值
(z&1)||!eax ;z和1进行“与”操作后非零或eax取反后非零
;也就是说x的位0等于1或者eax为零
细心的读者一定会发现,MASM的条件测试采用的是和C语言相同的语法。如!和&是对变量的操作符(取反和“与”操作),||和&&是表达式结果之间的逻辑“与”和逻辑“或”,而、!=、》、《 等是比较符。同样,对于不含比较符的单个变量或寄存器,MASM也是将所有非零值认为是“真”,零值认为是“假”。
MASM的条件测试语句有几个限制,首先是表达式的左边只能是变量或寄存器,不能为常数;其次表达式的两边不能同时为变量,但可以同时是寄存器。这些限制来自于80x86的指令,因为条件测试伪操作符只是简单地把每个表达式翻译成cmp或test指令,80x86的指令集中没有cmp 0;eax之类的指令,同时也不允许直接操作两个内存中的数,所以对这两个限制是很好理解的。
除了这些和高级语言类似的条件测试伪操作,汇编语言还有特殊的要求,就是程序中常常要根据系统标志寄存器中的各种标志位来做条件跳转,这些在高级语言中是用不到的,所以又增加了以下一些标志位的状态指示,它们本身相当于一个表达式:
CARRY? 表示Carry位是否置位
OVERFLOW? 表示Overflow位是否置位
PARITY? 表示Parity位是否置位
SIGN? 表示Sign位是否置位
ZERO? 表示Zero位是否置位
要测试eax等于ebx同时Zero位置位,条件表达式可以写为:
(eaxebx) && ZERO?
要测试eax等于ebx同时Zero位清零,条件表达式可以写为:
(eaxebx) && ! ZERO?
和C语言的条件测试同样,MASM的条件测试伪指令并不会改变被测试的变量或寄存器的值,只是进行“测试”而已,到最后它会被编译器翻译成类似于cmp或test之类的比较或位测试指令。
3。5。2 分支语句
分支语句用来根据条件表达式测试的真假执行不同的代码模块,MASM中的分支语句的语法如下:
。if 条件表达式1
表达式1为“真”时执行的指令
'。elseif 条件表达式2'
表达式2为“真”时执行的指令
'。elseif 条件表达式3'
表达式3为“真”时执行的指令
…
'。else'
所有表达式为“否”时执行的指令
。endif
注意:关键字if/elseif/else/endif的前面有个小数点,如果不加小数点,就变成宏汇编中的条件汇编伪操作了,结果可是天差地别。
为了说明编译器究竟是如何处理这些伪指令的,先写一段如下的源代码:
。if eax && (ebx 》= dwX) || !(dwY != ecx)
mov esi;1
。elseif edx
mov esi;2
。elseif esi & 1
mov esi;3
。elseif ZERO? && CARRY?
mov esi;4
。endif
然后反汇编:
;