写在最前面:学习汇编语言主要有3个原因,一是操作系统学习与汇编语言关系密切,利于解读操作系统源码;二是对于逆向工程来说,汇编语言也是必须学习的;三是对于二进制安全方面的内容,汇编也是核心部分。所以在此单开一个专题用于记录汇编语言的学习,不会从头开始按部就班学习,而是会根据其他知识学习进度选择性学习与整理。

寄存器整理

什么是寄存器?

寄存器是CPU的一个重要组成部分,主要用于信息的短期存储

因为RAM与CPU之间需要总线连接进行数据传输,虽然输出速度很快,但是在CPU超高的频率下,很小的延迟也会导致大问题。而由于寄存器就在CPU中,所以其数据传输基本不耗费时间,可以极大的提高效率。

同时,对于寻址计数等功能也需要专门的空间去存储数据,寄存器就是不二之选。

寄存器的分类

在8086CPU中共有14个寄存器,且均为16位。包括:AX、BX、CX、DX、SP、BP、DI、SI、IP、FLAG、CS、DS、SS、ES,这14个寄存器又可以分为通用寄存器、控制寄存器和段寄存器三类。

在 8086 CPU 中,通用寄存器有 8 个,分别是 AX,BX,CX,DX,SP, BP,SI,DI ,通用寄存器除了自身的专门用途外,还可以用来传送数据和暂存数据,所以才称它们为通用寄存器

而通用寄存器又可以分为三类:数据寄存器(AX、BX、CX、DX等)、指针寄存器(SP、BP)、变址寄存器(SI、DI)。


段寄存器包括CS、DS、SS、ES4个,一般用于存储段地址,具体介绍会在下文介绍。


控制寄存器包括IP和FLAG,这一类寄存器在cpu执行指令时起到控制作用,具体功能在下文专题介绍。

通用寄存器

通用寄存器是所有寄存器中数目最多的一类。其可以分为数据寄存器、指针寄存器和变址寄存器三类,下面具体介绍。

数据寄存器

数据寄存器包括:AX、BX、CX、DX 4个,这4个寄存器有各自负责不同功能

AX:累加寄存器

AX 寄存器,作为累加器,特殊用途是在使用DIVMUL指令时使用

DIV在 8086 CPU 中是除法指令,使用时应注意:

  • 除数,除数可以是8位或者是16位的,保存在一个寄存器或者内存单元中。
  • 被除数,默认放在AX中(或者AX和DX中):如果除数是8位,那么被除数是16位,放在AX中;如果除数是16位,那么被除数是32位,在DX中存放高16位,AX中存放低16位。
  • 商和余数,如果除数为8位,那么AL存放DIV操作的商,AH存放DIV操作的余数;如果除数为16位,那么AX存放DIV操作的商,DX存放DIV操作的余数。

也就是说AX和DX寄存器配合完成除法运算,具体可见下例:

1
2
3
4
5
6
7
8
9
div reg
div 内存单元

MOV DX,100H ;设置 32 位被除数的高 16 位为 100H
MOV AX,300H ;设置 32 位被除数的低 16 位为 300H
MOV BX,200H ;设置 16 位除数为 200H
DIV BX ;执行计算

根据运算过程可知,除数为16位,被除数是32位,而商和余数是16位,其中商存储在AX中,余数存储在DX中。

MUL在 8086 CPU 中是乘法指令,使用时应注意:

  • 乘数,两个乘数要么都是8位,要么都是16位。如果是8位数的相乘,一个默认放在AL中,另一个放在内存字节单元或者其他寄存器中;如果是16位相乘,一个默认放在AX中,另一个放在内存字单元或者其他寄存器中。
  • 乘积,8位数相乘结果默认保存在AX中,16位数相乘,默认运算结果有32位,高16位在DX中,低16位在AX中。

具体参见下例:

1
2
3
4
5
6
7
8
9
mul reg
mul 内存单元

MOV AX,80H ;设置 16 位乘数为 80H
MOV BX,10H ;设置 16 位乘数为 10H
MOV DX,0H ;清空用来保存乘法结果的高 16
MUL BX ;执行计算

通过上述乘法过程可知,一个乘数保存在AX,另一个保存在BX,而执行运算是只需要告知BX即可。结果为32位,高位在DX,低位在AX。

BX:基地址寄存器

BX主要是用做内存寻址时候表示偏移地址,[…]表示一个内存单元,使用格式如下:

1
2
3
4
5
6
7
8
9
10
mov 寄存器名,内存单元地址
mov ax, [bx] ;默认情况下段地址[bx]的段地址在ds中
mov ax, ds:[bx]
mov ax, cs:[bx] ;可以显式地指明段地址
mov al, [bx]

mov 内存单元地址,寄存器名
mov [bx], ax
mov [bx], ah

CX (Count):计数器寄存器

CX 作为计数寄存器,在使用loop指令循环时用来指定循环次数的寄存器。而 CPU 在每一次执行 loop指令的时候,都会做两件事:一是令
CX = CX – 1,即令 CX 计数器自动减去 1;还有一件就是判断 CX 中的值,如果 CX 中的值为 0 则会跳出循环,而继续执行循环下面的指令。

DX (Data):数据寄存器

作为数据寄存器,特殊用途是在使用DIV和MUL指令时使用。详情见 AX (Accumulator):累加寄存器中的乘法和除法示例。

指针寄存器

包括栈指针寄存器SP和基指针寄存器BP,其用途主要是进行栈顶和栈底的界定。

SP (Stack Pointer):栈指针寄存器

SP 寄存器上必须和 SS 段寄存器一起使用,表示栈顶的偏移地址.

其中,SS用于存储栈段地址,而SP则存储偏移地址。而SS:SP即可指向栈顶地址

既然提到栈,就必然涉及到两个操作:入栈和出栈。而这两个操作的实现都是靠SP寄存器来进行的。具体如下:

  • 入栈:先进行栈顶的偏移,即SP=SP-2;再进行数据的导入,即将数据放到对应栈空间。
  • 出栈:先进行数据的移除,即将对应数据放到对应位置;再进行栈顶指针的偏移,即SP=SP+2.

至于为什么出栈是+2,入栈是-2:因为栈空间在虚拟内存的上方位置,其增长方向是从大地址到小地址的。

BP (Base Pointer):基指针寄存器

一般来说,BP指向函数调用的基地址,用于界定函数的开始区间。

若没有指定段地址,则以SS段寄存器为主,即SS:BP

:BP和SP寄存器在函数调用时发挥重要作用,同时函数的调用执行过程是一个比较复杂的过程,在理解这个过程时我们要始终记得:BP和SP寄存器只是一个容器,其中存储着指针地址。无论如何描述,是PUSH还是POP都是改变其中的值而已。

变址寄存器

包括SI(Source Index)源变址寄存器和DI(Destination Source)目的变址寄存器。

SI和DI寄存器和BX寄存器的功能类似,通过这两个寄存器可以完成寻址工作,当然也可以存储一般性数据。

段寄存器

段寄存器与偏移地址(BX、DI、SI、BP、SP、IP)共同构成一个内存空间的具体地址。

为什么需要段寄存器和偏移地址组合确定地址,而不直接使用一个寄存器存储内存地址?

:一个寄存器空间太小,无法存储全部的物理地址。

因为地址总线共20条,也就是说一个物理地址也有20位,寻址能力是1M(2的20次方)。

而一个寄存器的大小是16位,寻址能力是64K,无法表示全部的物理地址。

所以就使用段寄存器和偏移地址的方式合成一个20位的物理地址物理地址=段地址x16+偏移地址

其实物理地址的计算方法就是将段地址向左移一位,而后加上偏移地址即可。

CS (Code Segment):代码段寄存器

CS中保存代码段寄存器的段地址,通常和IP一起使用,利用CS:IP确定当前需要执行的指令的地址。代码段是我们自己定义的一段内存,只是我们自己编程时候的逻辑定义。

DS (Data Segment):数据段寄存器

DS是数据段寄存器,存放的是数据段的段地址,偏移地址通常由BX,SI,DI或者常数给出,DS:BX。数据段是我们自己定义的一段内存,只是我们自己编程时候的逻辑定义。

SS (Stack Segment):栈段寄存器

SS是栈段寄存器,存放的是栈段的段地址,偏移地址通常由SP,BP给出,SS:SP。栈段是我们自己定义的一段内存,只是我们自己编程时候的逻辑定义。

ES (Extra Segment):附加段寄存器

ES是用于定义一个段的段地址,使用和CS、DS、SS类似。

控制寄存器

包括IP(指令指针寄存器)和FLAG(标志寄存器)

IP (Instruction Pointer):指令指针寄存器

IP通常是和CS一起使用,CS:IP表示将要读取的指令的内存地址,CS表示代码段地址,IP是表示偏移地址。

FLAG标志寄存器

flag寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。

标志位的值一般来源于ALU(算术逻辑单元)。


第一次补充:在上述内容的基础上增加了控制与状态寄存器部分。

多个寄存器的介绍

寄存器可以大略分为两类:程序可见寄存器控制、状态寄存器

  • 程序可见寄存器多用于存储数据和地址信息,其目的是为了提高CPU的访问速度。常见的程序可见寄存器包括:通用寄存器(存储数据)、地址寄存器(段地址、栈指针、索引地址等)
  • 控制与状态寄存器是一个寄存器组,也就是很多个寄存器分别承担一部分控制、状态表示的功能,它们共同配合完成程序控制状态记录的功能。

有一点需要注意,无论操作系统是多少位,CS\ES\SS等段寄存器是16位,并且其中存储的不是段的基址而是段选择子,通过段选择子在段表里索引查询以获取到段基址


控制与状态寄存器用于记录操作系统在控制程序执行时程序的动态行为、结果等。这个寄存器组由:指令指针寄存器FLAG标志寄存器和4个(连号)控制寄存器组成。下面我们一一介绍

指令指针寄存器介绍

指令指针寄存器EIP中存放下一条将要执行指令的偏移量(offset),这个偏移量是相对于目前正在运行的代码段寄存器CS而言的。偏移量加上当前代码段的基地址,就形成了下一条指令的地址。EIP中的低16位可以分开来进行访问,给它起名叫指令指针IP寄存器,用于16位寻址。

也就是说CS:IP可以定位到内存中的一个具体地址(可能是虚拟地址,后续还要涉及到页表查询以映射到物理地址),其中存储的就是所要执行的指令代码。当然,计算机硬件MMU会自动完成寻址,无需复杂的操作。


标志寄存器介绍

标志寄存器EFLAGS存放有关处理器的控制标志,是使用单bit来进行表示标志信息,具体见下图:

这些标志位可以分为以下三类:状态标志控制标志系统标志:

  • AF——辅助进位标志。若该位置位时,表示最低有效的4位向高位产生了进位或借位,则该标志位主要用于BCD算术运算。
  • CF——进位标志。当该位置位,表示8位或16位或32位数的算术操作产生了进位或借位。进行多字节数的加、减时要使用该标志。循环移位指令也影响进位标志。
  • PF——奇偶标志。主要用于数据通讯应用程序中,当该位置位时,表示结果数据位中有偶数个1,可以检查数据传送中是否出现错误。
  • SF——符号标志。该位置时表示结果的最高位(符号位)为1。对于带符号数,该位为1表示负数,该位为0表示正数。
  • ZF——零标志。当该位置位时,表示操作的结果为0。
  • DF——方向标志。用于控制数据串操作指令中的地址变化方向。DF为0时,SI/DI或ESI/EDI为自动增量,地址从低向高变化,DF为1,SI/DI或ESI/EDI为自动减量,地址从高向低变化。
  • IF——中断允许标志。该位置1时允许响应外部可屏蔽中断(INTR),该位复位时禁止响应外部可屏蔽中断。IF不影响非屏蔽外部中断(NMI)或内部产生的中断。
  • OF——溢出标志。若该位置位表示此次运算发生了溢出,即作为带符号数运算,其结果值超出目的单位所能表示的数值范围。这时目的单位的内容对带符号数没有意义。
  • TF——陷阱标志。当该位置位时,把处理器置成供调试的单步方式。在这种方式中,每条指令执行后CPU自动产生一个内部中断,使调试者可以观察程序中该条指令执行的情况。
  • NT——嵌套任务标志。用来表示当前的任务是否嵌套在另一任务内,当该位置1时,表示当前的任务有一个有效的链连接到前一个任务(被嵌套),如果执行IRET指令,则转换到前一个任务。
  • IOPL——输入/输出特权级标志,用于定义允许执行输入/输出指令的I/O特权级的数值。
  • RF——恢复标志。它是与调试寄存器的断点一起使用的标志,当该位置1时,即使遇到断点或调试故障,也不产生异常中断1。在成功地执行每条指令时,RF将自动复位。
  • VM——虚拟8086方式标志。当该位置位时,CPU工作在虚拟8086模式(简称为拟86模式),在这种模式下运行8086的程序就好象是在8086CPU上运行一样。
  • AC——对准检查标志。这是80486新定义的标志位。该位置时,如果进行未对准的地址访问,则产生异常中断17。所谓未对准的地址访问,是指访问字数据时为奇地址,访问双字数据时不是4的倍数地址,访问8字节数据时,不是8的倍数的地址。对准检查在特权级为0,1,2时无效,只有在特权级3时有效。
  • s—状态标志;c—控制标志;x—系统标

12、13位IOPL:输入输出特权级位。其值与输入输出特权级0~3级相对应。但Linux内核只使用了两个级别,即0和3级,0表示内核级,3表示用户级

在当前任务的特权级CPL(Current_Privilege_Level)高于或等于输入输出特权级时,就可以执行像IN、OUT、INS、OUTS、STI、CLI和LOCK等指令而不会产生异常13(即保护异常)。
在当前任务特权级CPL为0时,POPF(从栈中弹出至标志位)指令中断返回指令IRET可以改变IOPL字段的值。

第9位IF(Interrupt Flag)中断标志位,是用来表示允许或者禁止外部中断(具体见下文中断一节

对于这些标志位的含义理解是很重要的,这些标志位在之后的学习中也会频繁出现,我们会在出现时深入介绍


4个控制寄存器介绍:

除了上面介绍的两个寄存器之外,还有4个控制寄存器CR0~4 ,其结构见下图

这几个寄存器中保存全局性和任务无关的机器状态。也就是说其值不会因为执行的进程不同而发生改变,而EFALGS寄存器、EIP寄存器等的值会动态变化以记录进程不同时间下的状态信息。

CR0中包含了6个预定义标志,我们这里需要重点记住其中两个0位保护允许位PE(ProtedtedEnable),用于启动保护模式,如果PE位置1,则保护模式启动,如果PE=0,则在实模式下运行。CR0的第31位分页允许位(PagingEnable),它表示芯片上的分页部件是否允许工作。

PG位PE位定义的操作方式如下图所示:

即通过这两个位的值来确定操作系统的操作模式,一般来说会在操作系统启动时使用实模式,而后在启动中途(具体到steup.s)时切换到保护模式。这两个模式的一个显著差别就是寻址方式的不同前者使用20位移位加和的方式、后者使用到段表寻址

CR1未定义的控制寄存器,供将来的处理器使用。

CR2页故障线性地址寄存器,保存最后一次出现页故障的全32位线性地址。

CR3页目录基址寄存器,保存页目录表的物理地址,页目录表总是放在以4K字节为单位的存储器边界上,因此,它的地址的低12位总为0,不起作用,即使写上内容,也不会被理会。(在Linux0.11中,页表被放在0地址处,也就是说cr3寄存器为0)

有关段页式内存管理的相关内容可以参考本专题内存管理文章,我们后面也会进行补充。


其他寄存器介绍

除了我们上面介绍的几个寄存器之外,还有很多其他功能的寄存器,下面我们选择一些常见的进行介绍:

  • GDTR:48位全局描述符表寄存器,用于保存全局描述符表的32位基地址和全局描述符表的16位界限(全局描述符表最大为 216216 字节,共216/8=8K216/8=8K个全局描述符)。GDT表里面的每一项都表明一个段的信息,或者是一个LDT表的相关信息。其实一个LDT表也是一个段。所以也可以说GDT表的每一项都描述一个段。就像一个文件夹下面可以有文件,也可以有文件夹一样,GDT表里面既可以有段描述符,也可以有LDT的表。
  • IDTR:48位中断描述符表寄存器,用于保存中断描述符表的32位基地址和中断描述符表的16位界限(中断描述符表最大为 216216 字节,共216/8=8K216/8=8K个中断描述符)。
  • LDTR:16位局部描述符表寄存器,用于保存局部描述符表的选择符。一旦16位的选择符(也叫选择子)放入LDTR,CPU会自动将选择符所指定的局部描述符装入64位的局部描述符寄存器中。
  • TR:16位任务状态段寄存器,用于保存任务状态段(TSS)的16位选择符。与LDTR类似,一旦16位的选择符放入TR,CPU会自动将该选择符所指定的任务描述符装入64位的任务描述符寄存器中。 注:TSS是一个段,所以在GDT中有对应的表项描述

上面介绍的4个寄存器是一类,叫做系统地址寄存器。其功能是用于存储操作系统需要的保护信息和地址转换表信息。关于GDT表和IDT表、LDT表的相关信息可以参考这一篇文章。需要注意的是,上文4个系统地址寄存器中后两个LDTRTR寄存器是16位的,其中存储的信息与CS等段寄存器一样为段选择子用于在某表中根据索引查询到具体的段地址


寄存器还有很多,比如:主存地址寄存器MAR主存数据寄存器MDRIO地址寄存器IOARIO数据寄存器IODR等,还有很多调试寄存器测试寄存器等,用途各不相同,后续使用到时再进行详细介绍。


第二次更新:2022年11月9日,午。增加了一些寄存器,并区分了程序员可见、透明分类。

更新原因:计组中在学习CPU时涉及到了更多的寄存器,所以加以补充。

注:上文已经较为详细的介绍了常见的寄存器,这里不再赘述。这里会从可见与透明的角度来分析这些寄存器。

可见与透明寄存器

  • 用户(所有程序员)可见:PSW、通用寄存器、PC
  • 用户(所有程序员)透明:MAR、MDR、IR、Cache、微程序的结构和功能
  • 应用程序员透明:暂存寄存器、虚拟存储器
  • 汇编程序员可见:PC
  • 系统程序员可见:虚拟存储器

程序可见寄存器

程序可见寄存器,顾名思义,就是程序员可以直接使用相关指令进行访问的寄存器。比如通用寄存器组,可以使用mov指令进行赋值,可以使用add指令进行计算,就是程序可见寄存器。

程序不可见寄存器

处理器中有大量的程序不可见寄存器。比如AR地址寄存器、DR数据寄存器、IR指令寄存器等,都是用于程序控制的,程序员不可见。(引:再比如使用Tomasulo算法的陆续执行处理器,需要用到大量的队列和表格,体现在硬件实现上就是一大堆寄存器。而这些寄存器都是程序不可见的寄存器。)

何以区分之

引用之:
作者:王宇轩
链接:https://www.zhihu.com/question/378773699/answer/1075791022
来源:知乎

第一,没有必要。大部分程序员学习编程,根本就不需要了解这些寄存器,甚至不知道有这些寄存器的存在。程序员只需要使用通用寄存器堆就可以完成一切编程任务。对于那些程序不可见寄存器,哪个寄存器里存放了什么内容,这些数据有什么用,它们将要被谁读出,程序员并不知道也并不关心。

第二,过于危险。程序员如果能够随心所欲地运用代码访问这些程序不可见寄存器,会出现严重的安全问题。因为这些寄存器都是直接关系到处理器的执行过程,对这些值进行修改,就好比是运动员去修理裁判,造成处理器执行过程的混乱。甚至是仅仅读取这些寄存器也很危险,如果黑客通过某种方式读取了这些寄存器的值,就能够获取一些隐私信息,著名的硬件漏洞,熔断和幽灵指的就是这种行为。

第三,影响性能。程序员想要访问这些程序不可见寄存器,指令集务必做出调整,要么增加指令种类,要么增加寄存器编码的位数。增加指令种类务必会使译码(decode)过程更加复杂,从而必须降低时钟周期,处理器效率受到影响。甚至操作码位数可能会不够,增加了指令长度。指令长度增加会使代码规模大幅增长,严重地增加了存储负担。增加寄存器编码的位数首先会增加访问寄存器的时长,降低时钟周期。而且本来只需要4位二进制编码就可以区分通用寄存器组中的所有16个寄存器,现在要访问更多的寄存器自然就需要更多的编码,增加了指令长度,给存储带来负担。

所以,将大量的寄存器都设置为程序不可见寄存器是一件自然而然的事情,同时也是一件具有大智慧的事情。