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的大致步骤如下:

  1. 首先CPU需要查找GDT在内存中位置,GDT的位置从GDTR寄存器中直接获取
  2. 然后根据DS寄存器得到目标段描述符的物理地址
  3. 计算出描述符中的段基址的值加上SI寄存器存储的偏移量的结果,该结果为目标物理地址
  4. 将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可用286TSS
0x20 0 1 0该段存储了局部描述符表(LDT)
0x30 0 1 1忙的286TSS
0x40 1 0 0286调用门
0x50 1 0 1任务门
0x60 1 1 0286中断门
0x70 1 1 1286陷阱门
0x91 0 0 1可用386TSS
0xB1 0 1 1忙的386TSS
0xC1 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属性。如果设置了该属性,那么在执行堆栈访问指令(例如PUSHPOP指令)时采用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位)

然后是实模式下的初始化代码,主要完成三件事:

  1. 初始化段描述符
  2. 初始化GDT的基址,并存放到GDTR寄存器
  3. 切换到保护模式(打开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虚拟机,添加软盘设备并启动,运行结果:
在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐