80386寄存器共有34个寄存器,可分为7类,它们是通用寄存器,指令指针和标志寄存器,段寄存器,系统地址寄存器,控制寄存器、调试和测试寄存器。我们经常碰到的是前四类寄存器,也是我这篇文章总结的重点。

通用寄存器

80386寄存器有8个32位通用寄存器这8个通用寄存器都是由8088/8086/80286的相应的16位通用寄存器扩展成32位而得。名字分别是:EAX,EBX,ECX,EDX,ESI,EDI,EBP,ESP。每个32位的通用寄存器的低16位可以单独使用,对应于8088/8086/80286的相应16位通用寄存器作用相同。同时,EAX,EBX,ECX,EDX四个寄存器的低16位AX,BX,CX,DX还可以继续分为各高8位,低8位寄存器单独使用,例如AX可以分为AH和AL,每个都是8位寄存器。

这8个为通用寄存器,说明它们的用处不止一个。它们通常既可以保存逻辑和算术运算中的那个操作数,也可以保存地址运算中的操作数。

下面说介绍一下在c程序和系统调用时,这些寄存器的用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include<stdio.h>
int sum(int x, int y)
{
  int accum = 0; 
  int t;
  t = x + y;
  accum += t;
  return accum;
}

int main( int argc, char **argv)
{
  int x = 1, y = 2;
  int result = sum( x, y );
  printf("\nresult = %d\n", result);
  return 0;
}

在函数调用和系统调用时,需要先将参数压入栈,然后被调用函数再从相应的寄存器获取参数值,存储在被调用函数的栈中,所以被调用函数对参数做出的改变并不会修改主函数的数值,因为他们在不同的栈中。函数的参数存入栈的顺序是从右到左,如果是调用函数sum(x,y),则是先将y压入栈,然后再将x压入栈。

对于寄存器而言,c程序和系统调用的参数则必须按顺序放到寄存器 ebx,ecx,edx,esi,edi 中,而函数调用的参数操作是从右到左,所以最后一个参数放入esi,倒数第二个参数存入edi等以此类推。对于上述的例子main函数调用sum(x,y),main函数将y存入esi,x存入edi。然后sum函数内部从esi和edi获取参数值。

而eax在函数调用中,经常用来存储函数的返回值。上述例子中,sum函数将值存入eax寄存器中,main函数再从eax寄存器获取返回值。对于fork函数返回两个值,也可以通过eax来理解,当内核调度父进程时,键pid存入eax寄存器,当内核调度子进程时,将0存入eax寄存器。

esp寄存器为栈顶寄存器,并且始终指向栈顶。

ebp寄存器为基址寄存器,其实就是每个函数栈的栈底寄存器,通过这个基址寄存器,再加上偏移量,即可获取参数以及局部变量的值。

指令指针寄存器和标志寄存器

EIP为指令指针寄存器,是32位寄存器,低16位称为IP,用于兼容16为CPU,其内容是下一条要取入CPU的指令在内存中的偏移地址。当程序刚运行时,系统把EIP清零,每取入一条指令,EIP自动增加相应的字节数,指向下一条指令。

EFLAGS也是为标志寄存器,低16位称为FLAGS,与16位CPU的标志寄存器同名,同作用。可分为3类:状态标志,控制标志和系统标志,简述如下:

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—系统标

段寄存器

可能在刚接触汇编时,会觉得段寄存器存储的就是程序每个段的基地址,在了解了linux虚拟地址转为物理地址之后,我才知道原来段描述符存储的并不是段基地址,而是存储了在段描述符表的索引以及一些属性。

80386有6个段寄存器,分别是CS,DS,SS,ES,FS和GS,是16位寄存器。前4个段寄存器的名称与8088/8086相同,在实地址方式下使用方式也和8088/8086相同。80386又增加了FS与GS,主要为了减轻对DS段和ES段的压力。

这些16位段寄存器存放的并不是段基地址,而是存储了在段描述符表的索引,D3-D15位是索引值,D0-D1位是优先级(RPL)用于特权检查,D2位是描述符表引用指示位TI,TI=0指示从全局描述表GDT中读取描述符,TI=1指示从局部描述符中LDT中读取描述符。这些信息总称段选择器(段选择子).

系统地址寄存器

这些寄存器是我写这篇文章最主要的原因,因为这些寄存器在汇编代码中很少见,而在理解地址转换又是那么重要。

系统地址寄存器有四个,用来存储操作系统需要的保护信息和地址转换表信息、定义目前正在执行任务的环境、地址空间和中断向量空间。

  • GDTR 48位全局描述符表寄存器,用于保存全局描述符表的32位基地址和全局描述符表的16位界限(全局描述符表最大为 $2^{16}$ 字节,共$2^{16}/8 = 8K$个全局描述符)。GDT表里面的每一项都表明一个段的信息,或者是一个LDT表的相关信息。其实一个LDT表也是一个段。所以也可以说GDT表的每一项都描述一个段。就像一个文件夹下面可以有文件,也可以有文件夹一样,GDT表里面既可以有段描述符,也可以有LDT的表。

  • IDTR 48位中断描述符表寄存器,用于保存中断描述符表的32位基地址和中断描述符表的16位界限(中断描述符表最大为 $2^{16}$ 字节,共$2^{16}/8 = 8K$个中断描述符)。

  • LDTR 16位局部描述符表寄存器,用于保存局部描述符表的选择符。一旦16位的选择符(也叫选择子)放入LDTR,CPU会自动将选择符所指定的局部描述符装入64位的局部描述符寄存器中。

  • TR 16位任务状态段寄存器,用于保存任务状态段(TSS)的16位选择符。与LDTR类似,一旦16位的选择符放入TR,CPU会自动将该选择符所指定的任务描述符装入64位的任务描述符寄存器中。 注:TSS是一个段,所以在GDT中有对应的表项描述。

所以LDTR和TR寄存器分别是局部描述符表的选择符和任务状态段选择符,它们需要到GDT表格中找到各自的描述符才能定位到各自的表格,所以可以把这两个寄存器看作是和cs,ds等段寄存器一样,局部描述符表和任务状态段看作是一个段。