第 6章 子程序设计
6.1 堆栈
6.2 子程序
6.3 Windows API
6.1 堆栈
供程序使用的一块连续的内存空间;
用于保存和读取一些临时的数据
堆栈中的数据有以下几个特点:
临时性
快速性
动态扩展性
6.1.1 堆栈空间
相关的 3个寄存器,SS,ESP和 EBP
在 Windows用户模式下
SS段寄存器通常和 DS,ES段寄存器相等
ESP寄存器中的内容作为堆栈的当前指针。
EBP寄存器中的内容作为堆栈的,基准,指针。
6.1.2 进栈和出栈指令
1.进栈指令 PUSH
格式,PUSH SRC
功能:堆栈指针 ESP减 4,SRC保存在 ESP指向的堆栈单元中 。
SRC可以是 32位寄存器,内存操作数,立即数或 16位段寄存器 。
2.出栈指令 POP
格式,POP DST
功能:从 ESP指向的堆栈单元中取出数据送到 DST中,堆栈指针 ESP加 4。
DST是 32位寄存器,内存操作数或 16位段寄存器 。
立即数不能作为 DST。
3,PUSH,POP指令要点注意进栈和出栈的顺序入栈,
PUSH EAX
PUSH EBX
PUSH ECX
PUSH EDX
出栈 (与入栈相反 )
POP EDX
POP ECX
POP EBX
POP EAX
4,PUSHFD指令格式,PUSHFD
功能:堆栈指针 ESP减 4,EFLAGS标志寄存器保存在 ESP指向的堆栈单元中 。
举例:将 EFLAGS标志寄存器复制到 EAX中
PUSHFD
POP EAX
5,POPFD指令格式,POPFD
功能:从 ESP指向的堆栈单元中取出数据送到 EFLAGS中,堆栈指针 ESP加 4。
举例,PUSHFD和 POPFD可以配对使用,用来保存和恢复程序某一时刻的标志 。
PUSHFD ; 保存状态寄存器
… ; 执行其他的指令
POPFD ; 恢复状态寄存器
6,ENTER指令格式,ENTER SRC1,SRC2
功能,SRC1和 SRC2是两个立即数 。
SRC2?0时,该指令相当于下面的 3条指令
PUSH EBP
MOV EBP,ESP
SUB ESP,SRC1
7,LEAVE指令格式,LEAVE
功能:令 ESP等于 EBP,再从堆栈弹出 EBP。
相当于:
MOV ESP,EBP
POP EBP
常用于子程序返回之前
6.1.3 堆栈的用途
1.临时保存寄存器的值
PUSH EAX
PUSH EBX
PUSH ECX
PUSH EDX

POP EDX
POP ECX
POP EBX
POP EAX
堆栈的用途(续)
2.临时保存变量的值
PUSH Count

POP Count
堆栈的用途(续)
3,用于变量之间的数据传递将变量 Var1的内容传递给 Var2:
PUSH Var1
POP Var2
堆栈的用途(续)
4,交换两个变量 Var1和 Var2的值
PUSH Var1
PUSH Var2
POP Var2
POP Var1
堆栈的用途(续)
5.用做临时的数据区
6.子程序的调用和返回
在调用子程序时,CALL指令自动在堆栈中保存其返回地址
从子程序返回时,RET指令从堆栈中取出返回地址
6.2 子程序在编写较复杂的程序时,可以把整个功能分解为若干小的易于实现的子功能。每一个子功能由子程序段来完成。
汇编语言中的子程序就是 C语言中的函数。
6.2.1 子程序的定义和调用伪指令 PROC和 ENDP用来定义子程序子程序名 PROC ; 表示子程序定义开始

RET
子程序名 ENDP ; 表示子程序定义结束
子程序名的命名规则和变量相同
子程序结束时,用 RET指令返回主程序
在主程序中,使用 CALL指令来调用子程序
PROC后面可跟其他参数
6.2.2 调用和返回指令
1,CALL指令格式,CALL SRC
功能:调用子程序,入口地址为 SRC。
SRC可以是,
程序名 ( 标号 )
32位寄存器
内存操作数
带段寄存器的远地址常见的 CALL指令的用法
CALL 子程序名
CALL指令后面跟的是寄存器或内存操作数,则将寄存器或内存单元中的值取出来作为入口地址,再调用子程序。
CALL指令后面跟的是带段寄存器的远地址,则由段寄存器来决定 CALL指令的操作,可能是
( 1)段间调用
( 2)提升特权级
( 3)任务切换
2,RET指令格式,RET [SRC]
功能:从子程序返回到主程序
RET指令用法,
( 1) 段间返回
子程序是由另外一个段的 CALL指令调用的
( 2) 降低特权级
从级别较低的特权级调用高特权级的程序
3,CALL,RET指令对堆栈的使用在程序中我们设计了两个子程序:
第 1个子程序 AddProc1使用 ESI和 EDI作为加数,做完加法后把和放在 EAX中;
第 2个子程序 AddProc2使用 A和 B作为加数,做完加法后把和放在 R中。
程序如右,callret.asm
结果,10 + 20 = 30
50 + 60 = 110
CALL指令执行时,它首先把返回地址作为一个双字压栈,再进入子程序执行 。
子程序最后执行的 RET指令从堆栈中取出返回地址,返回到主程序 。
CALL指令和 RET指令执行是必须依赖于堆栈的 。
高级语言的函数就是汇编语言的子程序 。
汇编语言传递参数有 3种常用方法:
( 1) 通过寄存器传递;
( 2) 通过数据区内的变量来传递;
( 3) 通过堆栈传递 。
6.2.3 C语言函数的参数传递方式
1,cdecl方式
cdecl方式是 C语言函数的默认方式调用规则,
( 1) 使用堆栈传递参数 。
( 2) 主程序按从右向左的顺序将参数逐个压栈,
( 3) 在子程序中,使用 [EBP+X]的方式来访问参数 。
( 4) 子程序用 RET指令返回 。
( 5) 由主程序执行,ADD ESP,n”指令调整 ESP,达到堆栈平衡 。
( 6) 子程序的返回值放在 EAX中 。
2,stdcall方式
Windows API采用的调用规则是 stdcall方式调用规则:
1.使用堆栈传递参数,使用从右向左的顺序将参数入栈。
2.堆栈的平衡是由子程序来完成的。
子程序使用,RET n”指令
子程序的返回值放在 EAX中
3,fastcall方式调用方式和 stdcall类似调用规则:
( 1)它使用 ECX传递第 1个参数,EDX传递第 2个参数。
( 2)其余参数采用从右至左的顺序入栈。
( 3)由子程序在返回时平衡堆栈。
4,this方式在 C++类的成员函数中使用 。 它使用 ECX传递 this指针,即指向对象 。
调用方式和 stdcall类似 。
5,naked方式想由编程者自行编写函数内的所有代码,
就使用 naked调用规则。
6.2.4 子程序的参数传递方式汇编语言中,向子程序传递参数可以按照 C程序的方式来处理 。
例如,
下面的子程序 AddProc3采用 cdecl方式,
AddProc4采用 stdcall方式。
程序示例,callrule.asm
6.2.5 带参数子程序的调用注意的两个方面
( 1)参数转换。子程序中用 [ebp+8]表示第 1个参数,用 [ebp+12]表示第 2个参数,
用 [ebp+16]表示第 3个参数,依次类推。
( 2)平衡堆栈。
一种方式是在子程序中用,RET n”平衡堆栈;
另一种方式是在主程序中用,ADD ESP,n”
平衡堆栈。
invoke伪指令
1.使用 invoke伪指令对主程序和子程序的简化在调用子程序时,使用 invoke伪指令,后面跟子程序名和各个参数的取值即可。
使用 invoke伪指令对前面的 callrule.asm进行简化,有以下几点:
invoke伪指令
( 1)子程序的调用规则
( 2) 子程序的参数
( 3) 子程序的进入 /退出代码
( 4) 子程序的返回指令
( 5) 主程序中采用 invoke语句程序示例,invoke.asm
机器指令列表,invoke2
对照 invoke.asm和机器指令列表,可以观察到以下几点:
( 1) 自动加入的指令
AddProc5,AddProc6中的一些语句是 MASM自动加入的
( 2) 参数的替换参数 a用 [EBP+8]替换,参数 b用 [EBP+12]替换 。
( 3) 返回指令
AddProc5采用 C规则,用,RET” 返回; AddProc6采用
stdcall规则,用,RET 8”返回
( 4) invoke语句转换为 CALL指令
invoke后面跟的参数被逐一压入堆栈,再跟上一条 CALL指令。
( 5)堆栈平衡
对 AddProc5的调用,在 CALL指令后面用,ADD ESP,8”
来平衡堆栈。
对 AddProc6的调用,由于在返回时使用了,RET 8”
在 CALL指令后面不需要,ADD ESP,8”
invoke伪指令(续)
2,使用 invoke调用子程序的一些限制
invoke伪指令后面跟的参数必须直接能够作为 PUSH指令的源操作数 。
错误的写法,invoke addproc,r*2,30
正确的写法,MOV EBX,r
SHL EBX,1
invoke addproc,EBX,30
6.2.6 子程序中的局部变量定义:仅在子程序内部使用的变 量,可 提高程序的模块化程度 也被 称为自动变量。
局部变量的作用域是子程序
1.局部变量的实现原理
( 1)在进入子程序的时候,通过修改堆栈指针
ESP来预留出需要的空间。用 SUB ESP,x指令预留空间,x为该子程序中所有局部变量使用的空间。
( 2) 返回主程序之前,通过恢复 ESP释放空间,
在堆栈中不再为子程序的局部变量保留空间 。
2.堆栈帧包括以下几个部分:
子程序的参数
返回地址
主程序的 EBP
子程序的局部变量在子程序中:
由 EBP可确定堆栈帧的位置
由 [EBP+0]可以得到主程序的堆栈帧
3.在子程序中使用局部变量
local伪指令可以在子程序中定义局部变量格式:
local 变量名 1[重复数量 ][:类型 ],变量名 2[重复数量 ][:类型 ]……
程序示例,local.asm
结果,r=20 s=10
4.直接使用局部空间如果不使用 local伪指令,可以直接在堆栈中为子程序分配局部变量空间,
但不如用 local伪指令方便 。
5,ADDR伪操作符
ADDR伪操作符可以取出局部变量的地址格式,ADDR 局部变量
ADDR只能在 invoke语句中使用程序示例,addr.asm
结果,r=50 s=40
6.2.7 子程序的嵌套程序中同样可以调用其他子程序,构成子程序的嵌套 。
嵌套深度没有一个具体的限制,主要取决于堆栈的容量 。
子程序调用时,返回地址要保存在堆栈中
子程序中的局部变量也要在堆栈中分配 。
6.2.8 子程序的递归子程序自己调用自己的情况称做递归。
以计算 xn为例来说明递归子程序的写法:
C语言程序,recurse.c
汇编程序,recurse.asm
结果,x=3 n=5 x(n) =243
6.3 Windows API
API在实质上也是一个子程序,主要有:
1,printf
2,scanf
3,MessageBox
4,确定函数的声明语句和库文件
5,MASM32工具包