汇编语言学习

最近想了解一下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