计算机组成原理 | 逐行解析汇编代码中的栈调用

汇编技术名词

  • rbp(Register Base Pointer)是基址指针寄存器,它指向当前函数的栈帧的基址。栈帧是在函数调用期间用于保存局部变量和其他相关信息的一部分内存区域。通过在 rbp 中保存栈帧的基址,可以轻松地访问函数的参数和局部变量。
  • rsp(Register Stack Pointer)是栈指针寄存器,它指向当前栈顶的位置。栈是一种后进先出(LIFO)的数据结构,用于存储函数调用期间的临时数据。通过在 rsp 中保存栈顶的位置,可以在函数调用过程中分配和释放栈上的内存空间。
  • edx (Extended Data Register),数据寄存器,常用于存储和操作数据,作为通用寄存器之一。在函数调用中,edx 可用于存储参数值或临时变量。
  • eax (Extended Accumulator Register),累加器,用于执行算术和逻辑运算,以及处理函数返回值。eax 寄存器通常用于保存函数的返回值。
  • esi(Source Index Register)是源索引寄存器,通常用于存储源数据的地址或偏移量。它在字符串操作、循环遍历等场景中经常被使用。
  • edi(Destination Index Register)是目标索引寄存器,通常用于存储目标数据的地址或偏移量。它也常用于字符串操作、循环遍历等场景中。

一个简单的C语言示例:汇编代码逐行解析

function_example.c为例:

// function_example.c
#include 
int static add(int a, int b)
{
    return a+b;
}


int main()
{
    int x = 5;
    int y = 10;
    int u = add(x, y);
}

以下是function_example.c所对应的汇编代码:

int static add(int a, int b)
{
   0:   55                      push   rbp            ; 保存调用者的基址指针
   1:   48 89 e5                mov    rbp,rsp        ; 设置当前函数的基址指针
   4:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi  ; 将第一个参数 a 保存在栈帧中
   7:   89 75 f8                mov    DWORD PTR [rbp-0x8],esi  ; 将第二个参数 b 保存在栈帧中
    return a+b;
   a:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4] ; 将 a 加载到寄存器 edx
   d:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8] ; 将 b 加载到寄存器 eax
  10:   01 d0                   add    eax,edx         ; 将 a 和 b 相加并保存在寄存器 eax 中
}
  12:   5d                      pop    rbp             ; 恢复调用者的基址指针
  13:   c3                      ret                     ; 返回至调用者

0000000000000014 <main>:
int main()
{
  14:   55                      push   rbp            ; 保存调用者的基址指针
  15:   48 89 e5                mov    rbp,rsp        ; 设置当前函数的基址指针
  18:   48 83 ec 10             sub    rsp,0x10       ; 在栈上分配 16 字节的空间
    int x = 5;
  1c:   c7 45 fc 05 00 00 00    mov    DWORD PTR [rbp-0x4],0x5  ; 将值 5 存储在变量 x 的位置上
    int y = 10;
  23:   c7 45 f8 0a 00 00 00    mov    DWORD PTR [rbp-0x8],0xa  ; 将值 10 存储在变量 y 的位置上
    int u = add(x, y);
  2a:   8b 55 f8                mov    edx,DWORD PTR [rbp-0x8] ; 将变量 y 的值加载到寄存器 edx
  2d:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4] ; 将变量 x 的值加载到寄存器 eax
  30:   89 d6                   mov    esi,edx        ; 将 y 的值复制到 esi 寄存器,作为第二个参数
  32:   89 c7                   mov    edi,eax        ; 将 x 的值复制到 edi 寄存器,作为第一个参数
  34:   e8 c7 ff ff ff          call   0 <add>        ; 调用函数 add
  39:   

逐行解释上述汇编代码,首先对add函数进行解释:

  1. push rbp:将调用者的基址指针 rbp 压入栈中,以保存调用者函数的栈帧信息。

  2. mov rbp, rsp:将栈指针 rsp 的值复制给当前函数的基址指针 rbp,用于建立当前函数的栈帧。

  3. mov DWORD PTR [rbp-0x4], edi:将第一个参数 a 的值(存储在寄存器 edi 中)保存在栈帧中的偏移量为 -0x4 的位置上。

  4. mov DWORD PTR [rbp-0x8], esi:将第二个参数 b 的值(存储在寄存器 esi 中)保存在栈帧中的偏移量为 -0x8 的位置上。

  5. mov edx, DWORD PTR [rbp-0x4]:将栈帧中偏移量为 -0x4 处的值(即参数 a)加载到寄存器 edx 中。

  6. mov eax, DWORD PTR [rbp-0x8]:将栈帧中偏移量为 -0x8 处的值(即参数 b)加载到寄存器 eax 中。

  7. add eax, edx:将 eaxedx 寄存器中的值相加,结果存储在 eax 中,即得到了 a + b 的结果。

  8. pop rbp:弹出栈顶元素,即调用者函数的基址指针 rbp,以恢复调用者函数的上下文。

  9. ret:从当前函数返回,将控制流程返回给调用者。

接下来进入 main 函数的解释:

  1. push rbp:将调用者的基址指针 rbp 压入栈中,以保存调用者函数的栈帧信息。

  2. mov rbp, rsp:将栈指针 rsp 的值复制给当前函数的基址指针 rbp,用于建立当前函数的栈帧。

  3. sub rsp, 0x10:在栈上分配 16 字节的空间,用于存储局部变量和临时数据。

  4. mov DWORD PTR [rbp-0x4], 0x5:将值 5 存储在变量 x 的位置上,即栈帧中偏移量为 -0x4 的位置。

  5. mov DWORD PTR [rbp-0x8], 0xa:将值 10 存储在变量 y 的位置上,即栈帧中偏移量为 -0x8 的位置。

  6. mov edx, DWORD PTR [rbp-0x8]:将变量 y 的值加载到寄存器 edx 中。

  7. mov eax, DWORD PTR [rbp-0x4]:将变量 x 的值加载到寄存器 eax 中。

  8. mov esi, edx:将寄存器 edx 中的值(即变量 y 的值)复制到寄存器 esi1,作为第二个参数。

  9. mov edi, eax:将寄存器 eax 中的值(即变量 x 的值)复制到寄存器 edi 中,作为第一个参数。

  10. call 0 :调用函数 add,将控制流程转移到函数 add 的代码处。

参考文献

  • 徐文浩. 函数调用:为什么会发生stack overflow?极客时间. 2019

  1. 通过将参数值加载到寄存器中,函数 add 可以直接从寄存器中读取参数值进行计算,而无需直接访问内存中的变量。这样可以提高执行效率,并减少对内存的访问次数。 ↩︎

你可能感兴趣的:(计算机基础知识,#,计算机组成原理,汇编代码,C,计算机组成原理,汇编)