汇编语言--寄存器整理
写在最前面:学习汇编语言主要有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 寄存器
,作为累加器,特殊用途是在使用DIV
和MUL
指令时使用
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 | div reg |
MUL
在 8086 CPU 中是乘法指令,使用时应注意:
- 乘数,两个乘数要么都是8位,要么都是16位。如果是8位数的相乘,一个默认放在AL中,另一个放在内存字节单元或者其他寄存器中;如果是16位相乘,一个默认放在AX中,另一个放在内存字单元或者其他寄存器中。
- 乘积,8位数相乘结果默认保存在AX中,16位数相乘,默认运算结果有32位,高16位在DX中,低16位在AX中。
具体参见下例:
1 | mul reg |
BX:基地址寄存器
BX主要是用做内存寻址时候表示偏移地址,[…]表示一个内存单元,使用格式如下:
1 | mov 寄存器名,内存单元地址 |
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个系统地址寄存器中后两个LDTR
和TR
寄存器是16位的,其中存储的信息与CS等段寄存器一样为段选择子:用于在某表中根据索引查询到具体的段地址。
寄存器还有很多,比如:主存地址寄存器MAR
、主存数据寄存器MDR
、IO地址寄存器IOAR
、IO数据寄存器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个寄存器,现在要访问更多的寄存器自然就需要更多的编码,增加了指令长度,给存储带来负担。
所以,将大量的寄存器都设置为程序不可见寄存器是一件自然而然的事情,同时也是一件具有大智慧的事情。