linux深入解析分段机制

时间:2022-12-30 23:43:34

首先讨论下inux中为什么要有分段和分页机制   首先讲述linux系统上的实模式和保护模式是指什么。为什么要分实模式和保护模式呢?他们当中有什么区别呢?   首先实模式和保护模式是CPU的两种工作模式。一开始PC启动时CPU是工作在实模式下的,经过某种机制后,CPU跳转到保护模式。其访问空间扩大了,要想从保护模式返回到实模式就只能重启。 Intel 8086是16位CPU,它只有16位寄存器、16位数据总线和20位地址总线,它只能运行在实模式 在实模式, 物理地址=段值*16+偏移  段值和偏移都是16位的 具有1MB(2^16 * 2^4 + offset)的寻址能力。
 
而从80386开始CPU有32位地址线,所以寻址空间可以达到4GB。单从寻址这方面说,使用16位寄存器的方法已经不够用了,必须要开发一种新方法。在保护模式下,虽然CPU还是使用原来16位的cs、ds寄存器表示内存地址的段值,不过保护模式下的段值只是一个索引,这个索引指向一个数据结构的一个表项就是GDT(or LDT)。 在保护模式下,CPU有着巨大的寻址能力,并为强大的32位操作系统提供了更好的硬件保障。 所以实模式和保护模式其实就是随着CPU性能不断增强后为了前后能够兼容而设计出来的一套模式。是一个历史遗留产物。 其实CPU的工作模式还有一种虚拟X86模式虚拟X86可以与保护模式相互转换且从虚拟模式只能通过复位信号到实模式。 -------------------------------------- 为什么要分段? 分段的引入主要扩大了内存地址,程序的地址不再需要原始的硬编码,程序的调试也更简便。 当x86 CPU 工作在保护模式时,可以使用全部32根地址线访问4GB的内存,因为80386的所有通用寄存器都是32位的,所以用任何一个通用寄存器来间接寻址,不用分段就可以访问4G空间中任意的内存地址。但这并不意味着,此时段寄存器就不再有用了。实际上,段寄存器更加有用了,虽然再寻址上没有分段的限制了,但在保护模式下,一个地址空间是否可以被写入,可以被多少优先级的代码写入,是不是允许执行等等涉及保护的问题就出来了。要解决这些问题,必须对一个地址空间定义一些安全上的属性。   段寄存器这时就派上了用场。但是设计属性和保护模式下段的参数,要表示的信息太多了,要用64位长的数据才能表示。我们把着64位的属性数据叫做段描述符,上面说过,它包含3个变量:段物理首地址、段界限、段属性 80386的段寄存器是16位(注意:通用寄存器在保护模式下都是32位,但段寄存器没有被改变)的,无法放下保护模式下64位的段描述符。

如何解决这个问题呢?方法是把所有段的段描述符顺序存放在内存中的指定位置,组成一个段描述符表(Descriptor Table);而段寄存器中的16位用来做索引信息,这时,段寄存器中的信息不再是段地址了,而是段选择子(Selector)。可以通过它在段描述符表中“选择”一个项目已得到段的全部信息。那么段描述符表存放在哪里呢?80386引入了两个新的寄存器来管理段描述符,就是GDT和LDT。这样,用以下几步来总体体验下保护模式下寻址的机制
  1、段寄存器中存放段选择子Selector  2、GDT中存放着段描述符表的首地址  3、通过选择子根据GDT中的首地址,就能找到对应的段描述符  4、段描述符中有段的物理首地址,就得到段在内存中的首地址  5、加上偏移量,就找到在这个段中存放的数据的线性地址(只有分段机制情况下,就是真实物理地址)。    接上面所述,GDT的作用就是用来提供段式存储机制,这种机制是通过段寄存器和GDT中的描述符共同提供的。 因为要能够访问4GB内存,所以在32位模式下 物理地址不能等于 段值*16+偏移 (这种方法寻址1MB) 于是,设计者让 段值*16指向一个数据结构的一个表项就是GDT(or LDT),表项中详细定义了段的起始地址,界限和属性等内容  于是原先“段:偏移”形式的逻辑地址经过段机制转化成线性地址 如果只有分段机制,那么这个地址就是对应的真实内存的物理地址。 -------------------------------------- 为什么要分页? 其实它的主要目的在于实现虚拟存储器。   接上面所述,刚刚说到线性地址,并且说如果只有分段机制,线性地址就是物理地址 分段和分页是通过内存控制单元(MMU)进行的。分段使得逻辑地址转换为线性地址,而分页使得线性地址转换为物理地址。 在分页中,为了效率起见,将线性地址分成固定长度,称为页,与页长度一致的是页框(物理页)。每个页框包含一个页。 把线性地址映射到物理地址的数据结构称为页表。页表存放在主存之中,并在启用分页之前由内核对其进行初始化。   另外,如果你写完一个程序后,复制了一份,然后同时调试。你会发现,从变量地址到寄存器的值,这两个程序几乎全部是都是一样的,而这些一样的地址之间完全不会被混淆,而是各自完成自己的职责,这也是分页机制的功劳。 另外,因为分页机制的存在,程序使用的都是线性地址空间,而不再直接是物理地址。这好像是操作系统位应用程序提供了一个不依赖于硬件(物理内存)的平台,应用程序不必关心实际上有多少物理内存,也不必关心正在使用的是哪一段内存,甚至不必关心某一个地址是在物理内存里面还是在硬盘中。只要像操作系统申请就行,而操作系统全权负责了这其中的转换工作。

接下来对分段机制中必须用到的一些名词作一个解释:

段选择符:又称端选择子,是段的一个16位标识符。它并不直接指向段,而是指向段选择符表中定义段的段描述符。它有三个字段内容:请求特权级RPL(Request Privilege Level)、表指示标志TI(Table Index)、索引值(Index)

linux深入解析分段机制

段描述符:段描述符石GDT和LDT表中的一个数据结构项,用来向处理器提供一个有关段的位置和大小信息以及访问控制的状态信息。包含三个主要字段:段基地址、段限长、和段属性。段描述符通常由编译器、连接器、加载器或者操作系统来创建。

linux深入解析分段机制

段描述符表: 是段描述符的一个数组。

linux深入解析分段机制

接下来看一下适用分段机制将逻辑地址转化为线性地址的步骤:

1)使用段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符.(仅当一个新的段选择符加载到段寄存器中是才需要这一步)

2)利用段选择符检验段的访问权限和范围,以确保该段可访问。

3)把段描述符中取到的段基地址加到偏移量上,最后形成一个线性地址。

从《一个操作系统的实现》的源码来理解分段机制:

[cpp] view plain copy
  1. org 07c00h                                
  2.     jmp LABEL_BEGIN  
  3.   
  4. [SECTION .gdt]  
  5. ; GDT  
  6. ;                              段基址,       段界限     , 属性  
  7. LABEL_GDT:     Descriptor       0,                0, 0           ; 空描述符  
  8. LABEL_DESC_CODE32: Descriptor       0, SegCode32Len - 1, DA_C + DA_32; 非一致代码段  
  9. LABEL_DESC_LDT:    Descriptor       0,        LDTLen - 1, DA_LDT    ; LDT  
  10. LABEL_DESC_VIDEO:  Descriptor 0B8000h,           0ffffh, DA_DRW      ; 显存首地址  
  11. ; GDT 结束  
  12.   
  13. GdtLen      equ $ - LABEL_GDT   ; GDT长度  
  14. GdtPtr      dw  GdtLen - 1  ; GDT界限  
  15.         dd  0       ; GDT基地址  
  16.   
  17. ; GDT 选择子  
  18. SelectorCode32      equ LABEL_DESC_CODE32   - LABEL_GDT  
  19. SelectorLDT     equ LABEL_DESC_LDT      - LABEL_GDT  
  20. SelectorVideo       equ LABEL_DESC_VIDEO    - LABEL_GDT   
  21. ; END of [SECTION .gdt]  
  22.   
  23. [SECTION .s16]  
  24. [BITS   16]  
  25. LABEL_BEGIN:  
  26.     mov ax, cs  
  27.     mov ds, ax  
  28.     mov es, ax  
  29.     mov ss, ax  
  30.     mov sp, 0100h  
  31.   
  32.     ; a)初始化 32 位代码段描述符  
  33.     xor eax, eax  
  34.     mov ax, cs                             ;把代码段基地址内容赋值给ax  
  35.     shl eax, 4                             ;左移4为相当与*16  
  36.     add eax, LABEL_SEG_CODE32              ;把加上段相对代码段的偏移地址,等于段的基地址  
  37.     mov word [LABEL_DESC_CODE32 + 2], ax   ;将该基地址分别填充到段描述符的 2 3 4 7字节上去  
  38.     shr eax, 16  
  39.     mov byte [LABEL_DESC_CODE32 + 4], al  
  40.     mov byte [LABEL_DESC_CODE32 + 7], ah  
  41.   
  42.     ; 初始化 LDT 在 GDT 中的描述符             L1)为了从全局描述符表中找到局部描述符表在内存中的地址  
  43.     xor eax, eax  
  44.     mov ax, ds  
  45.     shl eax, 4  
  46.     add eax, LABEL_LDT  
  47.     mov word [LABEL_DESC_LDT + 2], ax  
  48.     shr eax, 16  
  49.     mov byte [LABEL_DESC_LDT + 4], al  
  50.     mov byte [LABEL_DESC_LDT + 7], ah  
  51.   
  52.     ; 初始化 LDT 中的描述符                    L2)再从局部描述符表中找到相应段在内存中的地址  
  53.     xor eax, eax  
  54.     mov ax, ds  
  55.     shl eax, 4  
  56.     add eax, LABEL_CODE_A  
  57.     mov word [LABEL_LDT_DESC_CODEA + 2], ax  
  58.     shr eax, 16  
  59.     mov byte [LABEL_LDT_DESC_CODEA + 4], al  
  60.     mov byte [LABEL_LDT_DESC_CODEA + 7], ah  
  61.               
  62.     ; b)为加载 GDTR 作准备  
  63.     xor eax, eax  
  64.     mov ax, ds  
  65.     shl eax, 4  
  66.     add eax, LABEL_GDT      ; eax <- gdt 基地址  
  67.     mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址  
  68.   
  69.     ; c)加载 GDTR  
  70.     lgdt    [GdtPtr]      
  71.                           
  72.     ; d)关中断  
  73.     cli  
  74.   
  75.     ; e)打开地址线A20  
  76.     in  al, 92h                               
  77.     or  al, 00000010b  
  78.     out 92h, al                               
  79.   
  80.     ; f)准备切换到保护模式  
  81.     mov eax, cr0  
  82.     or  eax, 1  
  83.     mov cr0, eax  
  84.   
  85.     ; g)真正进入保护模式  
  86.     jmp dword SelectorCode32:0  ; 执行这一句会把 SelectorCode32 装入 cs,   
  87.                     ; 并跳转到 Code32Selector:0  处  
  88. ; END of [SECTION .s16]  
  89.   
  90.   
  91. [SECTION .s32]; 32 位代码段. 由实模式跳入.  
  92. [BITS   32]  
  93.   
  94. LABEL_SEG_CODE32:  
  95.     mov ax, SelectorVideo                     
  96.     mov gs, ax          ; 视频段选择子(目的)        1)把SelectorVideo段段选择子加载到段寄存器gs中  
  97.   
  98.     mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。  
  99.     mov ah, 0Ch         ; 0000: 黑底    1100: 红字  
  100.     mov al, 'P'  
  101.     mov [gs:edi], ax        ;3)根据段选择子在GDT中找到相应的段描述符,获得段基地址,再加上偏移量,就得到了实际物理地址,把数据写入该地址,这里是把数据写到显存中             
  102.     jmp SelectorLDTCodeA:0  ; L3)跳入局部任务   
  103.   
  104.     ; 到此停止  
  105.     jmp $  
  106.   
  107. SegCode32Len    equ $ - LABEL_SEG_CODE32  
  108. ; END of [SECTION .s32]  
  109.   
  110. ; LDT  
  111. [SECTION .ldt]  
  112. ALIGN   32  
  113. LABEL_LDT:  
  114. ;                            段基址       段界限      属性  
  115. LABEL_LDT_DESC_CODEA: Descriptor 0, CodeALen - 1, DA_C + DA_32 ; Code, 32 位  
  116.   
  117. LDTLen      equ $ - LABEL_LDT  
  118.   
  119. ; LDT 选择子  
  120. SelectorLDTCodeA    equ LABEL_LDT_DESC_CODEA    - LABEL_LDT + SA_TIL  
  121. ; END of [SECTION .ldt]  
  122.   
  123.   
  124. ; CodeA (LDT, 32 位代码段)  
  125. [SECTION .la]  
  126. ALIGN   32  
  127. [BITS   32]  
  128. LABEL_CODE_A:  
  129.     mov ax, SelectorVideo  
  130.     mov gs, ax          ; 视频段选择子(目的)  
  131.   
  132.     mov edi, (80 * 12 + 0) * 2  ; 屏幕第 10 行, 第 0 列。  
  133.     mov ah, 0Ch         ; 0000: 黑底    1100: 红字  
  134.     mov al, 'L'  
  135.     mov [gs:edi], ax  
  136.   
  137.     ; 准备经由16位代码段跳回实模式  
  138.     jmp SelectorCode16:0  
  139. CodeALen    equ $ - LABEL_CODE_A  
  140. ; END of [SECTION .la]  


可以看到最终得到物理地址并写数据需一行代码,但是前面需要做许多准备工作:a、b、c、d、e、f、g。具体内容看代码和批注。

上面讲到的是加载全局描述符表GDT,但是还有一个局部描述符表LDT没有涉及,那么LDT和GDT有什么区别呢,从上面代码上可以看到,LDT可以看做是GDT中的一个段,不过这个段里的内容是一张段描述符表。这两张表的关系如下图:

linux深入解析分段机制

整个虚拟地址空间共含有2^14个段(段选择子索引值为12位,不是应该只有2^12个段吗?),一半空间由GDT映射,另一般则由LDT映射。GDT所映射的一半虚拟地址空间是系统所有任务共有的,而LDT映射的另一半则在任务切换时被改变(这应该就是内核空间与用户空间的概念吧)。

linux深入解析分段机制