x86保护模式——全局描述符表GDT详解
1 - GDT作用
GDT全称Global Descriptor Table,是x86保护模式下的一个重要数据结构,在保护模式下,GDT在内存中有且只有一个。GDT的数据结构是一个描述符数组,每个描述符8个字节,可以存放在内存当中任意位置: 其中,addr相当于GDT的基地址,GDT的总长度(单位字节)为GDT界限。
在实模式中,CPU通过段地址和段偏移量寻址。其中段地址保存到段寄存器,包含:CS、SS、DS、ES、FS、GS。段偏移量可以保存到IP、BX、SI、DI寄存器。在汇编代码mov ds:[si], ax中,会将AX寄存器的数据写入到物理内存地址DS * 16 + SI中。
而在保护模式下,也是通过段寄存器和段偏移量寻址,但是此时段寄存器保存的数据意义不同了。 此时的CS和SS寄存器后13位相当于GDT表中某个描述符的索引,即段选择子。第2位存储了TI值(0代表GDT,1代表LDT),第0、1位存储了当前的特权级(CPL)。 例如在保护模式下执行汇编代码mov ds:[si], ax的大致步骤如下:
首先CPU需要查找GDT在内存中位置,GDT的位置从GDTR寄存器中直接获取然后根据DS寄存器得到目标段描述符的物理地址计算出描述符中的段基址的值加上SI寄存器存储的偏移量的结果,该结果为目标物理地址将AX寄存器中的数据写入到目标物理地址
2 - GDTR寄存器
CPU切换到保护模式前,需要准备好GDT数据结构,并执行LGDT指令,将GDT基地址和界限传入到GDTR寄存器。
GDTR寄存器长度为6字节(48位),前两个字节为GDT界限,后4个字节为GDT表的基地址。所以说,GDT最多只能拥有8192个描述符(65536 / 8)。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AXzy19kY-1603970086921)(#pic_center)]
一旦切换到保护模式,一般不会更改GDTR寄存器的内容。
3 - GDT段描述符结构
一个GDT段描述符占用8个字节,包含三个部分:
段基址(32位),占据描述符的第16~39位和第55位~63位,前者存储低16位,后者存储高16位段界限(20位),占据描述符的第0~15位和第48~51位,前者存储低16位,后者存储高4位。段属性(12位),占据描述符的第39~47位和第49~55位,段属性可以细分为8种:TYPE属性、S属性、DPL属性、P属性、AVL属性、L属性、D/B属性和G属性。
下面介绍各个属性的作用:
S属性
S属性存储了描述符的类型
S
=
0
S=0
S=0 时,该描述符对应的段是系统段(System Segment)。
S
=
1
S=1
S=1 时,该描述符对应的段是数据段(Data Segment)或者代码段(Code Segment)
TYPE属性
TYPE属性存储段的类型信息,该属性的意义随着S属性不同而不同。 当
S
=
1
S=1
S=1 (该段为数据段或代码段)时,需要分为两种情况:
当TYPE属性第三位为0时,代表该段为数据段,第0~2位的作用为:
位作用值为0时值为1时2段的增长方向向上增长向下增长(例如栈段)1段的写权限只读可读可写0段的访问标记该段未被访问过该段已被访问过 (第0位对应描述符的第43位,第1位对应第42位,以此类推)当TYPE属性第三位为1时,代表该段为代码段,第0~2位的作用为:
位作用值为0时值为1时2一致代码段标记不是一致代码段是一致代码段1段的读权限只能执行可读、可执行0段的访问标记该段未被访问过该段已被访问过 一致代码段的“一致”意思是:当CPU执行jmp等指令将CS寄存器指向该代码段时,如果当前的特权级低于该代码段的特权级,那么当前的特权级会被延续下去(简单的说就是可以被低特权级的用户直接访问的代码),如果当前的特权级高于该代码段的特权级,那么会触发常规保护错误(可以理解为内核态下不允许直接执行用户态的代码)。 如果不是一致代码段并且该代码段的特权级不等于(高于和低于都不行)当前的特权级,那么会引发常规保护错误。
当
S
=
0
S=0
S=0 (该段为系统段)时:
TYPE的值(16进制)TYPE的值(二进制)解释0x10 0 0 1可用286TSS0x20 0 1 0该段存储了局部描述符表(LDT)0x30 0 1 1忙的286TSS0x40 1 0 0286调用门0x50 1 0 1任务门0x60 1 1 0286中断门0x70 1 1 1286陷阱门0x91 0 0 1可用386TSS0xB1 0 1 1忙的386TSS0xC1 1 0 0386调用门0xE1 1 1 0386中断门0xF1 1 1 1386陷阱门
(其余值均为未定义)
DPL属性
DPL属性占2个比特,记录了访问段所需要的特权级,特权级范围为0~3,越小特权级越高。
P属性
P属性标记了该段是否存在:
P
=
0
P=0
P=0 时,该段在内存中不存在
P
=
1
P=1
P=1 时,该段在内存中存在
尝试访问一个在内存中不存在的段会触发段不存在错误(#NP)
AVL属性
AVL属性占1个比特,该属性的意义可由操作系统、应用程序自行定义。 Intel保证该位不会被占用作为其他用途。
L属性
该属性仅在IA-32e模式下有意义,它标记了该段是否为64位代码段。 当
L
=
1
L=1
L=1 时,表示该段是64位代码段。 如果设置了L属性为1,则必须保证D属性为0。
D/B属性
D/B属性中的D/B全称 Default operation size / Default stack pointer size / Upper bound。 该属性的意义随着段描述符是代码段(Code Segment)、向下扩展数据段(Expand-down Data Segment)还是栈段(Stack Segment)而有所不同。
代码段(S属性为1,TYPE属性第三位为1) 如果对应的是代码段,那么该位称之为D属性(D flag)。如果设置了该属性,那么会被视为32位代码段执行;如果没有设置,那么会被视为16位代码段执行。
栈段(被SS寄存器指向的数据段) 该情况下称之为B属性。如果设置了该属性,那么在执行堆栈访问指令(例如PUSH、POP指令)时采用32位堆栈指针寄存器(ESP寄存器),如果没有设置,那么采用16位堆栈指针寄存器(SP寄存器)。
向下扩展的数据段 该情况下称之为B属性。如果设置了该属性,段的上界为4GB,否则为64KB。
G属性
G属性记录了段界限的粒度:
G
=
0
G=0
G=0 时,段界限的粒度为字节
G
=
1
G=1
G=1 时,段界限的粒度为4KB
例如,当
G
=
0
G=0
G=0 并且描述符中的段界限值为
10000
10000
10000,那么该段的界限为10000字节,如果
G
=
1
G=1
G=1,那么该段的界限值为40000KB。
所以说,当
G
=
0
G=0
G=0 时,一个段的最大界限值为1MB(因为段界限只能用20位表示,
2
20
=
1048576
2^{20}=1048576
220=1048576),最小为1字节(段的大小等于段界限值加1)。 当
G
=
1
G=1
G=1 时,最大界限值为4GB,最小为4KB。
在访问段(除栈段)时,如果超出了段的界限,那么会触发常规保护错误(#GP) 如果访问栈段超出了界限,那么会产生堆栈错误(#SS)
案例学习
实现保护模式下打印红色的Hello, world(不依赖操作系统和BIOS中断服务)。
首先定义单个描述符的数据结构,用NASM汇编宏可以表示为
; 描述符
; usage: Descriptor Base, Limit, Attr
; Base: dd
; Limit: dd (low 20 bits available)
; Attr: dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限 1 (2 字节)
dw %1 & 0FFFFh ; 段基址 1 (2 字节)
db (%1 >> 16) & 0FFh ; 段基址 2 (1 字节)
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2 (2 字节)
db (%1 >> 24) & 0FFh ; 段基址 3 (1 字节)
%endmacro
定义段属性常量:
DA_DR EQU 90h ; 存在的只读数据段类型值
DA_DRW EQU 92h ; 存在的可读写数据段属性值
DA_DRWA EQU 93h ; 存在的已访问可读写数据段类型值
DA_C EQU 98h ; 存在的只执行代码段属性值
DA_CR EQU 9Ah ; 存在的可执行可读代码段属性值
DA_CCO EQU 9Ch ; 存在的只执行一致代码段属性值
DA_CCOR EQU 9Eh ; 存在的可执行可读一致代码段属性值
定义三个描述符:
负责打印字符串的代码段DESC_CODE: Descriptor 0, CODE32_LEN - 1, DA_C + DA_32
此处段基址暂时设置为0,在实模式下动态设置存放需要打印的字符串(Hello, world字符串)的数据段DESC_DATA: Descriptor 0, STRING_LEN - 1, DA_DR
同样设置为0,在实模式下动态设置显存映射到内存的数据段(固定在0xB8000,如果不懂可以百度),并且该段要设置成可写DESC_VIDEO: Descriptor 0xB8000, 0xFFFF, DA_DRW
定义GDTR寄存器的数据:
GdtLen equ $ - DESC_GDT ; GDT表的长度
GdtPtr dw GdtLen ; GDT界限
dd 0 ; GDT基址,暂时设置为0,需要动态设置
定义段选择子:
DataSelector equ DESC_DATA - DESC_GDT
CodeSelector equ DESC_CODE - DESC_GDT
VideoSelector equ DESC_VIDEO - DESC_GDT
因为TI和DPL都为0,且正好占据3位,所以只需要将描述符的地址减去基址就可以得到索引了(等同于左移3位)
然后是实模式下的初始化代码,主要完成三件事:
初始化段描述符初始化GDT的基址,并存放到GDTR寄存器切换到保护模式(打开A20地址线,将CR0寄存器第0位设置为1),并跳转到负责打印字符串的代码段。
代码如下:
%include "pm.inc"
org 0x7C00
jmp _main_16
[SECTION .gdt]
DESC_GDT: Descriptor 0, 0, 0 ;该描述符仅用来计算下面三个描述符的地址
DESC_VIDEO: Descriptor 0xB8000, 0xFFFF, DA_DRW
DESC_DATA: Descriptor 0, STRING_LEN - 1, DA_DR
DESC_CODE: Descriptor 0, CODE32_LEN - 1, DA_C + DA_32
GdtLen equ $ - DESC_GDT
GdtPtr dw GdtLen
dd 0
DataSelector equ DESC_DATA - DESC_GDT
CodeSelector equ DESC_CODE - DESC_GDT
VideoSelector equ DESC_VIDEO - DESC_GDT
[SECTION .s16]
[BITS 16]
_main_16:
; 初始化DS/ES/SS/SP寄存器
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
; 初始化段描述符
call near _init_desc
; 初始化GDT基址
xor eax, eax
mov ax, ds
shl eax, 4
add eax, DESC_GDT
mov dword [GdtPtr + 2], eax
lgdt [GdtPtr]
cli
in al, 0x92
or al, 00000010b
out 0x92, al
mov eax, cr0
or eax, 1
mov cr0, eax
; 跳转到代码段
jmp dword CodeSelector:0
_init_desc:
xor eax, eax
mov ax, cs
shl eax, 4
add eax, _main_32
mov di, DESC_CODE
call near _init_desc_base_address
xor eax, eax
mov ax, cs
shl eax, 4
add eax, STRING
mov di, DESC_DATA
call near _init_desc_base_address
ret
_init_desc_base_address:
mov word [di + 2], ax
shr eax, 16
mov byte [di + 4], al
mov byte [di + 7], ah
ret
最后是打印字符串的32位代码段:
[SECTION .s32]
[BITS 32]
_main_32:
mov ax, VideoSelector
mov gs, ax
mov esi, 0xA0
mov ax, DataSelector
mov ds, ax
mov edi, 0
mov ecx, STRING_LEN
print_loop:
mov al, ds:[edi]
mov ah, 0xC
mov word gs:[esi], ax
add esi, 2
inc edi
loop print_loop
jmp $
CODE32_LEN equ $ - _main_32
[SECTION .s32]
[BITS 32]
STRING: db 'Hello, world'
STRING_LEN equ $ - STRING
完整代码:
pm.inc
DA_DR EQU 90h ; 存在的只读数据段类型值
DA_DRW EQU 92h ; 存在的可读写数据段属性值
DA_DRWA EQU 93h ; 存在的已访问可读写数据段类型值
DA_C EQU 98h ; 存在的只执行代码段属性值
DA_CR EQU 9Ah ; 存在的可执行可读代码段属性值
DA_CCO EQU 9Ch ; 存在的只执行一致代码段属性值
DA_CCOR EQU 9Eh ; 存在的可执行可读一致代码段属性值
; 宏 ------------------------------------------------------------------------------------------------------
;
; 描述符
; usage: Descriptor Base, Limit, Attr
; Base: dd
; Limit: dd (low 20 bits available)
; Attr: dw (lower 4 bits of higher byte are always 0)
%macro Descriptor 3
dw %2 & 0FFFFh ; 段界限 1 (2 字节)
dw %1 & 0FFFFh ; 段基址 1 (2 字节)
db (%1 >> 16) & 0FFh ; 段基址 2 (1 字节)
dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性 1 + 段界限 2 + 属性 2 (2 字节)
db (%1 >> 24) & 0FFh ; 段基址 3 (1 字节)
%endmacro
boot.asm:
%include "pm.inc"
org 0x7C00
jmp _main_16
[SECTION .gdt]
DESC_GDT: Descriptor 0, 0, 0
DESC_VIDEO: Descriptor 0xB8000, 0xFFFF, DA_DRW
DESC_DATA: Descriptor 0, STRING_LEN - 1, DA_DR
DESC_CODE: Descriptor 0, CODE32_LEN - 1, DA_C + DA_32
GdtLen equ $ - DESC_GDT
GdtPtr dw GdtLen
dd 0
DataSelector equ DESC_DATA - DESC_GDT
CodeSelector equ DESC_CODE - DESC_GDT
VideoSelector equ DESC_VIDEO - DESC_GDT
[SECTION .s16]
[BITS 16]
_main_16:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
call near _init_desc
; 设置段基址
xor eax, eax
mov ax, ds
shl eax, 4
add eax, DESC_GDT
mov dword [GdtPtr + 2], eax
lgdt [GdtPtr]
cli
in al, 0x92
or al, 00000010b
out 0x92, al
mov eax, cr0
or eax, 1
mov cr0, eax
jmp dword CodeSelector:0
_init_desc:
xor eax, eax
mov ax, cs
shl eax, 4
add eax, _main_32
mov di, DESC_CODE
call near _init_desc_base_address
xor eax, eax
mov ax, cs
shl eax, 4
add eax, STRING
mov di, DESC_DATA
call near _init_desc_base_address
ret
_init_desc_base_address:
mov word [di + 2], ax
shr eax, 16
mov byte [di + 4], al
mov byte [di + 7], ah
ret
[SECTION .s32]
[BITS 32]
_main_32:
mov ax, VideoSelector
mov gs, ax
mov esi, 0xA0
mov ax, DataSelector
mov ds, ax
mov edi, 0
mov ecx, STRING_LEN
print_loop:
mov al, ds:[edi]
mov ah, 0xC ;设置成红色
mov word gs:[esi], ax
add esi, 2
inc edi
loop print_loop
jmp $
CODE32_LEN equ $ - _main_32
[SECTION .s32]
[BITS 32]
STRING: db 'Hello, world'
STRING_LEN equ $ - STRING
times 290 db 0
dw 0xAA55
编译:
NASM -f bin -o boot.com boot.asm
生成IMG软盘文件镜像:
dd if=boot.com of=boot.img bs=512 count=1
dd if=/dev/zero of=/tmp/empty.img bs=512 count=2880
dd if=/tmp/empty.img of=boot.img skip=1 seek=1 bs=512 count=2879
使用VMWare虚拟机,添加软盘设备并启动,运行结果: