在x86架构的保护模式下,段访问是通过段选择子和段描述符来实现的。段选择子指定了要访问的段在全局描述符表或局部描述符表中的索引和访问权限,而段描述符则包含了有关段的详细信息,如段的起始地址、大小、访问权限等。
在保护模式下,段描述符被存储在全局描述符表(GDT)或局部描述符表(LDT)中。这些描述符表是由操作系统或应用程序定义的数据结构,用于存储段描述符。
当需要访问一个段时,需要使用合适的段选择子。段选择子由16位的值组成,其中包含了段描述符的索引和特定的标志位。通过段选择子的值,处理器可以在描述符表中查找到对应的段描述符。
GDTR的32位线性基地址部分保存的是全局描述符表在内存中的起始线性地址,16位边界部分保存的是全局描述符表的边界(界限),其在数值上等于表的大小(总字节数)减一。因为GDT的界限是16位的,所以,该表最大是65536字节(64KB)。又因为一个描述符占8字节,故最多可以定义8192个描述符。
段描述符
每个描述符在GDT中占8字节,也就是2个双字,64位。下面是低32位(低双字),上面是高32位(高双字)。
- G位是粒度(Granularity)位,用于解释段界限的含义。当G位是“0”时,段界限以字节为单位。此时,段的扩展范围是从1字节到1兆字节(1B~1MB),因为描述符中的界限值是20位的。相反,如果该位是“1”,那么,段界限是以4KB为单位的。这样,段的扩展范围(段的大小)是从4KB到4GB。
- S位用于指定描述符的类型(Descriptor Type)。当该位是“0”时,表示是一个系统段;为“1”时,表示是一个代码段或者数据段(栈段也是特殊的数据段)。
- DPL表示描述符的特权级(Descriptor Privilege Level, DPL)。共有4种处理器支持的特权级别,分别是0、1、2、3,其中0是最高特权级别,3是最低特权级别。特权级是一个数字,可以赋给一个程序,用来决定该程序能够执行哪些指令,或者能够访问哪些系统资源;也可以赋给系统资源,用来决定哪些程序可以访问它们。
- P是段存在位(Segment Present)。P位用于指示描述符所对应的段是否存在。一般来说,描述符所指示的段都位于内存中。但是,当内存空间紧张时,有可能只是建立了描述符,对应的内存空间并不存在,这时,就应当把描述符的P位清零,表示段并不存在。P位是由处理器负责检查的。每当通过描述符访问内存中的段时,如果P位是“0”,处理器就会产生一个异常中断。通常,该中断处理过程是由操作系统提供的,该处理过程的任务是负责将该段从硬盘换回内存,并将P位置1。在多用户、多任务的系统中,这是一种常用的虚拟内存调度策略。
- D/B位是“默认操作尺寸”(Default Operation Size)或者“默认的栈指针尺寸”(Default Stack Pointer Size),又或者“上部边界”(Upper Bound)标志。设立该标志位,主要是为了能够在32位处理器上兼容运行16位保护模式的程序。该标志位对不同的段有不同的效果。对于代码段,此位称作“D”位,用于指示指令中默认的有效地址和操作数尺寸。D=0表示指令中的有效地址或者操作数是16位的;D=1,指示32位的有效地址或者操作数。举个例子,如果代码段描述符的D位是0,那么,当处理器在这个段上执行时,将使用16位的指令指针寄存器IP来取指令,访问内存时,强制使用16位的有效地址;否则,使用32位的EIP,访问内存时,使用32位的有效地址。对于栈段和向下扩展的数据段来说,该位被叫作“B”位,用于指定在进行隐式的栈操作时,是使用寄存器SP还是寄存器ESP,隐式的栈操作指令包括push、pop和call等。如果该位是“0”,在访问那个段时,使用寄存器SP,否则就是使用寄存器ESP。
- L位是64位代码段标志(64-bit Code Segment),保留此位给64位处理器使用。
TYPE字段共4位,用于指示描述符的子类型,或者说是类别。
对于数据段来说,这4位分别是X、E、W、A位:
- X位是可执行位,数据段是不可执行的,X位为0。
- E位指示段的扩展方向。E=0是向上扩展的,也就是向高地址方向扩展的,是普通的数据段;E=1是向下扩展的,也就是向低地址方向扩展的,通常是栈段。
- W位指示段的读写属性,或者说段是否可写,W=0的段是不允许写入的,否则会引发处理器异常中断;W=1的段是可以正常写入的。
- A位是已访问(Accessed)位,用于指示它所指向的段最近是否被访问过。
对于代码段来说,这4位则分别是X、C、R、A位:
- X位是可执行位,X位为0,不可执行,X位为1,可执行。
- C位指示段是否为特权级依从的(Conforming)。C=0表示非依从的代码段,这样的代码段可以从与它特权级相同的代码段调用,或者通过门调用;C=1表示允许从低特权级的程序转移到该段执行。
- R位指示代码段是否允许读出。代码段总是可以执行的,但是,为了防止程序被破坏,它是不能写入的。至于是否有读出的可能,由R位指定。
- AVL是软件可以使用的位(Available),通常由操作系统来使用。
段选择子
在保护模式下访问一个段时,传送到段寄存器的是段选择子
15-3 | 2 | 1-0 |
---|---|---|
段描述符索引 (Segment Index) | TI | RPL |
- 段描述符索引(Segment Descriptor Index):占据 13 位,用于指定段描述符在 GDT(全局描述符表)或 LDT(局部描述符表)中的索引位置。
- TI(Table Indicator):占据 1 位,用于指示段描述符索引是指向 GDT(TI = 0)还是 LDT(TI = 1)。
- RPL(Requested Privilege Level):占据 2 位,指定请求访问该段的特权级别。
Nasm Code
section mbr vstart=0x7c00
mov ax, 0
mov ss, ax
mov ax, 0x7c00
mov sp, ax
mov ax,[cs:gdt_base] ; 低16位
mov dx,[cs:gdt_base+0x02]
mov bx, 16
div bx
mov ds, ax ; 段地址
mov bx, dx ; 偏移地址
mov dword [bx], 0x00 ; 创建空描述符
mov dword [bx + 0x04], 0x00
; 代码段描述符
mov dword [bx+0x08],0x7c0001ff ; 段基地址: 0x7c00
mov dword [bx+0x0c],0x00409800 ; 段界限: 0x1ff
; 粒度 G 0
; 描述符类型 S 1
; 描述符特权级 DPL 00
; 段存在位 P 1
; 默认操作尺寸 D/B 1
; 描述符的子类型 TYPE XEWA/XEWA 1000
; 软件可使用位 AVL
; 数据段描述符
mov dword [bx+0x10],0x8000ffff ; 段基地址: 0xb8000
mov dword [bx+0x14],0x0040920b ; 段界限: 0xffff
; 粒度 G 0
; 描述符类型 S 1
; 描述符特权级 DPL 00
; 段存在位 P 1
; 默认操作尺寸 D/B 1
; 描述符的子类型 TYPE XEWA/XEWA 0010
; 软件可使用位 AVL 0
; 堆栈段描述符
mov dword [bx+0x18],0x00007a00 ; 段基地址: 0x0
mov dword [bx+0x1c],0x00409600 ; 段界限: 0x7a00
; 粒度 G 0
; 描述符类型 S 1
; 描述符特权级 DPL 00
; 段存在位 P 1
; 默认操作尺寸 D/B 1
; 描述符的子类型 TYPE XEWA/XEWA 0110
; 软件可使用位 AVL 0
mov word [cs: gdt_size], 31 ; 描述符界限 字节数减一
lgdt [cs: gdt_size] ; 读6字节,先读大小,然后读地址
in al, 0x92 ; 端口0x92的位1用于控制A20,叫作替代的A20门控制(Alternate A20 Gate,ALT_A20_GATE)
or al, 0000_0010B ; 它和来自键盘控制器的A20控制线一起,通过或门连接到处理器的A20M#引脚
out 0x92, al ; 打开A20
cli ; 关中断,保护模式下的中断机制和实模式不同,
; 因此,原有的中断向量表不再适用,而且,必须要知道的是,在保护模式下,
; BIOS中断都不能再用,因为它们是16位的代码。在重新设置保护模式下的中断环境之前,必须关中断。
mov eax, cr0 ; CR0是32位的寄存器,包含了一系列用于控制处理器操作模式和运行状态的标志位。
or eax, 1 ; 它的第1位(位0)是保护模式允许位(Protection Enable,PE),是开启保护模式大门的门把手,
mov cr0 ,eax ; 如果把该位置“1”,则处理器进入保护模式,按保护模式的规则开始运行。
jmp dword 0x0008:flush - 0x7c00 ; 16位的描述符选择子:32位偏移;每当引用一个段时,处理器自动将段地址左移4位,并传送到描述符高速缓存器。
; 此后,就一直使用描述符高速缓存器的内容作为段的线性基地址。
; 刷新CS描述符高速缓存器,将CS描述符高速缓存器中的D位置1,使处理器从16位保护模式转至32位保护模式。
; 清流水线并串行化处理器
[bits 32]
flush:
mov ax, 0x10 ; 0x10 == 0b10000 索引0b10,TI=0,RPL=0
mov ds, ax ; 将通过段选择子,定位到段描述符,然后将段描述符中的信息送到描述符高速缓存器
mov byte [0x00], 'P'
mov byte [0x02], 'r'
mov byte [0x04], 'o'
mov byte [0x06], 't'
mov byte [0x08], 'e'
mov byte [0x0a], 'c'
mov byte [0x0c], 't'
mov byte [0x0e], ' '
mov byte [0x10], 'm'
mov byte [0x12], 'o'
mov byte [0x14], 'd'
mov byte [0x16], 'e'
mov byte [0x18], ' '
mov byte [0x1a], 'O'
mov byte [0x1c], 'K'
hlt
gdt_size dw 0 ; 全局描述符表边界
gdt_base dd 0x00007e00 ; 全局描述符表线性基地址,放置在主引导扇区之后
times 510-($-$$) db 0
db 0x55,0xaa
段描述符分析代码
import struct
# 定义 GDTEntry 结构体
class GDTEntry:
def __init__(self, limit, base, type, granularity, dpl, present, s, operation_size):
self.limit = limit & 0xFFFFF
self.base_low = base & 0xFFFF
self.base_mid = (base >> 16) & 0xFF
self.base_high = (base >> 24) & 0xFF
self.granularity = granularity & 0b1
self.operation_size = operation_size & 0b1
self.dpl = dpl & 0b11
self.present = present & 0b1
self.type = type & 0xF
self.avl = 0b0
self.l = 0
self.s = s & 0b1
def __str__(self):
return f"GDT Entry: \n" \
f" Limit: {hex(self.limit)}\n" \
f" Base: {hex(self.base_low | (self.base_mid << 16) | (self.base_high << 24))}\n" \
f" Granularity: {self.granularity}\n" \
f" Operation Size: {self.operation_size}\n" \
f" Present: {self.present}\n" \
f" DPL: {self.dpl}\n" \
f" Type: {bin(self.type)}"
def generate_gdt_entry(limit, base, type, granularity, dpl, present, s, operation_size):
entry = GDTEntry(limit, base, type, granularity, dpl, present, s, operation_size)
low = ((entry.base_low & 0xFFFF) << 16) + (entry.limit & 0xFFFF)
high = (entry.base_high << 24) + \
(entry.granularity << 23) + \
(entry.operation_size << 22) + \
(entry.l << 21) + (entry.avl << 20) + \
((entry.limit >> 16) << 16) + \
(entry.present << 15) + \
(entry.dpl << 13) + \
(entry.s << 12) + \
(entry.type << 8) + \
entry.base_mid
return {'high': hex(high), 'low': hex(low)}
def parse_gdt_entry(high, low):
entry_bytes = high.to_bytes(4, 'big') + low.to_bytes(4, 'big')
entry_bytes = entry_bytes[::-1]
limit = ((entry_bytes[7] & 0xf) << 16) + (entry_bytes[1] << 8) + entry_bytes[0]
base = (entry_bytes[7] << 24) + (entry_bytes[4] << 16) + (entry_bytes[3] << 8) + entry_bytes[2]
type_val = entry_bytes[5] & 0x0F
s = (entry_bytes[5] >> 4) & 0b1
granularity = (entry_bytes[6] >> 7) & 0b1
dpl = (entry_bytes[5] >> 5) & 0b11
present = (entry_bytes[5] >> 7) & 0b1
operation_size = (entry_bytes[6] >> 6) & 0b1
gdt_entry = GDTEntry(limit, base, type_val, granularity, dpl, present, s, operation_size)
return gdt_entry
print(parse_gdt_entry(0x00409800, 0x7c0001ff))
print(
generate_gdt_entry(limit=0x1ff, base=0x7c00, type=0b1000, granularity=0b0, dpl=0, present=1, s=1, operation_size=1))
Comments | NOTHING