计算机执行机器代码,用字节序列编码低级操作,而编译器基于编译语言的规则、目标机器的指令集和操作系统遵循的惯例,经过一系列阶段生成机器代码。GCC C语言编译器以汇编代码的形式产生输出,汇编代码是机器代码的文本表示。在本章中,我们会近距离的观察机器代码,以及人类可读的表示——汇编代码。
机器级代码
计算机系统使用了多种不同形式的抽象,利用更简单的抽象模型来隐藏实现的细节。对于机器变成来说,有两种抽象尤为重要。
- 由
指令集体系结构或指令集架构(ISA)
来定义机器级程序的格式和行为,它定义了处理器状态、指令的格式,以及每条指令对状态的影响。············· - 机器级程序使用的内存地址是虚拟地址,操作系统给程序提供了一个虚拟的内存模型。在更早以前,汇编程序员直接使用物理地址进行编程,我们不得不在编程时处处小心,仔细的分配每一块内存的使用,而现在我们不需要考虑这么多,整个内存都可以认为是我们当前一个程序在使用。
机器级代码与原始的C语言代码也有着巨大的差别。一些对C语言程序员隐藏的处理器状态都是可见的。
程序计数器(PC)
,在x86-64中用%rip寄存器表示(在8086处理器中用%ip表示),给出将要执行的下一条执行在内存中的地址。整数寄存器文件
,包含16个明明位置,分别存储64位的值。这些寄存器可以存储地址或者整数数据,有的寄存器可以被用来记录某些重要的程序状态,而其它的寄存器用来保存临时变量。条件码寄存器
,保存这最近执行的算术或逻辑指令的状态信息,用来实现C语言中的条件判断。一组向量寄存器
,可以存储一个或多个整数或者浮点数值。
代码示例
mstore.c
long mul2(long,long);
void mulstore(long x, long y, long *dest)
{
long t = mul2(x, y);
*dest = t;
}
用如下方式编译并汇编mstore.c并生成mstore.o
gcc -c mstore.c
接下来我们使用反汇编工具来检查汇编代码
objdump -d mstore.o
结果如下:
0000000000000000 <mulstore>:
#offset # Bytes
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 30 sub $0x30,%rsp
c: 48 89 7d e8 mov %rdi,-0x18(%rbp)
10: 48 89 75 e0 mov %rsi,-0x20(%rbp)
14: 48 89 55 d8 mov %rdx,-0x28(%rbp)
18: 48 8b 55 e0 mov -0x20(%rbp),%rdx
1c: 48 8b 45 e8 mov -0x18(%rbp),%rax
20: 48 89 d6 mov %rdx,%rsi
23: 48 89 c7 mov %rax,%rdi
26: e8 00 00 00 00 callq 2b <mulstore+0x2b>
2b: 48 89 45 f8 mov %rax,-0x8(%rbp)
2f: 48 8b 45 d8 mov -0x28(%rbp),%rax
33: 48 8b 55 f8 mov -0x8(%rbp),%rdx
37: 48 89 10 mov %rdx,(%rax)
3a: 90 nop
3b: c9 leaveq
3c: c3 retq
暂时你还不需要读懂这些代码,通过后面的学习我们会慢慢的了解这些指令。
除了编译并汇编一个文件,我们还需要学会如何生成可执行文件。
main.c
#include <stdio.h>
void mulstore(long, long, long *);
int main()
{
long d;
mulstore(2, 3, &d);
printf("2 * 3 --> %ld", d);
return 0;
}
long mult2(long a, long b)
{
long s = a * b;
return s;
}
指令如下
gcc -o main main.c mstore.c
该指令会把main.c和mstore.c一起编译,并通过-o
选项制定了可执行文件的名称。
关于格式的注解
我们可以通过-S
参数来直接查看c语言文件的汇编代码
gcc -S mstore.c
内容如下
.file "mstore.c"
.text
.globl mulst`在这里插入代码片`ore
.type mulstore, @function
mulstore:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $48, %rsp
movq %rdi, -24(%rbp)
movq %rsi, -32(%rbp)
movq %rdx, -40(%rbp)
movq -32(%rbp), %rdx
movq -24(%rbp), %rax
movq %rdx, %rsi
movq %rax, %rdi
call mul2@PLT
movq %rax, -8(%rbp)
movq -40(%rbp), %rax
movq -8(%rbp), %rdx
movq %rdx, (%rax)
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size mulstore, .-mulstore
.ident "GCC: (Ubuntu 9.3.0-10ubuntu2) 9.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
可以看到这与反汇编的结果类似,但还是有很多不同,其中大量的以.
开头的行我们并不需要关系,这是知道汇编器和链接器工作的伪指令。
此外,为了更加清晰地说明汇编代码,我们会用一种格式来表示汇编代码,它省略了大部分伪指令,但包括行号和解释性说明,对于上面的示例,带解释的汇编代码如下:
# void mulstore(long x,long y,long *dest)
# x in %rdi, y in %rsi, dest in %rdx
mulstore:
1 pushq %rbp # Save %dbx
2 movq %rsp, %rbp
3 subq $48, %rsp
4 movq %rdi, -24(%rbp)
5 movq %rsi, -32(%rbp)
6 movq %rdx, -40(%rbp)
7 movq -32(%rbp), %rdx
8 movq -24(%rbp), %rax
9 movq %rdx, %rsi # mov y to %rsi
10 movq %rax, %rdi # mov x to %rdi
11 call mul2@PLT # call mul2
12 movq %rax, -8(%rbp) # save result(%rax)
13 movq -40(%rbp), %rax
14 movq -8(%rbp), %rdx # mov result to %rdx
15 movq %rdx, (%rax) # mov %rdx to *dest
16 leave
17 ret
通常我们只会给出与讨论内容相关的代码行,每一行的左边都有编号供引用,右边是注释,简单地描述指令的效果以及它和原始C语言代码的关系。
本小节介绍了机器级代码和汇编语言,并为接下来的章节奠定基础。