运行时堆栈及函数调用中的部分相关指令

运行时堆栈

运行时堆栈是内存数组,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指令的执行过程

  1. CALL 指令将其返回地址压入堆栈
  2. 把被调用过程的地址复制到指令指针寄存器(EIP)
  3. 当过程准备返回时, 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不会修改标志位

猜你喜欢

转载自blog.csdn.net/qq_43313035/article/details/90215267