x86保护模式——全局描述符表GDT详解
GDT作用GDT全称Global Descriptor Table,是x86保护模式下的一个重要数据结构,在保护模式下,GDT在内存中有且只有一个。GDT的数据结构是一个描述符数组,每个描述符8个字节,可以存放在内存当中任意位置:其中,addr相当于GDT的基地址,GDT的总长度(单位字节)为GDT界限。在实模式中,CPU通过段地址和段偏移量寻址。其中段地址保存到段寄存器,包含:CS、SS、DS、
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位对应描述符的第43位,第1位对应第42位,以此类推)位 作用 值为0时 值为1时 2 段的增长方向 向上增长 向下增长(例如栈段) 1 段的写权限 只读 可读可写 0 段的访问标记 该段未被访问过 该段已被访问过 - 当TYPE属性第三位为1时,代表该段为代码段,第0~2位的作用为:
一致代码段的“一致”意思是:当CPU执行位 作用 值为0时 值为1时 2 一致代码段标记 不是一致代码段 是一致代码段 1 段的读权限 只能执行 可读、可执行 0 段的访问标记 该段未被访问过 该段已被访问过 jmp
等指令将CS寄存器指向该代码段时,如果当前的特权级低于该代码段的特权级,那么当前的特权级会被延续下去(简单的说就是可以被低特权级的用户直接访问的代码),如果当前的特权级高于该代码段的特权级,那么会触发常规保护错误(可以理解为内核态下不允许直接执行用户态的代码)。
如果不是一致代码段并且该代码段的特权级不等于(高于和低于都不行)当前的特权级,那么会引发常规保护错误。
当 S = 0 S=0 S=0 (该段为系统段)时:
TYPE的值(16进制) | TYPE的值(二进制) | 解释 |
---|---|---|
0x1 | 0 0 0 1 | 可用286TSS |
0x2 | 0 0 1 0 | 该段存储了局部描述符表(LDT) |
0x3 | 0 0 1 1 | 忙的286TSS |
0x4 | 0 1 0 0 | 286调用门 |
0x5 | 0 1 0 1 | 任务门 |
0x6 | 0 1 1 0 | 286中断门 |
0x7 | 0 1 1 1 | 286陷阱门 |
0x9 | 1 0 0 1 | 可用386TSS |
0xB | 1 0 1 1 | 忙的386TSS |
0xC | 1 1 0 0 | 386调用门 |
0xE | 1 1 1 0 | 386中断门 |
0xF | 1 1 1 1 | 386陷阱门 |
(其余值均为未定义)
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 ; 存在的可执行可读一致代码段属性值
定义三个描述符:
- 负责打印字符串的代码段
此处段基址暂时设置为0,在实模式下动态设置DESC_CODE: Descriptor 0, CODE32_LEN - 1, DA_C + DA_32
- 存放需要打印的字符串(
Hello, world
字符串)的数据段
同样设置为0,在实模式下动态设置DESC_DATA: Descriptor 0, STRING_LEN - 1, DA_DR
- 显存映射到内存的数据段(固定在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虚拟机,添加软盘设备并启动,运行结果:
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)