汇编语言学习

最近想了解一下c是如何编译为汇编语言的
首先写了一个相当简单的c

1
2
3
4
5
6
7
8
9
10
#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编译为汇编语言:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
	.file	"hello.c"
.text
.section .rodata
.LC0:
.string "%d %d"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
truepushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
truemovq %rsp, %rbp
.cfi_def_cfa_register 6
truesubq $16, %rsp
truemovl $1, -16(%rbp)
truemovl $2, -12(%rbp)
truemovl -16(%rbp), %edx
truemovl -12(%rbp), %eax
trueaddl %edx, %eax
truemovl %eax, -8(%rbp)
truemovl $7, -4(%rbp)
truemovl -4(%rbp), %edx
truemovl -8(%rbp), %eax
truemovl %eax, %esi
trueleaq .LC0(%rip), %rdi
truemovl $0, %eax
truecall printf@PLT
truemovl $0, %eax
trueleave
.cfi_def_cfa 7, 8
trueret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 9.1.0"
.section .note.GNU-stack,"",@progbits

以”点”做为前缀的指令都是用来指导汇编器的命令。
精简一下差不多这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.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指向目标串.
1
2
3
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级别的优化:

1
gcc -O3 -S

生成的汇编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
	.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
truesubq $8, %rsp
.cfi_def_cfa_offset 16
truemovl $7, %edx
truemovl $3, %esi
truexorl %eax, %eax
trueleaq .LC0(%rip), %rdi
truecall printf@PLT
truexorl %eax, %eax
trueaddq $8, %rsp
.cfi_def_cfa_offset 8
trueret
.cfi_endproc
.LFE11:
.size main, .-main
.ident "GCC: (GNU) 9.1.0"
.section .note.GNU-stack,"",@progbits

从以上汇编代码中我们可以发现,优化之后的汇编代码将最后相加的值给了内存,而不像之前那样先占用两个内存空间然后再相加.


[1] x86-64 下函数调用及栈帧原理
[2] アセンブラ学習log_1