第三章 程序的机器级表示
3.2.1 机器级代码
对于机器级编程来说,其中两种抽象尤为重要。第一种是由捍令集体系结构或指令集架构(Instruction Set Architecture, ISA)来定义机器级程序的 格式和行为,它定义了处理器状态、指令的格式,以及每条指令对状态的影响。
第二种抽象是,机器级程序使用的内存地址是虚拟地址,提供的内存模型看上去是一个非常大的字节数组。存储器系 统的实际实现是将多个硬件存储器和操作系统软件组合起来。
汇编代码表示非常接近于机器代码。与机器代码的二进制格式相比,汇编代码的主要特点是它用可读性更好的文本格式表示。 能够理解汇编代码以及它与原始C代码的联系,是理解计算机如何执行程序的关键一步。
X86-64 的机器代码和原始的 C代码差别非常大。一些通常对C语言程序员隐藏的处理器状态都是可见的:
程序计数器(通常称为 “PC”,在 x86-64 中用%rip 表示)给出将要执行的下一条指令在内存中的地址。
整数寄存器文件包含16个命名的位置,分别存储64位的值。这些寄存器可以存储地址 (对应于C语言的指针)或整数数据。有的寄存器被用来记录某些重要的程序状态,而其 他的寄存器用来保存临时数据,例如过程的参数和局部变量,以及函数的返回值。
条件码寄存器保存着最近执行的算术或逻辑指令的状态信息。它们用来实现控制或 数据流中的条件变化,比如说用来实现if和while语句。
一组向量寄存器可以存放一个或多个整数或浮点数值。
程序内存包含:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调 用和返回的运行时桟,以及用户分配的内存块(比如说用malloc库函数分配的)。
3.2.2 代码示例
假设我们写了一个C语言代码文件mstore.c,在命令行上使用 “-s” 选项,就能看到C语言编译器产生的汇编代码:
linux> gcc -Og -S mstore.c
这会使GCC运行编译器,产生一个汇编文件mstore.s,但是不做其他进一步的工 作。(通常情况下,它还会继续调用汇编器产生目标代码文件)。
如果我们使用 “-c” 命令行选项,GCC会编译并汇编该代码:
linux> gcc-Og-c mstore.c
这就会产生目标代码文件mstore.o, 它是二进制格式的,所以无法直接查看。
要查看机器代码文件的内容,有一类称为反汇编器(disassembler)的程序非常有用。
3.3 数据格式
由于是从16位体系结构扩展成32位的,Intel用术语 “字(word)” 表示 16位数据类型。因此,称32 位数为 “双字(double words)”,称 64 位数为 “四字(quad words)”。 图3-1 给出了C语言基本数据类型对应的 x86-64 表示。
3.4 访问信息
一个x86-64 的中央处理单元(CPU)包含一组 16 个存储 64 位值的通用目的寄存器。 这些寄存器用来存储整数数据和指针。
字节级操作可以访问最低的字节,16位操作可以访问最低的2个字 节,32位操作可以访问最低的4个字节,而64位操作可以访问整个寄存器。
3.4. 1 操作数指示符
大多数指令有一个或多个操作数(operand), 指示出执行一个操作中要使用的源数据 值,以及放置结果的目的位置。
操作数的可能性被分为三种类型。
第一种类型是立即数(immediate), 用来表 示常数值。
第二种类型是寄存器(register), 它表示某个寄存器的内容,16个寄存器的低位1字节、2字节、4 字节或8字节中的一个作为操作数, 这些字节数分别对应于8位、16位、32位或64位。
第三类操作数是内存引用,它会根据计算出来的地址(通常称为有效地址)访问某个内存位置。
3.4.2 数据传送指令
最频繁使用的指令是将数据从一个位置复制到另一个位置的指令。操作数表示的通用 性使得一条简单的数据传送指令能够完成在许多机器中要好几条不同指令才能完成的功能。
最简单形式的数据传送指令一MOV类。这些指令把数据从源位置 复制到目的位置,不做任何变化。
MOV 类由四条指令组成:movb、movw、movl 和 movq。
这些指令都执行同样的操作;主要区别在于它们操作的数据大小不同:分别是1、 2、4 和8字节。
注意事项:
源操作数指定的值是一个立即数,存放在寄存器活着内存中。
目的操作数指定的是一个位置,寄存器或者内存地址。
X86-64 限制,两个操作数都不能都是内存地址,
要么从内存移入寄存器,
要么寄存器移入内存,
要么寄存器移入寄存器
只有有名字的寄存器才能作为指令的操作数。即可以作为 MOV 命令的操作数的寄存器,只能是前文讲过的 16 个整数寄存器之一
寄存器部分的大小必须于指令的最后一个字符(‘b’,‘w’,‘l’,‘q’)指定的大小匹配
如果 MOV 传送 4 字节到寄存器(movl指令),按照之前 x86-64 的惯例,指令会将目的寄存器的高四字节清零。
惯例:任何为寄存器生成 32 位值的指令都会把该寄存器的高位部分置为 0。
常规 mov 命令只能处理 32 位的源操作数,然后经过拓展变为 64 位数。
movbsq可以直接将 64 位数作为源操作数,但目的操作数只能是寄存器。
3.4.3 压入和弹出栈数据
找在处理过程调用中起到至关重要的作用。栈是一种数据结构,可以添加或者删除值,不过要遵循 “后进先出” 的原则。通过push操作把数据压入栈中,通过pop操作删除数据;它具有一个属性:弹出的值永远是最近被压人而且仍然在栈中的值。
将一个四字值压人栈中,首先要将栈指针减8,然后将值写到新的栈顶地址。
因此,指令pushq %rbp的行为等价于下面两条指令:
subq $8,%rsp
这条指令用于分配额外的堆栈空间,其含义是从栈指针 %rsp中减去立即数 8。在x86-64汇编中,subq是减法指令,$8表示立即数8字节,%rsp是栈指针寄存器。
movq %rbp,(%rsp)
这条指令将 %rbp 寄存器的值(即当前函数的基址指针)移动到 %rsp 指向的内存地址中。%rbp 是基址指针寄存器,它通常用于访问当前函数的栈帧。(%rsp) 表示 %rsp 指向的内存地址。
图示:
前两栏给出的是,当%rsp 为 0xl08,%rax 为 0x123 时,执行指令 pushq %rax 的效果。首先%rsp 会减 8,得到 0x100,然后会将 0x123存放到内存地址 0x100 处。
弹出一个四字的操作包括从栈顶位置读出数据,然后将栈指针加8。
第三栏说明的是在执行完pushq后立即执行指令popq %rdx的效果。先从内 存中读出值0x123, 再写到寄存器%rdx 中,然后,寄存器%rsp的值将增加回到 0x108。