写个简单的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把它编译成汇编代码
bashgcc -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
总结一下,当调用一个函数时,需要为该函数的局部变量、参数、返回地址等信息分配内存空间。这些信息存储在函数栈帧中,函数栈帧是在栈上动态分配的,用于函数调用和返回。
pushq %rbp
:将当前函数的栈帧指针 %rbp
压入栈中,保存上一个函数的栈帧指针,以便后续恢复。movq %rsp, %rbp
:将当前栈指针 %rsp
的值存储到 %rbp
,创建新的栈帧,将 %rbp
指向当前函数的栈帧。subq $16, %rsp
:分配 16 字节的栈空间,用于存储函数的局部变量。movl $5, -12(%rbp)
和 movl $7, -8(%rbp)
。call add
:调用函数 add
,将控制流跳转到 add
函数,同时将当前函数的返回地址存储在栈帧中。movl %eax, -4(%rbp)
:将 add
函数的返回值存储在栈帧中,以备后续使用。movl -4(%rbp), %eax
:将存储在栈帧中的返回值加载到 %eax
寄存器中。popq %rbp
:从栈中弹出之前保存的栈帧指针 %rbp
,恢复到上一个函数的栈帧。ret
:从栈帧中加载返回地址,将控制流恢复到调用函数的位置。一时间看不懂上面的?来对比一下,多写几个函数
上面的
add
和subl
就是加减指令,图中被框起来的就是寄存器取参数压栈,因为参数是局部变量。看好了,上面的返回值都被保存到了%eax这个寄存器。
在main这里,每次调用完一个函数就把%eax里的返回值存到变量里。对上了不是。基本符合上图。
本文作者:yowayimono
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!