机器级代码
两种抽像:
- (Instruction Set Architecture,ISA) 定义机器级田可以存入格式和行为,定义了处理器状态,指令的格式,以及每条指令对状态的影响。
- 机器机程序使用的内存地址是虚拟地址,提供的内存模型看上去是一个非常大的按字节寻址的数组。
一些C 语言中隐藏的处理器状态:
-
程序计数器 (PC,
%rip
) 给出将要执行的下一条指令在内存中的地址。 - 整数寄存器(包含16个命名位置),分别存储64位的值,用来存储 地址,整数数据, 记录状态, 临时数据, 局部变量.
-
条件码寄存器 保存最近执行的算术或逻辑指令的状态信息。用来实现控制或数据流中的条件变化,比如说用来实现
if
或switch
语句。 - 一组向量寄存器 用来存放一个或多个整数或浮点数值。
目前一般 x86-64的虚拟地址是 由64位的字来表示,在目前的实现中这些地址的高16位必须设置为0,所以实现目前一般最高可以指定 64TB 内存地址空间。
代码示例
long mult2(long,long);
void multstore(long x, long y, long *dest){
long t = mult2(x,y);
*dest = t;
}
使用 gcc -Og -S mstore.c
编译之后得到的文件的主要内容如下:
.global _multstore
_multstore:
pushq %rbp
movq %rsp, %rbp
pushq %rbx
pushq %rax
movq %rdx, %rbx
callq _mult2
movq %rax, (%rbx)
addq $8, %rsp
popq %rbx
popq %rbp
retq
上面编译选项使用的是 -S
如果使用 -c
则可以得到汇编的目标文件。
objdump
工具可以用来反汇编目标文件。
➜ ch3 objdump -d mstore.o
mstore.o: file format Mach-O 64-bit x86-64
Disassembly of section __TEXT,__text:
_multstore:
0: 55 pushq %rbp
1: 48 89 e5 movq %rsp, %rbp
4: 53 pushq %rbx
5: 50 pushq %rax
6: 48 89 d3 movq %rdx, %rbx
9: e8 00 00 00 00 callq 0 <_multstore+0xE>
e: 48 89 03 movq %rax, (%rbx)
11: 48 83 c4 08 addq $8, %rsp
15: 5b popq %rbx
16: 5d popq %rbp
17: c3 retq
将上面的输出整理成下表:
Offset | Bytes | Equivalent assembly language |
---|---|---|
0: | 55 | pushq %rbp |
1: | 48 89 e5 | movq %rsp, %rbp |
4: | 53 | pushq %rbx |
5: | 50 | pushq %rax |
6: | 48 89 d3 | movq %rdx, %rbx |
9: | e8 00 00 00 00 | callq 0 <_multstore+0xE> |
e: | 48 89 03 | movq %rax, (%rbx) |
11: | 48 83 c4 08 | addq $8, %rsp |
15: | 5b | popq %rbx |
16: | 5d | popq %rbp |
17: | c3 | retq |
上面一共 24个字节分成了 11 组,代表了11条指令。 右边是等价的汇编语言。
比如 c3
字节表示的机器码对应了汇编指令就是 retq
。
AT&T 语法与 Intel 语法
gcc 与 objdump 等工具默认使用 AT&T 语法。也可以使用如下指令生成 Intel 语法格式的汇编。 gcc -Og -S -masm=intel mstore.c
对应的输出的代码如下:
.intel_syntax noprefix
global _multstore
_multstore:
push rbp
mov rbp, rsp
push rbx
push rax
mov rbx,rdx
call _mult2
mov qword ptr [rbx], rax
add rsp, 8
pop rbx
pop rbp
ret
这两种语法的对比如下:
语法 | AT&T | Intel | 说明 |
---|---|---|---|
指令名称 | pushq | push | Intel 语法省略了指示大小的后缀 |
寄存器引用 | %rbp | rbp | Intel 语法省略了寄存器前面的 % 符号 |
地址引用 | (%rbp) | qword ptr [rbp] | 内存位置 |
字面量 | $8 | 8 | Intel 语法省略了字面量前面的 $ 符号 |
多操作数顺序 | addq $8, %rsp | add rsp, 8 | 两种语法的操作数顺序相反 |
关于格式的注解
gcc -Og -S mstore.c
实际的完整如下:
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 13
.globl _multstore ## -- Begin function multstore
.p2align 4, 0x90
_multstore: ## @multstore
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi0:
.cfi_def_cfa_offset 16
Lcfi1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi2:
.cfi_def_cfa_register %rbp
pushq %rbx
pushq %rax
Lcfi3:
.cfi_offset %rbx, -24
movq %rdx, %rbx
callq _mult2
movq %rax, (%rbx)
addq $8, %rsp
popq %rbx
popq %rbp
retq
.cfi_endproc
## -- End function
.subsections_via_symbols
其中以 .
开头的行都是指导汇编器和链接器工作的伪指令。
汇编代码没有注释是很难看的。
基本要求是对于指令在右给出注释或对应的可能 C 语言方法。
函数的参数需要给出对应 C 语言的签名,参数的说明。
把 C 程序与汇编代码结合起来
- 用汇编编写完整的汇编代码(主要是函数),放进一个独立的汇编文件中,让汇编器和链接器反它和用 C 语言书写的代码合并起来。
- 用 GCC 内联汇编的特性,用
__asm__
伪指令可以在 C 程序中包含简短的汇编代码。
C 代码调用外部汇编函数代码
编写 add.s
文件,内容如下:
# 两整数想加,返回和
# @signature: int (int a,int b)
# @body: return a + b
# @param a - %edi
# @param b - %esi
# @return 返回两数想加和 - %eax
.global _add
_add:
pushq %rbp # 保存老的基址指针(即将 rbp 寄存器的值压入当前栈顶)
movq %rsp, %rbp # 将当前栈指针作为新的基址指针
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %esi
addl -8(%rbp), %esi
movl %esi, %eax
popq %rbp # 将栈顶保存的老的基址指针恢复到 rbp 寄存器
retq
编写 add_main.c
内容如下:
#include<stdio.h>
extern int add(int a,int b); // 在 add.s 中实现
int main(int argc, char const *argv[]) {
int sum = add(3,4);
printf("3 + 4 = %d\n", sum);
return 0;
}
编译与运行:
➜ ch3 cc -o add_main add_main.c add.s
➜ ch3 ./add_main
3 + 4 = 7
➜ ch3