历史发展

程序编码
linux命令中,gcc指的就是GCC编译器。因为这是 Linux 上默认的编译器,我们也可以简单地用 gcc来启动它。实际上 gcc 命令调用了一整套的程序,将源代码转化成可执行代码。首先,C 预处理 器扩展源代码,插人所有用include 命令指定的文件,并扩展所有声明指定的宏。
机器级代码
计算机系统是一个高度抽象的系统,使用更简单的抽线模型来颖仓复杂的细节。对于机器级别的编程而言,其中最为重要的两种抽象分别是:
- 由指令集体系结果或者指令集架构来定义机器级程序的格式和行为,它定义了处理器的状态、指令的格式,以及每条指令对状态的影响。
- 机器级程序使用的是虚拟地址,提供的内存模型看上去是一个非常大的字节数组。
整个编译的过程中,编译器会完成大部分的工作,把C语言提供的比较抽象的执行模型表示的程序转化为处理器执行的非常基本的指令。汇编代码表示非常接近域机器代码。
接下来以x86-64处理器为里,介绍汇编语言:
- 程序计数器(通常称为PC,在x86-64中使用%rip表示):给出将要执行的下一条指令在内存中的地址。
- 整数寄存文件:包含16个命名的位置,分别存储64为的值。这些寄存器可以存储地址或者整数数据。有的寄存器被用来记录某些重要的程序状态,其他的被用来保存临时数据,例如过程的参数和局部变量以及函数的返回值。
- 条件码寄存器:保存着最近执行的算数或者逻辑指令的状态信息。。它们用来实现控制或 数据流中的条件变化,比如说用来实现 if 和 while 语句。
- 一组向量寄存器可以存放一个或多个整数或浮点数值。
需要指出的是机器代码将内存看作一个极大的数组,其不区分有符号和无符号整数,不区分各种指针,甚至不区分指针和整数。
程序内存包含:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调 用和返回的运行时栈,以及用户分配的内存块(比如说用 malloc 库函数分配的)。 正如前 面提到的,程序内存用虚拟地址来寻址。
代码示例
假设我们写了一个C语言文件,包含如下的函数定义:
1 | long mult(long, long); |
经过gcc编译之后产生如下的汇编代码,其中每一条均对应于一条机器指令。

事实上,机器执行的程序只是一个字节序列,它是对一系列指令的编码。机器对产生这些指令的源代码几乎一无所知。要查看机器代码文件的内容,有一类称为反汇编器(disassembler)的程序非常有用。 这些程序根据机器代码产生一种类似于汇编代码的格式。在 Linux 系统中,带‘-d’命令行 标志的程序 OBJDUMP
我们在此介绍几个重要的反汇编特性:
- x86-64的指令长度从1到15个字节不等。常用的指令以及操作数较少的指令所需的字节数少,而那些不常用,且操作数较多的指令所需要的字节数较多。
- 设计指令格式的,从某个给定位置开始,可以将字节唯一地解码成机器指 令。例如,只有指令 pushq%rbx是以字节值 53 开头的。
- 反汇编器使用的是指令命名规则于GCC生成的汇编代码使用有些席位的差别。
格式的注解
GCC产生的汇编代码对于我们而言阅读上有点困难。所有以”.”开头的行都是知道汇编器和连接器工作的伪指令。我们通常可以将其忽略。

更清楚的解释如下:

数据格式
由于是从16位体系结构拓展为32位的,Intel用术语“字(word)”表示16位数据类型。因此,称32位数位“双字(double words)”,64位位“四字”。

浮点数主要有两种形式:单精度(4 字节)值,对应于 C 语言数据类型 float; 双精度 (8 字节)值,对应于 C 语言数据类型 doubleÿ x86 家族的微处理器历史上实现过对一种特 殊的 80 位(10 字节)浮点格式进行全套的浮点运算(参见家庭作业 2.86)。可以在 C 程序中 用声明 long double 来指定这种格式。不过我们不建议使用这种格式。它不能移植到其他 类型的机器上,而且实现的硬件也不如单精度和双精度算术运算的高效。
大多数 GCC 生成的汇编代码指令都有一个字符的后缀:
- movb:传送字节
- movw:传送字
- movl:传送双字
- movq:传送四字
访问信息
一个 X86-64 的中央处理单元(CPU)包含一组 16 个存储 64 位值的通用目的寄存器。 这些寄存器用来存储整数数据和指针。

上图中16个寄存器,它们的名字均以%r开头,不过后面还耕者一些不同命名规则的名字。指令可以对这 16 个寄存器的低位字节中存放的不同 大小的数据进行操作。字节级操作可以访问最低的字节,16 位操作可以访问最低的 2 个字 节,32 位操作可以访问最低的 4 个字节,而 64 位操作可以访问整个寄存器。
操作数指示符
大多数指令有一个或者多个操作数(operand),指示出执行一个操作中使用的源数据值,以及防止结果的目的位置。x86-64支持多种操作数格式,源数据常常以常数形式给出,或者是从寄存器中读出。结果可以存放在寄存器或者内存中。
由此,各种不同的操作数的可能性被分为三种类型:
- 立即数:用来表示常数值。在ATT格式的汇编代码中,立即数的书写方式是在$符号后面跟一个使用标准C表示的整数。比如$-577或$0x1F.不同的指令允许的立即数值范围不同,汇编器会自动选择最为紧凑的方法进行数值编码。
- 寄存器:它表示某个寄存器的内容,16个寄存器的低位1字节、2字节、4字节或者8字节中的一个作为操作数,这些字节数分别对应于8为、16位、32位和64位。,我们用符号 表示任 意寄存器 <2, 用引用 R[ra]来表示它的值,这是将寄存器集合看成一个数组 Rÿ 用寄存器标 识符作为索引。
- 内存引用:,它会根据计算出来的地址(通常称为有效地址)访问某个内 存位置。因为将内存看成一个很大的字节数组,我们用符号 表示对存储在内存 中从地 开始的6 个字节值的引用。为了简便,我们通常省去下标b。
事实上,计算机中存在多种不同的寻址模式,允许不同形式的内存应用,表中底部用语法$Imm(\r_{b},r_{i},s)$表示的是最常用的形式。这样的引用有四个部分:
- 立即数偏移Imm
- 基址寄存器$r_{b}$
- 变址寄存器$r_{i}$
- 比例因子s,这里的s必须是1,2,4,8

数据传输指令
最频繁使用的指令是将数据从一个位置复制到另外一个位置的指令。操作数表示的通用性使得一条简单的数据出阿松指令呢个够完成在许多机器中需要好几条不同指令才能完成的工作呢。
MOV类
MOV类指令有四条指令组成:movb\movw\movl和movq。这些指令都执行同样的操作;组要区别在于它们操作的数据大小不同,分别为1,2,4,8字节。如图:
源操作数指定的值是一个立即数,存储在寄存器中或者内存中。目的操作数指定一个 位置,要么是一个寄存器或者,要么是一个内存地址。。X86-64 加了一条限制,传送指令的 两个操作数不能都指向内存位置。将一个值从一个内存位置复制到另一个内存位置需要两 条指令—— 第一条指令将源值加载到寄存器中,第二条将该寄存器值写入目的位置。
源和目的操作数类型共有如下五种可能:

除此之外,还有:
