实模式/保护模式下的寻址

8086处理器是一个16位的处理器,它的数据总线是16位,而地址总线是20位的,最多可以寻址1MB的地址空间。之后的80286处理器也是16位,但是地址总线有24位,从80286开始CPU演变出两种工作模式:实模式和保护模式

x86寄存器

在x86架构中,16位的处理器与32位处理器所对应的寄存器是有所不同的。8086寄存器组就分为通用寄存器,专用寄存器和段寄存器三类总共15个,其中通用寄存器有AX、BX、CX、DX、SP、BP、DI及 SI,专用寄存器包括IP、SP 和FLAGS三个16位寄存器, 而段寄存器则有CS、DS、SS、ES,这些寄存器都是 16位的

8086与80386寄存器对比表

为支持1MB寻址空间,8086在实模式下引入了分段的方法。CPU中设置了四个段寄存器:CS、DS、SS和 ES,分别用于可执行代码段 、数据段以及堆栈段 。 每个段寄存器都是16位的,对应于地址总线中的高16位

段式内存管理

段式内存管理带来了优势:

  • 调试错误更容易定位
  • 程序的地址不再需要采用内存物理地址进行编码
  • 支持更大的内存地址

但是在该环境下,应用程序可以直接对系统的任意内存地址(包括操作系统所在的区域)进行操作

保护模式,这种模式下内存段的访问受到了限制。访问内存时不能直接从段寄存器中获得段的起始地址,而需要经过额外转换和检查。在保护模式下,段范围不再受限于64K,可以达到16MB(或者80386的4GB)。

实模式下段的管理

实模式采用16位寻址模式,在该模式中,最大寻址空间为1MB,最大分段为64KB。

8086处理器地址总线扩展到20位,但算术逻辑运算单元(ALU)宽度即数据总线却只有16位,也就是说直接参与运算的数值都是16位的。寻址时,采用以下公式计算实际访问的物理内存地址:实际物理地址 = (段寄存器 << 4) + 偏移地址这样,便实现了16位内存地址到20位物理地址的转换

80x86系列是使用CS寄存器配合IP寄存器的组合来通知CPU指令在内存中的位置。程序指令在执行过程中一般还需要有各种数据,80x86系列有DS、ES、FS、GS、SS等用于指示不同用途的数据段在内存中的位置。80x86系列使用中断机制来实现系统服务。总的来说,这些就是实模式一个程序运行所需的主要内容。

保护模式下段的管理

保护模式下的分段机制
利用段选择子到全局描述符表中找到需要的段描述符,段描述符中就存放着真正的段的物理首地址,再加上偏移地址量便得到了最后的物理地址

保护模式段式寻址
一般保护模式段式寻址可用 xxxx:yyyyyyyy表示。其中xxxx表示索引,也就是 段选择子,是16位的;yyyyyyyy是偏移量,是32位的。

到哪里去寻找全局描述符表呢?80386以及以后的处理器专门设计了一个寄存器GDTR(Global Descriptor Table Register),专门用于存储全局描述符表在内。GDTR寄存器有48位,其中有32位记录描述符表的物理地址,16位记录全局描述符表的长度(该表占据的物理内存字节数)
GDTR位分布图

再来看看段描述符,段描述符实际上是一个占据64位内存(8个字节)的结构体
段描述符的结构

一个64位的段描述符包含了段的物理首地址、段的界限以及段的属性。在描述符中,段基址占32位,段限长占20位,属性占12位

保护模式寻址实例

下面将通过一个例子来加深对保护模式寻址方式的理解。在这个程序开始执行的时候cs寄存器的值为0

.set PROT_MODE_CSEG, 0x8         # kernel code segment selector
.set PROT_MODE_DSEG, 0x10        # kernel data segment selector
.set CR0_PE_ON,      0x1         # protected mode enable flag
lgdt    gdtdesc

  movl    %cr0, %eax
  orl     $CR0_PE_ON, %eax
  movl    %eax, %cr0

  # Jump to next instruction, but in 32-bit code segment.
  # Switches processor into 32-bit mode.
  #  实模式跳转到保护模式
  ljmp    $PROT_MODE_CSEG, $protcseg

protcseg:
  movw    0x10, %ax
  movw    %ax,  %dx
  movl    0xf0000000, %ebx
  movl    0x20(%ebx), %eax

gdt:                                    #gdt表的内容
  SEG_NULL                                # null seg
  SEG(STA_X|STA_R, 0x0, 0xffffffff)        # code seg
  SEG(STA_W, 0x0, 0xffffffff)            # data seg

gdtdesc:
  .word   0x17                            # sizeof(gdt) - 1
  .long   gdt

以上这一小段程序展示了系统进入保护模式以及在保护模式中利用寄存器寻址的过程

首先lgdt指令将GDT表的地址和表长装入GDTR寄存器,在gdtdesc标识的地方存有一个字及一个双字,前者为0x17表示表的长度(字节数),后者是表的物理地址。

接着再将CR0的保护模式开启位打开,系统便进入了保护模式,开始采用保护模式的寻址模式进行地址的转换。这个时候,内存中有GDT的3个表项

进入保护模式后系统立即执行了一个长跳转指令,由于是在保护模式中,所以 PROT_MODE_CSEG被当作段选择子,而protcseg是偏移地址。段选择子的值是0x8,于是对应的段描述符会是表中的第一项,即是SEG(STA_X|STA_R, 0x0, 0xffffffff)这一项,0x0表示段首地址是0,所以最终得到的物理地址为0 + protcseg,程序便会跳到 protcseg 所标识的位置来执行。

之后执行指令: movl 0x20(%ebx), %eax, 如图所示可知段基址为0,于是物理地址是0+0x20+0xf000000=0xf000020,内存中这个位置的一个双字会被复制到eax寄存器中
保护模式寻址过程

这里能访问的到的0到4G的地址空间实际上是虚拟地址空间,在开启分页机制后,还要经过页表转换才能得到真实地址,而在开启分页之前系统一般会控制只访问低地址,这些问题到内存管理我们会进行更深入的讨论