汇编语言¶
原代码:
#include <stdio.h>
int main(int argc, char* argv[]){
printf("Hello %s!\n", "Richard");
return 0;
}
生成的汇编代码:
$ clang -S -O2 hello.c -o hello.s
或者:
$ gcc -S -O2 hello.c -o hello.s
-S 参数就是告诉编译器把源代码编译成汇编代码
-O2 参数告诉编译器进行 2 级优化
把这段汇编代码编译成可执行文件:
$ as hello.s -o hello.o //用汇编器编译成目标文件
$ gcc hello.o -o hello //链接成可执行文件
$ ./hello //运行程序
1. 指令(instruction)
直接由 CPU 进行处理的命令
如:
pushq %rbp
movq %rsp, %rbp
说明:
开头的一个单词是助记符(mnemonic),后面跟着的是操作数(operand)
有多个操作数时以逗号分隔
2. 伪指令
伪指令以“.”开头,末尾没有冒号“:”
伪指令是是辅助性的,汇编器在生成目标文件时会用到这些信息,
但伪指令不是真正的 CPU 指令,就是写给汇编器的
3. 标签
标签以冒号“:”结尾,用于对伪指令生成的数据或指令做标记
L_.str: 标签是对一个字符串做了标记
标签很有用,它可以代表一段代码或者常量的地址(也就是在代码区或静态数据区中的位置)
4. 注释
以“#”号开头,这跟 C 语言中以 // 表示注释语句是一样的
指令中使用操作数,可以使用四种格式,它们分别是:
1. 立即数
立即数以 $ 开头, 比如 $40
如: 把 40 这个数字拷贝到 %eax 寄存器
movl $40, %eax
2. 寄存器
注: GNU 的汇编器规定寄存器一定要以 % 开头
3. 直接内存访问
操作数是一个数字时,它其实指的是内存地址(不要误以为它是一个数字,因为数字立即数必须以 $ 开头)
汇编代码里的标签,也会被翻译成直接内存访问的地址。
比如“callq _printf”中的“_printf”是一个函数入口的地址
汇编器帮我们计算出程序装载在内存时,每个字面量和过程的地址。
4. 间接内存访问
带有括号,比如(%rbp),它是指 %rbp 寄存器的值所指向的地址
间接内存访问的完整形式是:
偏移量(基址,索引值,字节数)
其地址是:基址 + 索引值 * 字节数 + 偏移量
举例来说:
8(%rbp),是比 %rbp 寄存器的值加 8
-8(%rbp),是比 %rbp 寄存器的值减 8
(%rbp, %eax, 4)的值,等于 %rbp + %eax*4
这个地址格式相当于访问 C 语言中的数组中的元素,数组元素是 32 位的整数,
其索引值是 %eax,而数组的起始位置是 %rbp。其中字节数只能取 1,2,4,8 四个值
几个常用的指令:
1. mov 指令
格式: mov 寄存器|内存|立即数, 寄存器|内存
2. lea 指令
lea 是“load effective address”的意思,装载有效地址
格式: lea 源,目的
例:
leaq L_.str(%rip), %rdi
把字符串的地址加载到 %rdi 寄存器
3. 算术运算的指令:
a) add
b) sub
c) imul
d) xor
e) or
f) and
g) inc
h) dec
i) neg
ii) not
4. 与栈有关的操作
a) push 源
b) pop 目的
5. 跳转类
a) jmp 标签/地址
6. 过程调用
a) call 标签/地址
b) ret
7. 比较操作
a) cmp 源1, 源2
b) test 源1, 源2
x86-64 架构的 CPU 最常用的是 16 个 64 位的通用寄存器:
%rax,%rbx,%rcx,%rdx,%rsi,%rdi,%rbp,%rsp
%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15
寄存器在历史上有各自的用途,比如:
rax 中的“a”,是 Accumulator(累加器) 的意思,这个寄存器是累加寄存器
但随着技术的发展,这些寄存器基本上都成为了通用的寄存器
为了方便软件的编写,我们还是做了一些约定,给这些寄存器划分了用途
1. %rax 除了其他用途外,通常在函数返回的时候,把返回值放在这里
2. %rsp 作为栈指针寄存器,指向栈顶
3. %rdi,%rsi,%rdx,%rcx,%r8,%r9 给函数传整型参数,依次对应第 1 参数到第 6 参数
超过 6 个参数怎么办?放在栈桢里
4. 如果程序要使用 %rbx,%rbp,%r12,%r13,%r14,%r15 这几个寄存器
是由被调用者(Callee)负责保护的,也就是写到栈里,在返回的时候要恢复这些寄存器中原来的内容。
其他寄存器的内容,则是由调用者(Caller)负责保护,
如果不想这些寄存器中的内容被破坏,那么要自己保护起来。