汇编语言 ======== 原代码:: #include 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 语言中以 // 表示注释语句是一样的 .. figure:: https://img.zhaoweiguo.com/knowledge/images/cores/compilers/assembler1.jpeg 助记符“movq”“xorl”中的“mov”和“xor”是指令,而“q”和“l”叫做后缀,表示操作数的位数。后缀一共有 b, w, l, q 四种,分别代表 8 位、16 位、32 位和 64 位 指令中使用操作数,可以使用四种格式,它们分别是:: 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)负责保护, 如果不想这些寄存器中的内容被破坏,那么要自己保护起来。 参考 ---- * https://stackoverflow.com/questions/40329260/why-is-the-address-of-static-variables-relative-to-the-instruction-pointer * https://stackoverflow.com/questions/3250277/how-to-use-rip-relative-addressing-in-a-64-bit-assembly-program * https://zhuanlan.zhihu.com/p/58774036