第 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工具包
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工具包