本文转载自x86汇编语法基础(gnu格式),中间针对自己的理解略有修改或注释。在此处做一个备份,一方面为了加深对x86汇编语言的理解,另一方面也方便自己的后续查找,防止文章丢失。

1. 寄存器

1.1 通用寄存器

一个x86-64的中央处理单元(CPU)包含一组16个存储64位值的通用寄存器。这些寄存器用来存储整数数据和指针。下图显示了这16个寄存器。它们的名字都以%r开头,不过后面还跟着不同命名规则的名字,这是由于指令集历史演化造成的。最初的8086中有8个16位的寄存器,即图中的%ax%bp。每个寄存器都有特殊的用途,它们的名字就反映了这些不同的用途。扩展到IA32架构时,这些寄存器也扩展成32位寄存器,标号从%eax%ebp。扩招到x86-64后,原来的8个寄存器扩展成64位,标号从%rax%rbp。除此之外,还增加了8个新的寄存器,它们的标号是按照新的命名规则制定的:从%r8%r15

通用寄存器

如上图中嵌套的方框表明的,指令可以对这16个寄存器的低位字节中存放的不同大小的数据进行操作。字节级操作可以访问最低的字节,16位操作可以访问最低的2个字节,32位操作可以访问最低的4个字节,而64位操作可以访问整个寄存器。

Tips:当指令以寄存器作为目标时,对于生成小于8字节结果的指令,寄存器中剩下的字节如何处理,有两条规则

  • 生成1字节和2字节数字的指令会保持剩下的字节不变

  • 生成4字节的指令会把高位4字节置为0。

后面这条规则是作为从IA32到x86-64的扩展的一部分而采用的。

1.2 标志寄存器EFLAFS

EFLAGS标志寄存器包含有状态标志位、控制标志位以及系统标志位,处理器在初始化时将EFLAGS标志寄存器赋值为00000002H。

下图描绘了EFLAGS标志寄存器各位的功能,其中的第1、3、5、15以及22~31位保留未使用。由于64位模式不再支持VM和NT标志位,所以处理器不应该再置位这两个标志位。

标志寄存器EFLAFS

TIPs:在64位模式中,EFLAGS标志寄存器已从32位扩展为64位,被称作RFLAGS寄存器。其中高32位保留未使用,低32位与EFLAGS相同。

接下来,我们会根据标志位功能将EFLAGS划分位状态标志、方向标志、系统标志和IOPL区域等几部分,并对各部分的标志位功能进行逐一讲解。(请参考Intel官方白皮书Volumn 1的3.4.3节)。

1.2.1 状态标志

EFLAGS标志寄存器的状态标志(位0、2、4、6、7和11)可以反映出汇编指令计算结果的状态,像add、sub、mul、div等汇编指令计算结果的奇偶性、溢出状态、正负值皆可从上述状态找那个反映出来。

下表是这些状态标志的功能描述:

缩写 全称       名称 位置 描述
CF Carry Flag 进位标志 0 运算中,当数值的最高位产生了进位或者借位,CF位都会置1,否则为0。它可用于检测无符号整数运算结果是否溢出。也可用于多精度运算中
PF Parity Flag 奇偶标志 2 用于标记结果低8位中1的个数,如果为偶数, PF位为1,否则为0 。注意,是最低的那8位,不管操作数是16位,还是32位。奇偶校验经常用于数据传输开始时和结束后的对比,判断传输过程中是否出现错误。
AF Auxiliary Carry Flag 辅助进位标志 4 辅助进位标志,用来记录运算结果低4位的进、借位情况,即若低半字节有进、借位,AF为1,否则为0。
ZF Zero Flag 零值标志 6 若计算结果为0,此标志位置1,否则为0。
SF Sign Flag 符号标志 7 若运算结果为负,则SF位为1,否则为0。
OF Overflow Flag 溢出标志 11 用来标识计算的结果是否超过了数据类型可以表示的范围,若OF为1,表示有溢出,为0则未溢出。专门用于检测有符号整数运算结果是否溢出。

这些标志位可反映出三种数据类型的计算结果:无符号整数、有符号整数和BCD整数(Binary-coded decimal integers)。其中CF标志位可反映出无符号整数运算结果的溢出状态;OF标志位可反映出有符号整数(补码表示)运算结果的溢出状态;AF标志位表示BCD整数运算结果的溢出状态;SF标志位反应出有符号整数运算结果的正负值;ZF标志位反映出有符号或无符号整数运算的结果是否为0。

以上这些标志位,只有CF标志位可通过stc、clc和cmc(Complement Carry Flag,计算原CF位的补码)汇编指令更改位值。它也可借助位操作指令(bt、bts、btr和btc指令)将指定位值复制到CF标志位。而且,CF标志位还可在多倍精度整数计算时,结合adc指令(含进位的加法计算)或sbb指令(含借位的减减法)将进位计算或借位计算扩展到下次计算中。

至于状态跳转指令Jcc、状态字节置位指令SETcc、状态循环指令LOOPcc以及状态移动指令CMOVcc,它们可将一个或多个状态标志位作为判断条件,进程分支跳转、字节置位以及循环计数。

1.2.2 方向标志

DF方向标志位(Direction Flag)位于EFLAGS标志寄存器的第 10 位,它控制着字符串指令(诸如movs、cmps、scas、lods、stos等)的操作方向。置位DF标志位可使字符串指令按从高到低的地址方向(自减)操作数据,复位DF标志位可使字符串指令按从低到高的地址方向(自增)操作数据。汇编指令std和cld可用于置位和复位DF方向标志。

1.2.3 系统标志和IOPL区域

  • 第 8 位为TF位,即Trap Flag,意为陷阱标志位。此位若为1,用于让CPU进入单步运行方式,若为0,则为连续工作的方式。平时我们用的debug程序,在单步调试时,原理上就是让TF位为1。

  • 第 9 位为IF位,即Interrupt Flag,意为中断标志位。若IF位为1,表示中断开启,CPU可响应外部可屏蔽中断。若为0,表示中断关闭,CPU不再响应来自CPU外部的可屏蔽中断,但CPU内部的异常还是要响应的。

  • 第 12~13 位为IOPL,即 Input Output Privilege Level,这用在有特权级概念的CPU中。有4个任务特权级,即特权级0~3,故IOPL要占用2位来表示这4种特权级。

  • 第 14 位为NT,即 Nest Task,意为任务嵌套标志位。8088支持多任务,一个任务就是一个进程。当一个任务中又嵌套调用了另一个任务时,此NT位为1,否则为0。

  • 第 16 位为RF位,即 Resume Flag,意为恢复标志位。该标志位用于程序调试,指示是否接受调试故障,它需要与调试寄存器一起使用。当RF为1时忽略调试故障,为0时接受。

  • 第 17 位为VM位,即 Virtual 8086 Model,意为虚拟8086模式。

  • 第 18 位为AC位,即 Alignment Check / Access Control,意为对齐检查。若AC位为1时,则进行地址对齐检查,位0时不检查。

  • 第 19 位为VIF位,即 Virtual Interrupt Flag,意为虚拟终端标志位,虚拟模式下的中断标志。

  • 第 20 位为VIP位,即 Virtual Interrupt Pending Flag,意为虚拟中断挂起标志位。在多任务情况下,为操作系统提供的虚拟中断挂起信息,需要与 VIF 位配合。

  • 第 21 位为ID位,即 Identification Flag,意为识别标志位。系统经常要判断CPU型号,若ID位为1,表示当前CPU支持CPUID指令,这样便能获取CPU的型号、厂商信息等;若ID位为0,则表示当前CPU不支持CPUID指令。

1.3 段寄存器

x86-64架构,拥有6个16位段寄存器(CS、DS、SS、ES、FS和GS),用于保存16位段选择子。段选择子是一种特殊的指针,用于标识内存中的段。要访问内存中的特定段,该段的段选择子必须存在于相应的段寄存器中。

在平坦内存模型中,段选择子指向线性地址空间的地址0;在分段内存模型中,每个分段寄存器通常加载有不同的段选择子,以便每个分段寄存器指向线性地址空间内的不同分段。

每一个段寄存器表示三种存储类型之一:代码,数据,栈。

  • CS寄存器保存了代码段(code segment)选择子。代码段存储的是需要执行的指令,处理器使用CS寄存器内的代码段选择子和RIP/EIP寄存器的内容生成的线性地址来从代码段查询指令。RIP/EIP寄存器存储的是下一条要执行的指令在代码段上的偏移。

  • DS、ES、FS和GS寄存器指向了四个数据段(data segment)。

  • SS寄存器包含栈段(stack segment)的段选择子,其中存储当前正在执行的程序、任务或处理程序的过程堆栈。

1.4 控制寄存器

目前,Intel处理器共拥有6个控制寄存器(CR0、CR1、CR2、CR3、CR4、CR8),它们有若干个标志位组成,通过这些标志位可以控制处理器的运行模式、开启扩展特性以及记录异常状态等功能。

1.5 指令指针寄存器

RIP/EIP寄存器,即指令指针寄存器,有时称为程序计数器。指令指针(RIP/EIP)寄存器包含当前代码段中要执行的下一条指令的偏移量。

1.6 MSR寄存器组

MSR(Model-Specific Register)寄存器组可提供性能监测、运行轨迹跟踪与调试以及其它处理器功能。在使用MSR寄存器组之前,我们应该通过CPUID.01h:EAX[5]来检测处理器是否支持MSR寄存器组。处理器可以使用RDMSR和WRMSR对MSR寄存器组进行访问,整个访问过程借助ECX寄存器索引寄存器地址,再由EDX:EAX组成的64位寄存器保持访问值。(在处理器支持64位模式下,RCX、RAX和RDX寄存器的高32位将会被忽略)。

2. 指令集

2.1 操作数和指令后缀

2.1.1 操作数

x86指令可以有0到3个操作数,多个操作数之间以逗号(“,”)分隔。对于有两个操作数的指令,第一个是源操作数,第二个是目的操作数,指令的执行结果保存到第二个操作数表示的寄存器或内存地址中。

源数据值可以以常数形式给出,或是从寄存器或内存中读取。结果可以存放在寄存器或内存中。因此,操作数被分为三种类型:

  • 立即数(immediate): 用来表示常数值。立即数通过在整数前加一个$来表示。比如,$-577$0x1F;

  • 寄存器(register),它表示某个寄存器的内容,通过在寄存器名声前加上%来表示。比如:%eax%al

  • 内存引用: 它会根据计算出来的地址(通常称为有效地址)访问某个内存位置。内存引用的语法:segment:offset(base, index, scale)。

    • segment 可以是 x86 架构的任意段寄存器。segment 是可选的,如果指定的话,后面要跟上冒号(”:”)来与offset隔离开;如果未指定指定的话,默认为数据段寄存器–%ds。

    • offset 是一个立即数偏移量,是可选的。

    • base表示基址寄存器,可以是16个通用寄存器中的任意一个。

    • index表示变址寄存器,可以是16个通用寄存器中的任意一个。

    • scale表示比例因子,scale会与index相乘再加上base来表示内存地址。比例因子必须是1、2、4、或者8,若果比例因子未指定,默认为1。

有效地址被计算为:D[segment]+offset+R[base]+R[index]*scale,其中D[]表示对应段寄存器的数据,R[]表示通用寄存器里的数据。我们用M[addr]来表示内存地址addr。

内存地址操作示例:

     指令                                       说明
-----------------------------------------------------------------------------------------
movl var, %eax	                把内存地址M[var]处的数据传送到eax寄存器
movl %cs:var, %eax	            把代码段偏移量为var处的内存数据传送到eax寄存器
movl $var, %eax	                把立即数var传送到eax寄存器
movl var(%esi), %eax	        把内存地址 M[R[%esi] + var] 处的数据传送到eax寄存器
movl (%ebx, %esi, 4), %eax      把内存地址 M[R[%ebx] + R[%esi]*4] 处的数据传送到eax寄存器
movl var(%ebx, %esi, 4), %eax	把内存地址 M[var+R[%ebx] + R[%esi]*4] 处的数据传送到eax寄存器



[参看]

  1. x86汇编语法基础(gnu格式)

  2. linux下汇编语言开发