出处:http://blog.csdn.net/h_armony/article/details/9751605
先对分段机制中必须用到的一些名词作一个解释:
段选择符:又称端选择子,是段的一个16位标识符。它并不直接指向段,而是指向段选择符表中定义段的段描述符。它有三个字段内容:请求特权级RPL(Request Privilege Level)、表指示标志TI(Table Index)、索引值(Index)
段描述符:段描述符石GDT和LDT表中的一个数据结构项,用来向处理器提供一个有关段的位置和大小信息以及访问控制的状态信息。包含三个主要字段:段基地址、段限长、和段属性。段描述符通常由编译器、连接器、加载器或者操作系统来创建。
段描述符表: 是段描述符的一个数组。
接下来看一下适用分段机制将逻辑地址转化为线性地址的步骤:
1)使用段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符.(仅当一个新的段选择符加载到段寄存器中是才需要这一步)
2)利用段选择符检验段的访问权限和范围,以确保该段可访问。
3)把段描述符中取到的段基地址加到偏移量上,最后形成一个线性地址。
从《一个操作系统的实现》的源码来理解分段机制:
- org 07c00h
- jmp LABEL_BEGIN
- [SECTION .gdt]
- ; GDT
- ; 段基址, 段界限 , 属性
- LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
- LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32; 非一致代码段
- LABEL_DESC_LDT: Descriptor 0, LDTLen - 1, DA_LDT ; LDT
- LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
- ; GDT 结束
- GdtLen equ $ - LABEL_GDT ; GDT长度
- GdtPtr dw GdtLen - 1 ; GDT界限
- dd 0 ; GDT基地址
- ; GDT 选择子
- SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
- SelectorLDT equ LABEL_DESC_LDT - LABEL_GDT
- SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
- ; END of [SECTION .gdt]
- [SECTION .s16]
- [BITS 16]
- LABEL_BEGIN:
- mov ax, cs
- mov ds, ax
- mov es, ax
- mov ss, ax
- mov sp, 0100h
- ; a)初始化 32 位代码段描述符
- xor eax, eax
- mov ax, cs ;把代码段基地址内容赋值给ax
- shl eax, 4 ;左移4为相当与*16
- add eax, LABEL_SEG_CODE32 ;把加上段相对代码段的偏移地址,等于段的基地址
- mov word [LABEL_DESC_CODE32 + 2], ax ;将该基地址分别填充到段描述符的 2 3 4 7字节上去
- shr eax, 16
- mov byte [LABEL_DESC_CODE32 + 4], al
- mov byte [LABEL_DESC_CODE32 + 7], ah
- ; 初始化 LDT 在 GDT 中的描述符 L1)为了从全局描述符表中找到局部描述符表在内存中的地址
- xor eax, eax
- mov ax, ds
- shl eax, 4
- add eax, LABEL_LDT
- mov word [LABEL_DESC_LDT + 2], ax
- shr eax, 16
- mov byte [LABEL_DESC_LDT + 4], al
- mov byte [LABEL_DESC_LDT + 7], ah
- ; 初始化 LDT 中的描述符 L2)再从局部描述符表中找到相应段在内存中的地址
- xor eax, eax
- mov ax, ds
- shl eax, 4
- add eax, LABEL_CODE_A
- mov word [LABEL_LDT_DESC_CODEA + 2], ax
- shr eax, 16
- mov byte [LABEL_LDT_DESC_CODEA + 4], al
- mov byte [LABEL_LDT_DESC_CODEA + 7], ah
- ; b)为加载 GDTR 作准备
- xor eax, eax
- mov ax, ds
- shl eax, 4
- add eax, LABEL_GDT ; eax <- gdt 基地址
- mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
- ; c)加载 GDTR
- lgdt [GdtPtr]
- ; d)关中断
- cli
- ; e)打开地址线A20
- in al, 92h
- or al, 00000010b
- out 92h, al
- ; f)准备切换到保护模式
- mov eax, cr0
- or eax, 1
- mov cr0, eax
- ; g)真正进入保护模式
- jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,
- ; 并跳转到 Code32Selector:0 处
- ; END of [SECTION .s16]
- [SECTION .s32]; 32 位代码段. 由实模式跳入.
- [BITS 32]
- LABEL_SEG_CODE32:
- mov ax, SelectorVideo
- mov gs, ax ; 视频段选择子(目的) 1)把SelectorVideo段段选择子加载到段寄存器gs中
- mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。
- mov ah, 0Ch ; 0000: 黑底 1100: 红字
- mov al, 'P'
- mov [gs:edi], ax ;3)根据段选择子在GDT中找到相应的段描述符,获得段基地址,再加上偏移量,就得到了实际物理地址,把数据写入该地址,这里是把数据写到显存中
- jmp SelectorLDTCodeA:0 ; L3)跳入局部任务
- ; 到此停止
- jmp $
- SegCode32Len equ $ - LABEL_SEG_CODE32
- ; END of [SECTION .s32]
- ; LDT
- [SECTION .ldt]
- ALIGN 32
- LABEL_LDT:
- ; 段基址 段界限 属性
- LABEL_LDT_DESC_CODEA: Descriptor 0, CodeALen - 1, DA_C + DA_32 ; Code, 32 位
- LDTLen equ $ - LABEL_LDT
- ; LDT 选择子
- SelectorLDTCodeA equ LABEL_LDT_DESC_CODEA - LABEL_LDT + SA_TIL
- ; END of [SECTION .ldt]
- ; CodeA (LDT, 32 位代码段)
- [SECTION .la]
- ALIGN 32
- [BITS 32]
- LABEL_CODE_A:
- mov ax, SelectorVideo
- mov gs, ax ; 视频段选择子(目的)
- mov edi, (80 * 12 + 0) * 2 ; 屏幕第 10 行, 第 0 列。
- mov ah, 0Ch ; 0000: 黑底 1100: 红字
- mov al, 'L'
- mov [gs:edi], ax
- ; 准备经由16位代码段跳回实模式
- jmp SelectorCode16:0
- CodeALen equ $ - LABEL_CODE_A
- ; END of [SECTION .la]
可以看到最终得到物理地址并写数据需一行代码,但是前面需要做许多准备工作:a、b、c、d、e、f、g。具体内容看代码和批注。
上面讲到的是加载全局描述符表GDT,但是还有一个局部描述符表LDT没有涉及,那么LDT和GDT有什么区别呢,从上面代码上可以看到,LDT可以看做是GDT中的一个段,不过这个段里的内容是一张段描述符表。这两张表的关系如下图:
整个虚拟地址空间共含有2^14个段(段选择子索引值为12位,不是应该只有2^12个段吗?),一半空间由GDT映射,另一般则由LDT映射。GDT所映射的一半虚拟地址空间是系统所有任务共有的,而LDT映射的另一半则在任务切换时被改变(这应该就是内核空间与用户空间的概念吧)。