=[xw−1,xw−2,xw−3,...,x0]:B2Uw(x)≐i=0∑w−1xi2i样例:
B2U4([0001])=0×23+0×22+0×21+1×20=0+0+0+1=1
B2U4([0101])=0×23+1×22+0×21+1×20=0+4+0+1=5
B2U4([1011])=1×23+0×22+1×21+1×20=8+0+2+1=11
B2U4([1111])=1×23+1×22+1×21+1×20=8+4+2+1=15
而w能表示的值的范围,最小值用位向量[000.......000]表示,也就是整数0,
补码编码
对于许多应用,我们还希望表示负数值。最常见的有符号数的计算机表示方式就是补码形式。
对向量x=[xw−1,xw−2,xw−3,...,x0]:
B2Tw(x)≐−xw−12w−1+i=0∑w−2xi2i最高有效位xw−1也称为符号位,它的“权重”为−2w−1,是无符号表示中权重的负数。符号位为 1 时,表示为负数,而当符号位为 0 时,值为正数。
样例:
B2T4([0001])=−0×23+0×22+0×21+1×20=0+0+0+1=1
B2T4([0101])=−0×23+1×22+0×21+1×20=0+4+0+1=5
B2T4([1011])=−1×23+0×22+1×21+1×20=−8+0+2+1=−5
B2T4([1111])=−1×23+1×22+1×21+1×20=−8+4+2+1=−1
通用目的寄存器
一个 x86-64 的中央处理单元(CPU)包含一组 16 个存储 64 位值的通用目的寄存器。这些寄存器用来存储整数数据和指针。

- 黄色区域的是 8 位寄存器,其可以访问 1 个字节。
- 紫色区域的是 16 位寄存器,其可以访问 2 个字节,可以访问 8 位寄存器中的内容。
- 绿色区域的是 32 位寄存器,其可以访问 4 个字节,可以访问 8 位和 16 位寄存器中的内容。
- 蓝色区域的是 64 位寄存器,其可以访问 8 个字节,可以访问 8 位、16 位和 32 位寄存器中的内容。
对此有两条规则:
- 生成 1 字节和 2 字节数字的指令会保持剩下的字节不变。
- 生成 4 字节数字的指令会把高位 4 个字节置为 0。
规则 2 是作为从 IA32 到 x86-64 的扩展的一部分而采用的。
数据格式
由于是从 16 位体系结构扩展成 32 位的,Intel 用术语 “字(word)” 表示 16 位数据类型。因此,称 32 位数为“双字(double words)”,称 64 位数为“四字(quad words)”。标准 int 值存储为双字(32 位)。指针(在此用 char*表示)存储为 8 字节的四字,64 位机器本来就预期如此。x86-64 中,数据类型 long 实现为 64 位,允许表示的值范围较大。x86-64 指令集同样包括完整的针对字节、字和双字的指令。
数据类型 | Intel 数据类型 | 汇编后缀 | 大小 |
---|
char | 字节 | b | 1 |
short | 字 | w | 2 |
int | 双字 | l | 4 |
long | 四字 | q | 8 |
char* | 四字 | q | 8 |
float | 单精度 | s | 4 |
double | 双精度 | l | 8 |
操作数指令符
大多数指令有一个或多个操作数(operand),指示出执行一个操作中要使用的源数据值,以及放置结果的目的位置。源数据值可以以常数形式给出,或是从寄存器或内存中读出。结果可以存放在寄存器或内存中。
因此,各种不同的操作数的可能性被分为三种类型:
立即数(immediate)
,用来表示常数值。在ATT格式的汇编代码中,立即数的书写方式是 “$” 后面跟一个用标准 C 表示法表示的整数,比如,$-577 或 $0x1F。不同的指令允许的立即数值范围不同,汇编器会自动选择最紧凑的方式进行数值编码。寄存器(register)
,它表示某个寄存器的内容,16 个寄存器的低位 1 字节、2 字节、4 字节或 8 字节中的一个作为操作数,这些字节数分别对应于 8 位、16 位、32 位或 64 位。用符号ra,来表示任意寄存器 a,用引用R[ra]来表示它的值,这是将寄存器集合看成一个数组 R,用寄存器标识符作为索引。内存引用
,它会根据计算出来的地址(通常称为有效地址)访问某个内存位置。因为将内存看成一个很大的字节数组,我们用符号Mb[Addr]表示对存储在内存中从地址 Addr 开始的 b 个字节值的引用。为了简便,我们通常省去下标b。
其实有多种不同的寻址模式,允许不同形式的内存引用。表中底部用语法Imm(rb,ri,s)表示的是最常用的形式。这样的引用有四个组成部分:
- 立即数偏移Imm
- 基址寄存器rb
- 变址寄存器ri
- 比例因子 s
这里 s 必须是1、2、4 或者 8。基址和变址寄存器都必须是 64 位寄存器。有效地址被计算为Imm+R[rb]+R[ri]⋅s。引用数组元素时,会用到这种通用形式。其他形式都是这种通用形式的特殊情况,只是省略了某些部分。
类型 | 格式 | 操作数值 | 名称 |
---|
立即数 | Imm | Imm | 立即数寻址 |
寄存器 | ra | R[ra] | 寄存器寻址 |
存储器 | Imm | M[Imm] | 绝对寻址 |
存储器 | (ra) | M[R[ra]] | 间接寻址 |
存储器 | Imm(rb) | M[Imm+R[rb]] | (基址 + 偏移量)寻址 |
存储器 | (rb,ri) | M[R[rb]+R[ri]] | 变址寻址 |
存储器 | Imm(rb,ri) | M[Imm+R[rb]+R[ri]] | 变址寻址 |
存储器 | (,ri,s) | M[R[ri]⋅s] | 比例变址寻址 |
存储器 | Imm(,ri,s) | M[Imm+R[ri]⋅s] | 比例变址寻址 |
存储器 | (rb,ri,s) | M[R[rb]+R[ri]⋅s] | 比例变址寻址 |
存储器 | Imm(rb,ri,s) | M[Imm+R[rb]+R[ri]⋅s] | 比例变址寻址 |
数据传送指令
数据传送指令用来把数据、地址或者立即数传送到寄存器或存储单元中。
而数据传送指令又可分为4类:
- 通用数据传送指令
- 输入输出指令
- 地址传送指令
- 标志传送指令
除标志传送指令外,其他指令的执行对标志位均不产生影响。
标志位相关知识:[标志位详解][]
通用数据传送指令
通用数据传输指令又可分为 5 类:
- 一般数据传输指令
- 堆栈操作指令
- 交换指令
- 查表转换指令
- 字位扩展指令
这 5 类指令的执行,均不影响标志位。
一般数据传输指令 MOV
格式:MOV DEST,SRC
操作:src⟶dest
例如:MOV AL,BL
上述例子就是将 BL 中的数据传输至 AL 中,但是MOV指令有一下几点需要注意:
- 两操作数字长必须相同
- 两操作数不允许同时为存储器操作数
- 两操作数不允许同时为段寄存器
- 在源操作数是立即数时,目标操作数不能是段寄存器
- IP和CS不作为目标操作数,FLAGS一般也不作为操作数在指令中出现
CS:指令段地址
IP:当前指令地址
CS + IP:当前指令的绝对地址
MOV指令有分以下几种:
MOV指令 | 作用 | MOV种类 |
---|
MOVB | 完成 1 个字节的复制 | 普通的MOV指令 |
MOVW | 完成 2 个字节的复制 | 普通的MOV指令 |
MOVL | 完成 4 个字节的复制 | 普通的MOV指令 |
MOVQ | 完成 8 个字节的复制 | 普通的MOV指令 |
MOVSBW | 做符号扩展的 1 字节复制到 2 字节 | 做符号扩展的MOVS指令 |
MOVSBL | 做符号扩展的 1 字节复制到 4 字节 | 做符号扩展的MOVS指令 |
MOVSBQ | 做符号扩展的 1 字节复制到 8 字节 | 做符号扩展的MOVS指令 |
MOVSWL | 做符号扩展的 2 字节复制到 4 字节 | 做符号扩展的MOVS指令 |
MOVSWQ | 做符号扩展的 2 字节复制到 8 字节 | 做符号扩展的MOVS指令 |
MOVSLQ | 做符号扩展的 4 字节复制到 8 字节 | 做符号扩展的MOVS指令 |
MOVZBW | 做零扩展的 1 字节复制到 2 字节 | 做零扩展的MOVZ指令 |
MOVZBL | 做零扩展的 1 字节复制到 4 字节 | 做零扩展的MOVZ指令 |
MOVZBQ | 做零扩展的 1 字节复制到 8 字节 | 做零扩展的MOVZ指令 |
MOVZWL | 做零扩展的 2 字节复制到 4 字节 | 做零扩展的MOVZ指令 |
MOVZWQ | 做零扩展的 2 字节复制到 8 字节 | 做零扩展的MOVZ指令 |
MOVZLQ | 做零扩展的 4 字节复制到 8 字节 | 做零扩展的MOVZ指令 |
堆栈操作指令 PUSH POP
原则:先进后出,以字(两字节)为单位。
操作数可以是寄存器或者存储器两单元,不能是立即数,如果是存储器操作数必须声明字长,不能从栈顶弹出一个字给CS。
PUSH 指令
压栈:PUSH OPRD
PUSH指令有以下几种:
MOV指令 | 作用 |
---|
PUSHW | 将 2 个字节压入堆栈 |
PUSHL | 将 4 个字节压入堆栈 |
PUSHQ | 将 8 个字节压入堆栈 |
PUSHA | 将所有的 16 位通用寄存器压入堆栈,AX,CX,DX,BX,BP,SI,DI |
PUSHAD | 将所有的 32 位通用寄存器压入堆栈,EAX,ECX,EDX,EBX,EBP,ESI,EDI |
PUSHF | 将所有的 16 位标志寄存器EFLAGS压入堆栈 |
PUSHFD | 将所有的 32 位通用寄存器EFLAGS压入堆栈 |
POP 指令
弹栈:POP OPRD
POP指令有以下几种:
MOV指令 | 作用 |
---|
POPW | 将 2 个字节弹出堆栈 |
POPL | 将 4 个字节弹出堆栈 |
POPQ | 将 8 个字节弹出堆栈 |
POPA | 将所有的 16 位通用寄存器弹出堆栈,DI,SI,BP,BX,DX,CX,AX |
POPAD | 将所有的 32 位通用寄存器弹出堆栈,EDI,ESI,EBP,EBX,EDX,ECX,EAX |
POPF | 将所有的 16 位标志寄存器EFLAGS弹出堆栈 |
POPFD | 将所有的 32 位通用寄存器EFLAGS弹出堆栈 |
算术和逻辑操作
下图 x86-64 的一些整数和逻辑操作。大多数操作都分成了指令类,这些指令类有各种带不同大小操作数的变种(只有LEAQ没有其他大小的变种)。
例如,指令类ADD由四条加法指令组成: ADDB、ADDW、ADDL 和 ADDQ,分别是字节加法、字加法、双字加法和四字加法。
事实上,给出的每个指令类都有对这四种不同大小数据的指令。这些操作被分为四组:加载有效地址、一元操作、二元操作和移位。二元操作有两个操作数,而一元操作有一个操作数。
指令 | 效果 | 描述 |
---|
LEAQ | D⟵&S | 加载有效地址 |
INC | D⟵D+1 | 加 1 |
DEC | D⟵D−1 | 减 1 |
NEG | D⟵−D | 取负 |
NOT | D⟵∼D | 取补 |
ADD | D⟵D+S | 加 |
SUB | D⟵D−S | 减 |
IMUL | D⟵D∗S | 乘 |
XOR | D⟵D∧S | 异或 |
OR | D⟵D∣S | 或 |
AND | D⟵D&S | 与 |
SAL | D⟵D≪k | 左移 |
SHL | D⟵D≪k | 左移(等同于 SAL) |
SAR | D⟵D≫Ak | 算术右移 |
SHR | D⟵D≫Lk | 逻辑右移 |
整数算术操作。加载有效地址(LEAQ)指令通常用来执行简单的算术操作。其余的指令是更加标准的一元或二元操作。我们用≫A和≫L,来分别表示算术右移和逻辑右移。注意,这里的操作顺序与 ATT 格式的汇编代码中的相反。
加载有效地址
加载有效地址(Load Effective Address)指令 LEAQ 实际上是 MOVQ 指令的变形。它的指令形式是从内存读数据到寄存器,但实际上它根本就没有引用内存。它的第一个操作数看上去是一个内存引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数。编译器经常发现 LEAQ 的一些灵活用法,根本就与有效地址计算无关。目的操作数必须是一个寄存器。
LEAQ 指令能执行加法和有限形式的乘法,在编译像1+7∗3+12∗5−7这样的算术表达式时,是很有用处的。
一元和二元操作
INC、DEC、NEG、NOT 是一元操作,只有一个操作数,既是源操作数又是目的操作数。这个操作数可以是一个寄存器,也可以是一个内存位置。比如说,指令 INCQ (%rsp) 会使栈顶的 8 字节元素加 1。这种语法让人想起 C 语言中的加 1 运算符(++)和减 1 运算符(–)。
ADD、SUB、IMUL、XOR、OR、AND 是二元操作,其中,第二个操作数既是源操作数又是目的操作数。这种语法让人想起 C 语言中的赋值运算符,例如 x-=y、x+=y。不过,要注意,源操作数是第一个,目的操作数是第二个,对于不可交换操作来说,这看上去很奇特。例如,指令 subq %rax,%rdx 使寄存器 %rdx 的值减去 %rax 中的值。第一个操作数可以是立即数、寄存器或是内存位置。第二个操作数可以是寄存器或是内存位置。注意,当第二个操作数为内存地址时,处理器必须从内存读出值,执行操作,再把结果写回内存。
移位操作
SAL、SHL、SAR、SHR 是移位操作,先给出移位量,然后第二项给出的是要移位的数。可以进行算术和逻辑右移。移位量可以是一个立即数,或者放在单字节寄存器 %cl 中(这些指令很特别,因为只允许以这个特定的寄存器作为操作数)。原则上来说,1 个字节的移位量使得移位量的编码范围可以达到28−1=255。x86-64 中,移位操作对 w 位长的数据值进行操作,移位量是由 %cl 寄存器的低 m 位决定的,这里2m=w。高位会被忽略。所以,例如当寄存器 %cl 的十六进制值为 0xFF 时,指令 SALB 会移 7 位, SALW 会移 15 位,SALL 会移 31 位,而 SALQ 会移 63 位。
特殊的算术操作
两个 64 位有符号或无符号整数相乘得到的乘积需要 128 位来表示。x86-64 指令集对 128 位(16 字节)数的操作提供有限的支持。延续字(2 字节)、双字(4 字节)和四字(8 字节)的命名惯例,Intel 把 16 字节的数称为八字(oct word)。下图描述的是支持产生两个 64 位数字的全 128 位乘积以及整数除法的指令。
指令 | 效果 | 描述 |
---|
IMULQ | R[%rdx]:R[%rax]⟵S×R[%rax] | 有符号全乘法 |
MULQ | R[%rdx]:R[%rax]⟵S×R[%rax] | 无符号全乘法 |
CLTO | R[%rdx]:R[%rax]⟵(R[%rax]) | 转换为八字 |
IDIVQ | R[%rdx]:R[%rdx]⟵R[%rax]modS R[%rdx]:R[%rdx]⟵R[%rax]÷S | 有符号除法 |
DIVQ | R[%rdx]:R[%rdx]⟵R[%rax]modS R[%rdx]:R[%rdx]⟵R[%rax]÷S | 无符号除法 |
特殊的算术操作。这些操作提供了有符号和无符号数的全 128 位乘法和除法。一对寄存器 %rdx 和 %rax 组成一个 128 位的八字。
控制
到目前为止,我们只考虑了直线代码的行为,也就是指令一条接着一条顺序地执行。C 语言中的某些结构,比如条件语句、循环语句和分支语句,要求有条件的执行,根据数据测试的结果来决定操作执行的顺序。机器代码提供两种基本的低级机制来实现有条件的行为 : 测试数据值,然后根据测试的结果来改变控制流或者数据流。
与数据相关的控制流是实现有条件行为的更一般和更常见的方法,所以我们先来介绍它。通常,C 语言中的语句和机器代码中的指令都是按照它们在程序中出现的次序,顺序执行的。用 jump 指令可以改变一组机器代码指令的执行顺序,jump 指令指定控制应该被传递到程序的某个其他部分,可能是依赖于某个测试的结果。编译器必须产生构建在这种低级机制基础之上的指令序列 , 来实现 C 语言的控制结构。
本文会先涉及实现条件操作的两种方式,然后描述表达循环和 switch 语句的方法。
本文是原创文章,采用CC BY-NC-SA 4.0协议,完整转载请注明来自喜结良袁