下面有这样一个简单的c 程序
//file 1.c
#include <stdio.h>
int add(int a,int b){
return a+b;
}
int main(){
int result = add(1,2);
printf("%d\n",result);
return 0;
}
我们使用 gcc -S 1.c ,可以将 上面这段代码转换为同目录下的 1.s 汇编文件,其内容如下:
.file "1.c"
.text
.globl add
.type add, @function
add:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size add, .-add
.section .rodata
.LC0:
.string "%d\n"
.text
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $2, %esi
movl $1, %edi
call add
movl %eax, -4(%rbp)
movl -4(%rbp), %eax
movl %eax, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
我们将编译器生成的一些注释信息给去掉,不影响程序功能。并且注释如下:
add:
# 当我们调用 call 指令的时候,会自动将 main 函数里面 调用 add 的那一行的下一行代码压入栈
# 现在rbp 存的是调用者 main 函数的栈帧的栈底地址,下面这句将 rbp 的全部64位入栈
pushq %rbp
# 现在 rsp 存的是main 函数栈顶地址,下面这一句将 rsp指针作为当前栈帧的栈底指针(上一个栈帧的栈顶作为当前栈帧的栈底,栈帧直接是紧挨着的)
movq %rsp, %rbp
# 在 main 函数中,已经将入参分别放到了 edi,esi 寄存器中,所以下面这两句是将第一个参数和第二个参数入栈,-4 和-8表示的都是地址偏移多少字节
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
# 这两句又将栈里面的两个参数复制到 edx,eax 寄存器
movl -4(%rbp), %edx
movl -8(%rbp), %eax
# 在这里进行加法操作,将 edx 的值加到 eax 里面去,因此这里 eax 存放的就是最终结果
addl %edx, %eax
# 下面这句将原先存入到栈里面的,main 函数的栈底指针恢复
popq %rbp
ret
LC0:
.string "%d\n"
.globl main
main:
# 保存上一个栈帧的栈底地址
pushq %rbp
# 将上一个栈帧的栈顶地址作为本栈帧的栈底地址
movq %rsp, %rbp
# 将 本栈帧的栈顶地址往栈容量增大方向挪动16个字节
subq $16, %rsp
# 将 add 函数的入参分别存到 esi、edi 寄存器
movl $2, %esi
movl $1, %edi
# 跳转到子函数
call add
# 子函数返回之后,eax 保存的是返回值,下面这句将返回值存到本栈帧底部的第一个位置
movl %eax, -4(%rbp)
# 又从该位置保存会 eax 寄存器,这是为啥?这不是多此一举吗?
movl -4(%rbp), %eax
# 又把 eax 的值复制到 esi
movl %eax, %esi
# 下面这句 将 LC0地址+rip地址,结果放到 rdi 中,成为后面printf@PLT系统调用的参数
leaq LC0(%rip), %rdi
# 这句可省略
movl $0, %eax
call printf@PLT
# 这句也可以省略
movl $0, %eax
# 下面这句相当于 movq %rbp %rsp;popq $rbp,即回复调用 main 函数前的栈底和栈顶指针
leave
# 返回
ret
可以看出,c 编译 出的汇编还是有一定的冗余的,上面的代码至少还可以注释掉6句而保持功能不变,如下:
add:
pushq %rbp
movq %rsp, %rbp
# movl %edi, -4(%rbp)
# movl %esi, -8(%rbp)
# movl -4(%rbp), %edx
# movl -8(%rbp), %eax
addl %edx, %eax
popq %rbp
ret
LC0:
.string "%d\n"
.globl main
main:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movl $2, %eax
movl $1, %edx
call add
movl %eax, -4(%rbp)
# movl -4(%rbp), %eax
movl %eax, %esi
leaq LC0(%rip), %rdi
# movl $0, %eax
call printf@PLT
# movl $0, %eax
leave
ret