最近想了解一下c是如何编译为汇编语言的
首先写了一个相当简单的c
#include "stdio.h"
int main(){
int a = 1;
int b = 2;
int c = a + b;
int d = 3 + 4;
printf("%d %d",c,d);
return 0;
}
使用gcc -S
编译为汇编语言:
.file "hello.c"
.text
.section .rodata
.LC0:
.string "%d %d"
.text
.globl main
.type main, @function
main:
.LFB0:
.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 $1, -16(%rbp)
movl $2, -12(%rbp)
movl -16(%rbp), %edx
movl -12(%rbp), %eax
addl %edx, %eax
movl %eax, -8(%rbp)
movl $7, -4(%rbp)
movl -4(%rbp), %edx
movl -8(%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
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 9.1.0"
.section .note.GNU-stack,"",@progbits
以”点”做为前缀的指令都是用来指导汇编器的命令。
精简一下差不多这样:
.LC0:
.string "%d %d"
.text
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
movl $1, -16(%rbp)
movl $2, -12(%rbp)
movl -16(%rbp), %edx
movl -12(%rbp), %eax
addl %edx, %eax
movl %eax, -8(%rbp)
movl $7, -4(%rbp)
movl -4(%rbp), %edx
movl -8(%rbp), %eax
movl %eax, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
leave
ret
首先了解一下几个寄存器[1]
- %rbp 是栈帧指针,用于标识当前栈帧的起始位置
- %rsp 是堆栈指针寄存器,通常会指向栈顶位置,堆栈的 pop 和push 操作就是通过改变 %rsp 的值即移动堆栈指针的位置来实现的。
- %eax 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
- %edx 则总是被用来放整数除法产生的余数。
- %rip 指令地址寄存器,用来存储 CPU 即将要执行的指令地址。每次 CPU 执行完相应的汇编指令之后,rip 寄存器的值就会自行累加;rip 无法直接赋值,call, ret, jmp 等指令可以修改 rip。
- %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。
- ESI/EDI 分别叫做"源/目标索引寄存器"(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串.
pushq %rbp
movq %rsp, %rbp
确认栈
movl $1, -16(%rbp)
把1移入寄存器rbq中-16是地址起始位置
movl $2, -12(%rbp)
跟上面相同
movl -16(%rbp), %edx
把-16(%rbp)
移入寄存器edx中,上面可知就是把1移入edx中
movl -12(%rbp), %eax
跟上面类似 不过这次用的是寄存器eax
addl %edx, %eax
把edx和eax中的内容相加,并存入edx中
movl %eax, -8(%rbp)
把eax中的内容存入rbq中位置为-8
movl $7, -4(%rbp)
把7存入rbq位置为-4,对应c代码为int d = 3 + 4
,由此可知编译器在编译时其实是会进行相应优化的
.LC0(%rip)
は、printfが使うアドレス、前もってprintfで使用する書式を設定している(就是说设置printf使用的地址,并提前设置格式)[2]
call printf@PLT
调用printf
leave
离开printf
ret
:结束当前函数
接下来我们可以简单的实战一下:
给gcc开启O3级别的优化:
gcc -O3 -S
生成的汇编码:
.file "hello.c"
.text
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d %d"
.section .text.startup,"ax",@progbits
.p2align 4
.globl main
.type main, @function
main:
.LFB11:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
movl $7, %edx
movl $3, %esi
xorl %eax, %eax
leaq .LC0(%rip), %rdi
call printf@PLT
xorl %eax, %eax
addq $8, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE11:
.size main, .-main
.ident "GCC: (GNU) 9.1.0"
.section .note.GNU-stack,"",@progbits
从以上汇编代码中我们可以发现,优化之后的汇编代码将最后相加的值给了内存,而不像之前那样先占用两个内存空间然后再相加.
[1] x86-64 下函数调用及栈帧原理
[2] アセンブラ学習log_1