• x86汇编之栈与子程序调用


    什么是栈

    栈与普通数据结构所说的栈的概念是相似的,遵循后进先出原则。不同的是汇编中所说的栈是一个在内存中连续的保存数据的区域,也即是实际存在的内存区域,进栈和出栈遵循后进先出原则。

    在x86架构中,栈是向下生长的,即栈顶指针小于栈底指针。

    ESP

    ESP是x86架构中用于保存当前栈顶位置的寄存器。更多详细内容请参阅参考资料[1]

    下面的两对代码是相互等价的
    入栈操作:

    push eax
    
    ;修改栈顶指针
    sub esp, 4  ; 由于是向下生长,所以esp - 4, 减去4是因为eax占4个字节
    mov DWORD PTR SS:[ESP], eax ;放入esp指定的内存区域
    

    出栈操作

    pop eax
    
    mov eax, dword ptr ss:[esp]
    add esp, 4  ;理解同入栈,注意这两行代码顺序与入栈不同
    

    清除栈顶数据

    假如我们要清除栈顶的四个双字的数据,只需要修改ESP即可

    add esp, 4 * 4  ; 一个双字占4个字节,共4个双字
    

    EBP

    栈的一个典型应用就是函数调用时的参数传递。ESP保存的是当前栈的栈顶指针,EBP保存的是当前stack frame的基址[2].

    [3]所述,在可执行环境中函数经常以stack frame的形式来进行参数传递和函数局部变量的访问。stack frame的概念使得每一个子程序(在汇编中函数通常称为子程序)都能够拥有独立的栈空间。当函数被调用时,以当前esp所在位置为基址创建了stack frame,当前的esp就是stack frame的栈帧基址,在执行其他命令之前需要把栈基址保存在ebp当中。

    值得注意的是栈帧的概念是逻辑上的概念,实际上并不存在。一个进程仍然只是拥有一个栈,只是为了方便子程序内部的使用而引入了栈帧的概念。

    standard entry sequence

    有关更多在子程序调用中如何使用栈帧概念进行子程序调用请参阅[3:1].

    一般而言,在子程序中首先要执行下面一段代码:

    push ebp    ;保存主调函数的栈帧基址
    mov ebp, esp    ;当前函数的栈帧基址
    sub esp, X  ;X表示函数中要用到的变量大小,用于分配空间
    

    例如一个C程序的函数:

    void MyFunction()
    {
        int a, b, c;
        ...
    }
    

    则对应汇编程序的进入代码为:

    _MyFunction:
        push ebp
        mov ebp, esp
        sub esp, 12 ;4 * 3, int 类型是dword
    

    若对上面的代码有:

    a = 10;
    b = 5;
    c = 2;
    

    则对应的汇编为:

    mov [ebp - 4], 10
    mov [ebp - 8], 5
    mov [ebp - 12],2
    

    为什么保存ebp

    为了更好的理解ebp,我们考虑下面带有参数的函数

    vod MyFunction2(int x, int y, int z)
    {
        ...
    }
    

    汇编代码如下:

    _MyFunction2:
        push ebp
        mov ebp, esp
        sub esp, 0; no local variables, most compilers will omit this line
    

    当调用函数时MyFunction2(10,5, 2),在汇编中调用格式如下:

    ;通过栈进行参数传递
    ; 参数从右向左压入栈,这样第一个pop出来的数据即是第一个参数
    push 2
    push 5
    push 10
    call _MyFunction2
    

    其中,call _MyFunction2等价于下列指令:

    push eip + 2 ;return address is current address + size of two instructions
    jmp _MyFunction2
    

    进入到子程序之后就要执行entry sequence代码:

    push ebp
    mov ebp, esp
    sub esp, X; X为局部变量需要的字节数目
    

    此时在栈中的内容如下:

    :    : 
    |  2 | [ebp + 16] (3rd function argument)
    |  5 | [ebp + 12] (2nd argument)
    | 10 | [ebp + 8]  (1st argument)
    | RA | [ebp + 4]  (return address)
    | FP | [ebp]      (old ebp value)
    |    | [ebp - 4]  (1st local variable)
    :    :
    :    :
    |    | [ebp - X]  (esp - the current stack pointer. The use of push / pop is valid now)
    

    就目前看来似乎并没有必要使用ebp,因为单单使用esp也能够解决问题,但是利用esp访问变量是不可靠的,因此需要ebp去访问变量,因此需要保存旧的ebp的值。

    Standard Exit Sequence

    standard exit sequence是用于撤销standard entry sequence的。

    void MyFunction3(int x, int y, int z)
    {
      int a, b, c;
      ...
      return;
    }
    
    _MyFunction3:
      push ebp
      mov ebp, esp
      sub esp, 12 ; sizeof(a) + sizeof(b) + sizeof(c)
      ;x = [ebp + 8]
      ;y = [ebp + 12]
      ;z = [ebp + 16]
      ;a = [ebp - 4] = [esp + 8]
      ;b = [ebp - 8] = [esp + 4]
      ;c = [ebp - 12] = [esp]
      mov esp, ebp ; 这一步是直接把栈顶指针指向保存返回地址的地方
                   ; 直接消除了局部变量的影响
      pop ebp
      ret 12 ; sizeof(x) + sizeof(y) + sizeof(z)
    

    参考资料


    1. x86 Disassembly/The Stack ↩︎

    2. What is between ESP and EBP? ↩︎

    3. x86 Disassembly/Functions and Stack Frames ↩︎ ↩︎

  • 相关阅读:
    软件工程实践总结
    用户使用调查报告
    Beta 冲刺 随笔合集
    Beta 冲刺 七
    Beta 冲刺 六
    Beta 冲刺 五
    Beta 冲刺 四
    Beta 冲刺 三
    Beta 冲刺 二
    Beta 冲刺 一
  • 原文地址:https://www.cnblogs.com/harrypotterjackson/p/12738705.html
Copyright © 2020-2023  润新知