编辑
2023-10-25
汇编
00
请注意,本文编写于 563 天前,最后修改于 562 天前,其中某些信息可能已经过时。

写个简单的c程序

c
#include <stdio.h> int add(int a, int b) { int result = a + b; return result; } <!-- more --> int main() { int x = 5; int y = 7; int sum = add(x, y); printf("Sum: %d\n", sum); return 0; }

接下来用gcc把它编译成汇编代码

bash
gcc -S -o add.s add.c

查看汇编代码cat add.s,这是一段x86_64汇编。

.file "add.c" // 源文件名 .text // 文本段开始 .globl add // 声明全局可见函数 add .type add, @function // 声明 add 为函数类型 add: // add 函数标签 .LFB0: // 函数基本块标签 .cfi_startproc // 开始定义过程 endbr64 // 启用分支目标地址检查 pushq %rbp // 将 %rbp 寄存器值压入栈 .cfi_def_cfa_offset 16 // 定义栈偏移量 .cfi_offset 6, -16 // 定义 %rbp 的偏移 movq %rsp, %rbp // 将栈指针保存到 %rbp .cfi_def_cfa_register 6 // 定义 %rbp 为栈指针 movl %edi, -20(%rbp) // 将 %edi 寄存器值存储到偏移 -20(%rbp) 的位置 movl %esi, -24(%rbp) // 将 %esi 寄存器值存储到偏移 -24(%rbp) 的位置 movl -20(%rbp), %edx // 将偏移 -20(%rbp) 处的值加载到 %edx 寄存器 movl -24(%rbp), %eax // 将偏移 -24(%rbp) 处的值加载到 %eax 寄存器 addl %edx, %eax // 将 %edx 和 %eax 寄存器值相加,结果存入 %eax movl %eax, -4(%rbp) // 将 %eax 寄存器值存储到偏移 -4(%rbp) 的位置 movl -4(%rbp), %eax // 将偏移 -4(%rbp) 处的值加载到 %eax 寄存器 popq %rbp // 弹出栈中的 %rbp 寄存器值 .cfi_def_cfa 7, 8 // 定义新的栈顶 ret // 函数返回指令 .cfi_endproc // 结束定义过程 .LFE0: // 函数结束标签 .size add, .-add // 计算函数大小 .section .rodata // 数据段开始 .LC0: // 字符串常量标签 .string "Sum: %d\n" // 定义字符串常量 .text // 文本段 .globl main // 声明全局可见函数 main .type main, @function // 声明 main 为函数类型 main: // main 函数标签 .LFB1: // 函数基本块标签 .cfi_startproc // 开始定义过程 endbr64 // 启用分支目标地址检查 pushq %rbp // 将 %rbp 寄存器值压入栈 .cfi_def_cfa_offset 16 // 定义栈偏移量 .cfi_offset 6, -16 // 定义 %rbp 的偏移 movq %rsp, %rbp // 将栈指针保存到 %rbp subq $16, %rsp // 减少栈指针 movl $5, -12(%rbp) // 将 5 存储到偏移 -12(%rbp) 的位置 movl $7, -8(%rbp) // 将 7 存储到偏移 -8(%rbp) 的位置 movl -8(%rbp), %edx // 将偏移 -8(%rbp) 处的值加载到 %edx 寄存器 movl -12(%rbp), %eax // 将偏移 -12(%rbp) 处的值加载到 %eax 寄存器 movl %edx, %esi // 将 %edx 寄存器值存入 %esi movl %eax, %edi // 将 %eax 寄存器值存入 %edi call add // 调用 add 函数 movl %eax, -4(%rbp) // 将 %eax 寄存器值存储到偏移 -4(%rbp) 的位置 movl -4(%rbp), %eax // 将偏移 -4(%rbp) 处的值加载到 %eax 寄存器 movl %eax, %esi // 将 %eax 寄存器值存入 %esi leaq .LC0(%rip), %rax // 将字符串常量地址存入 %rax movq %rax, %rdi // 将 %rax 寄存器值存入 %rdi movl $0, %eax // 将 0 存入 %eax 寄存器 call printf@PLT // 调用 printf 函数 movl $0, %eax // 将 0 存入 %eax 寄存器 leave // 离开函数 .cfi_def_cfa 7, 8 // 定义新的栈顶 .ret // 函数返回 .cfi_endproc // 结束定义过程 .LFE1: // 函数结束标签 .size main, .-main // 计算函数大小 .ident "GCC: (Ubuntu 11.2.0-19ubuntu1) 11.2.0" // 标识信息 .section .note.GNU-stack,"",@progbits // 标志堆栈是否可执行 .section .note.gnu.property,"a" // GNU属性 .align 8 // 对齐 .long 1f - 0f // 跳转偏移 .long 4f - 1f // 跳转偏移 .long 5 // 版本信息 0: // 标志 0 .string "GNU" // "GNU" 字符串 1: // 标志 1 .align 8 // 对齐 .long 0xc0000002 // 属性值 .long 3f - 2f // 跳转偏移 2: // 标志 2 .long 0x3 // 属性值 3: // 标志 3 .align 8 // 对齐 4: // 标志 4

总结一下,当调用一个函数时,需要为该函数的局部变量、参数、返回地址等信息分配内存空间。这些信息存储在函数栈帧中,函数栈帧是在栈上动态分配的,用于函数调用和返回。

  1. pushq %rbp:将当前函数的栈帧指针 %rbp 压入栈中,保存上一个函数的栈帧指针,以便后续恢复。
  2. movq %rsp, %rbp:将当前栈指针 %rsp 的值存储到 %rbp,创建新的栈帧,将 %rbp 指向当前函数的栈帧。
  3. subq $16, %rsp:分配 16 字节的栈空间,用于存储函数的局部变量。
  4. 将函数的参数存储到栈帧中,例如 movl $5, -12(%rbp)movl $7, -8(%rbp)
  5. call add:调用函数 add,将控制流跳转到 add 函数,同时将当前函数的返回地址存储在栈帧中。
  6. movl %eax, -4(%rbp):将 add 函数的返回值存储在栈帧中,以备后续使用。
  7. movl -4(%rbp), %eax:将存储在栈帧中的返回值加载到 %eax 寄存器中。
  8. popq %rbp:从栈中弹出之前保存的栈帧指针 %rbp,恢复到上一个函数的栈帧。
  9. ret:从栈帧中加载返回地址,将控制流恢复到调用函数的位置。

image.png

一时间看不懂上面的?来对比一下,多写几个函数

image.png 上面的addsubl就是加减指令,图中被框起来的就是寄存器取参数压栈,因为参数是局部变量。看好了,上面的返回值都被保存到了%eax这个寄存器。

image.png 在main这里,每次调用完一个函数就把%eax里的返回值存到变量里。对上了不是。基本符合上图。

本文作者:yowayimono

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!