文章目录
运行时堆栈
运行时堆栈是内存数组,CPU用ESP(堆栈指针寄存器)对其进行直接管理,32位模式下,ESP寄存器存放的是堆栈中某个位置的32位偏移量.ESP基本上不会直接被程序员控制,它是用CALL,RET,PUSH和POP等指令间接进行修改.
运行时堆栈工作于系统层,处理子程序调用
如下图所示:ESP中保存的是刚压入堆栈数值(00000001)的偏移量。当指针数值减少时,栈顶也随之下移。
入栈操作(PUSH)
步骤:
- 把栈顶指针减4
- 将数值复制到栈顶指向的堆栈位置
下图是将数值0000B1压入堆栈的结果
出栈操作(POP)
步骤:
- 先将数值弹出堆栈
- 增加栈顶指针(按堆栈元素大小)
堆栈的应用
关于堆栈的应用可参考:https://blog.csdn.net/qq_43313035/article/details/90215278
函数调用中的相关指令
PUSH指令
PUSH指令首先减少ESP的值,再将源操作数复制到堆栈。操作数是16位的,则ESP减2,操作数是32位的,则ESP减4。
POP指令
POP指令首先把ESP指向的堆栈的元素复制到一个16位或者32位的目的操作数中去,再增加ESP的值。操作数是16位的,则ESP加2,操作数是32位的,则ESP加4。
PUSHFD和POPFD指令
- PUSHFD指令把32位EFLAGS寄存器内容压入堆栈
- POPFD指令则把栈定元素弹出到EFLAGS寄存器
不能用MOV指令把标识寄存器内容复制给一个变量,因此,PUSHFD可能就是保存标志位的最佳途径。有时候保存
标志寄存器的副本是非常有用的,这样之后就可以恢复标志寄存器原来的值。
通常会用PUSHFD和POPFD封存一段代码:
pushfd ;保存标志寄存器
;
;任意语句序列
;
popfd ; 恢复标志寄存器
当用这种方式使用入栈和出栈指令时,必须确保程序的执行路径不会跳过POPFD指令。当程序随着时间不断修改时,很难记住所有入栈和出栈指令的位置。
一种不容易出错的保存和恢复标识寄存器的方法是:将它们压入堆栈后,立即弹出给一个变量:
.data
saveflags DWORD ?
.code
pushfd ;标识寄存器内容入栈
pop saveflags; ;复制给一个变量
下述语句从同一个变量中恢复标识寄存器的内容
push saveflags ;被保存的标识入栈
popfd ;复制给标识寄存器
PUSHAD,PUSHA,POPAD,POPA
-
PUSHAD 指令按照 EAX、ECX、EDX、EBX、ESP(执行 PUSHAD 之前的值)、EBP、ESI 和 EDI 的顺序,将所有 32 位通用寄存器压入堆栈。
-
POPAD 指令按照相反顺序将同样的寄存器弹出堆栈。
-
PUSHA 指令按序(AX、CX、DX、BX、SP、BP、SI 和 DI)将 16 位通用寄存器压入堆栈。
-
POPA 指令按照相反顺序将同样的寄存器弹出堆栈。在 16 位模式下,只能使用 PUSHA 和 POPA 指令。
如果编写的过程会修改 32 位寄存器的值,则在过程开始时使用 PUSHAD 指令,在结束时使用 POPAD 指令,以此保存和恢复寄存器的内容。
举例:
MySub PROC
pushad ;保存通用寄存器的内容
.
.
mov eax,...
mov edx,...
mov ecx,...
.
.
popad ;恢复通用寄存器的内容
ret
MySub ENDP
必须要指岀,上述示例有一个重要的例外:过程用一个或多个寄存器来返回结果时,不应使用 PUSHA 和 PUSHAD。假设下述 ReadValue 过程用 EAX 返回一个整数;调用 POPAD 将会覆盖 EAX 中的返回值:
ReadValue PROC
pushad ;保存通用寄存器的内容
.
.
mov eax rreturn_value
.
.
popad ;覆盖 EAX
ret
ReadValue ENDP
CALL指令和RET指令
CALL 指令调用一个过程,指挥处理器从新的内存地址开始执行。过程使用 RET(从过程返回)指令将处理器转回到该过程被调用的程序点上。
CALL指令的执行过程:
- CALL 指令将其返回地址压入堆栈
- 把被调用过程的地址复制到指令指针寄存器(EIP)
- 当过程准备返回时, RET 指令从堆栈把返回地址弹回到指令指针寄存器(EIP)
32 位模式下,CPU 执行的指令由 EIP(指令指针寄存器)在内存中指岀。16 位模式下,由 IP 指出指令。
举例:
假设在 main 过程中,CALL 指令位于偏移量为 0000 0010 处。通常,这条指令需要 5 个字节的机器码,因此,下一条语句(本例中为一条 MOV 指令)就位于偏移量为 0000 0015 处:
main PROC
00000010 call fun
00000015 mov eax,ebx
然后,假设 fun过程中第一条可执行指令位于偏移量 0000 0050 处
fun PROC
00000050 mov eax edx
.
.
ret
fun ENDP
当 CALL 指令执行时如下图所示,调用之后的地址(0000 0015)被压入堆栈,fun的地址加载到 EIP
执行fun 中的全部指令直到 RET 指令。
当执行 RET 指令时,ESP 指向的堆栈数值被弹岀到 EIP,,ESP 的数值增加,从而指向堆栈中的前一个值。
ENTER指令
ENTER指令自动为被调用过程创建堆栈框架,它为局部变量保留堆栈空间并在堆栈上保存EBP
该指令执行以下动作:
- 在堆栈上压入EBP(push ebp)
- 把EBP设置为堆栈框架的基指针(mov ebp,esp)
- 为局部变量保留空间(sub,esp,numbytes)
ENTER指令有两个操作数:
- 第一个操作数是个常量,用于指定要为局部变量保留出多少堆栈空间(numbytes)
- 第二个操作数指定过程的嵌套层次(nestinglevel)
语法:
ENTER numbytes,nestinglevel
两个操作数都是立即数,numbytes总是向上取整为4的倍数,以使ESP按双字节边界地址对齐。nestinglevel决定了从调用过程复制到当前堆栈框架指针的数目。
LEAVE指令
LEAVE指令释放一个过程的堆栈框架。LEAVE指令执行与前面的ENTER指令相反的动作,把EBP和ESP恢复为过程开始的值。再次以mysub过程为例:
mysub PROC
enter 8,0
.
.
leave
ret
mysub ENDP
下面的指令与上面的指令是等价的,它首先为局部变量保留8字节的堆栈空间然后丢弃:
mysub PROC
push ebp
mov ebp,esp
sub esp,8
.
.
mov esp,ebp
pop ebp
ret
mysub ENDP
LEA指令
定义:LEA是微机8086/8088系列的一条指令,取自英语Load effect address——取有效地址,也就是取偏移地址。在微机8086/8088中有20位物理地址,由16位段基址向左偏移4位再与偏移地址之和得到
指令格式:
LEA 目的,源
指令功能:取源操作数地址的偏移量,并把它传送到目的操作数所在的单元。
LEA 指令要求原操作数必须是存储单元,而且目的操作数必须是一个除段寄存器之外的16位或32位寄存器。当目的操作数是16位通用寄存器时,那么只装入有效地址的低16位。
- 对于寄存器来说:第二个操作数是寄存器必须要加[],不然报错,lea就是取[寄存器]的值。
mov eax,2
lea ebx,[eax] ;ebx=2
mov ebx,eax ;ebx=2
lea ebx,eax;编译器报错:error A2070:invalid instruction operands
- 对于变量来说加不加[]]都是一样的效果,都是取变量的地址,相当于指针
如:
num dword 1
lea ebx,num
lea eax,[num] ;eax中保存的是num的地址,eax=ebx
举例:
- 假设:SI=1000H , DS=5000H, (1000H)=1234H
LEA BX , [SI] ;BX=1000H
MOV BX , [SI] ;BX=1234H
有时,LEA指令也可用取偏移地址的MOV指令替代
例如:
下面两条指令就是等价的,他们都取TABLE的偏移地址,然后送到BX中
LEA BX,TABLE
MOV BX,OFFSET TABLE
但有些时候,必须使用LEA指令来完成某些功能,不能用MOV指令来实现,必须使用下面指令:
LEA BX, 6[DI]
解释:某数组含20个元素,每个元素占一个字节,序号为0~19。设DI指向数组开头处,如果把序号为6的元素的偏移地址送到BX中
与mov指令的区别:
- 对于 变量 来说
num dword 1
mov eax,1
mov ebx,num
mov ecx,[num] ;执行完eax==ebx==ecx==2
- 对 寄存器 来说
mov ebx,eax ;ebx==2
mov ecx,[eax];可能会报错,这里翻译成汇编是mov ecx,DS:[eax]
- lea也同样可以实现类似mov的操作
例如:
lea edi,[ebx-0ch]
方括号表示存储单元,也就是提取方括号中的数据所指向的内容,然而lea提取内容的地址,这样就实现了把(ebx-0ch)放入到了edi中,但是 mov指令是不支持第二个操作数是一个寄存器减去一个数值的。
和offset的区别:
- lea:机器指令
- offset:伪指令
LEA BX, BUFFER ;在实际执行时才会将变量buffer的地址放入bx
MOV BX, OFFSET BUFFER ;在编译时就已经计算出buffer的地址为4300(假设),然后将上句替换为: mov bx,4300
- OFFSET只能取得用"数据定义伪指令"定义的变量的有效地址,不能取得一般操作数的有效地址(摘自80x86汇编语言程序设计教程)
MOV BX,OFFSET [BX+200]这句是错误的 应该用LEA BX,[BX+200]
LEA指令的应用举例
- 计算(EAX + EBX + 12345678)的值,注意这里有3个操作数,是add指令无法做到的:
LEA EAX, [ EAX + EBX + 1234567 ]
- 不覆盖目的寄存器值的情况下,计算(EBX + ECX)的结果,这也是add指令无法做到的:
LEA EAX, [ EBX + ECX ]
- 常数乘法(倍数N为1,2,3,4等)
LEA EAX, [ EBX + N * EBX ]
- 在loop循环中
LEA EAX, [ EAX + 1 ]
和
INC EAX
inc会修改EFLAGS标志位,lea不会修改标志位