段描述符、段描述符表

段描述符 Segment Descriptor

来自 MIT

来自 UNM

段描述符共 64 位

  • [0..15] 16 位,为 Segment Limit
    • 和 [48..51] 的 4 位加在一起,共 20 位 Limit
  • [16..39] 24 位,为 Segment Base
    • 和 [56..63] 的 8 位加在一起,共 32 位 Base
  • [40..47] 8 位,为 Access Rights 访问权限
    • A 1 位,Accessed 缩写
      • 表示是否被访问过,A=0 表示没有,A=1 表示被访问过
    • TYPE 3 位,类型
      • 000 Data, read-only
      • 001 Data, read/write
      • 010 Stack, read-only
      • 011 Stack, read/write
      • 100 Code, execute-only,非一致代码段
      • 101 Code, execute/read,非一致代码段
      • 110 Code, execute-only, conforming, 一致代码段
      • 111 Code, execute/read, conforming,一致代码段
        • 第二位的 01 表示是否为一致代码段,也就是能否被低级权限代码执行的意思。
    • S 1 位,System 缩写
      • 表示 S=0 表示系统描述,S=1 表示代码、数据或堆栈描述
    • DPL 2 位,Descriptor Privilege Level 缩写
      • 表示权限级别,0 为特权,3 为最低。
    • P 1 位,Present 缩写
      • 表示当前描述是否有效,P=0 为无效(操作系统可自行扩展),P=1 为有效(仅对 CPU)
  • [48..51] 4 位,为 Limit 扩展字节,与前面加一起共 20 位
  • [52..55] 4 位,为 Segment Descriptors 段描述
    • U 用户自定义位
    • X Intel 保留位
    • D 指令位数,D=0 为 16 位,D=1 为 32 位
    • G 段大小,G=0 为 1b ~ 1MB,G=1 为 4KB ~ 4GB
  • [56..63] 8 位,为 Base 扩展字节,与前面加一起共 32 位

参考:

描述符表 Descriptor Tables

描述符表分为 Global Descriptor Table(全局描述符表) 与 Local Descriptor Table (局部描述符表)。

描述符表,本质上就是一个 8 字节(64位)的数组。

GDT 和 LDT 在内存中的地址,分别通过 GDTR(Global Descriptor Table Register) 与 LDTR(Local Descriptor Table Register)来指定。

  • GDT 的第一个为空,用于表示 null selector,从而避免自动转义。

在汇编中,通过 LGDT (Load Global Descriptor Table)SGDT (Store Global Descriptor Table) 指令来读写 GDTR。对于 LDTR,同理有 LLDT 与 SLDT 指令。

根据 LGDT 的描述:

IF OperandSize = 16
THEN GDTR.Limit:Base := m16:24 (* 24 bits of base loaded *)
ELSE GDTR.Limit:Base := m16:32;
FI;
  • 如果操作符为 16 位,则共有 limit(16)+ base(24)共 40 位
  • 如果操作符为其他位(如:32),则共有 limit(16)+ base(32)共 48 位
    • 下面的汇编示例中,就是 48 位

对于 LLDT,则有可能是 r/m16

参考:

测试 DT

Test Local Descriptor, read it by Assembly.

SGDT/SLDT(读取 DT)

编写汇编,参考 i386(3),不同的汇编需要不同的汇编工具,以下代码请使用 nasm(如果你使用的 gcc 过不了的情况)。

section	.text
	global _start
_start:
	SGDT	[gdtcopy]
	SLDT	[ldtcopy]
section	.data
gdtcopy times 8 db 'x'
ldtcopy times 8 db 'x'
# 启动虚拟机
# M1 mac 上,virtualbox 还是测试版,无法使用,用 qemu 来模拟
# 并开启网络方便 ssh,不然 qemu 自带的无法复制,粘贴,很难受
$ qemu-system-x86_64 \
  -drive "file=./ubuntu.img.qcow2,format=qcow2"\
  -m 2G \
  -smp 2 \
  -net user,hostfwd=tcp::10022-:22 \
  -net nic

# 连接到 qemu 虚拟机
$ ssh user@localhost -p10002

# 编辑 asm 文件
$ vi dt.asm

# vi 删除所有
(vi):%d
# 粘贴代码
# 保存
(vi):wq

# 编译
$ nasm -f elf dt.asm
# 连接
$ ld -m elf_i386 -s -o dt dt.o

# 调试
$ lldb dt

# 设置入口断点,因为无法 b main 来断点
(lldb) process launch --stop-at-entry
->  0x8049000 <+0>: sgdtl  0x804a000
    0x8049007 <+7>: sldtw  0x804a008

# 复制 lldb 提示编译后的那个地址
(lldb) x 0x804a000
0x0804a000: 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78 78  xxxxxxxxxxxxxxxx

# 下一步
(lldb) n

# GDTR (12 * 4 = 48bits)  7f 00 00 c0 03 00
# LDTR (4 * 4 = 8bits)    00 00
(lldb) x 0x804a000
0x0804a000: 7f 00 00 c0 03 00 78 78 00 00 78 78 78 78 78 78  ......xx..xxxxxx

LGDT(写入 GDT)

写入 GDT 一般由操作系统完成,比如在 Linux 0.11 中,由 boot/head.s,在更新版本的 Linux 中,由 arch/x86/kernel/head_32.S 设置。

DGDT 样例

参考 Linux 0.11

_gdt:	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x00c09a0000000fff	/* 16Mb */
	.quad 0x00c0920000000fff	/* 16Mb */
	.quad 0x0000000000000000	/* TEMPORARY - don't use */
	.fill 252,8,0			/* space for LDT's and TSS's etc */

Linux 6

SYM_DATA_START(boot_gdt)
	.fill GDT_ENTRY_BOOT_CS,8,0
	.quad 0x00cf9a000000ffff	/* kernel 4GB code at 0x00000000 */
	.quad 0x00cf92000000ffff	/* kernel 4GB data at 0x00000000 */

在 QEMU 中添加 monitor 选项,通过 info registers 查看 i386 的所有寄存器

(qemu) info registers

CPU#0
EAX=c1034280 EBX=00000000 ECX=00000001 EDX=c1b06000
ESI=00000000 EDI=00000000 EBP=c1b07f70 ESP=c1b07f70
EIP=c105e575 EFL=00200246 [---Z-P-] CPL=0 II=0 A20=1 SMM=0 HLT=1
ES =007b 00000000 ffffffff 00cff300 DPL=3 DS   [-WA]
CS =0060 00000000 ffffffff 00cf9a00 DPL=0 CS32 [-R-]
SS =0068 00000000 ffffffff 00c09300 DPL=0 DS   [-WA]
DS =007b 00000000 ffffffff 00cff300 DPL=3 DS   [-WA]
FS =00d8 34b1b000 ffffffff 008f9300 DPL=0 DS16 [-WA]
GS =00e0 f67ce8c0 00000018 00409100 DPL=0 DS   [--A]
LDT=0000 00000000 00000000 00008200 DPL=0 LDT
TR =0080 f67cc740 0000206b 00008900 DPL=0 TSS32-avl
GDT=     f67c4000 000000ff
IDT=     fffbb000 000007ff
CR0=80050033 CR2=09651c28 CR3=3588bca0 CR4=000006b0
...

由于 GDT 的格式如下:GDTR.Limit:Base := m16:32;

上述中的 GDT 的值为 GDT= f67c4000 000000ff,所以 BASE 地址为 0xf67c4000

通过内存 x 命令直接读取 32 位地址。其中 0x0000ffff 0x00cf9a000x0000ffff 0x00cf9300 等,可与上述的 Linux 内核代码相对应。

(qemu) x/64x 0xf67c4000
f67c4000: 0x00000000 0x00000000 0x00000000 0x00000000
f67c4010: 0x00000000 0x00000000 0x00000000 0x00000000
f67c4020: 0x00000000 0x00000000 0x00000000 0x00000000
f67c4030: 0x00000000 0x00000000 0x00000000 0x00000000
f67c4040: 0x00000000 0x00000000 0x00000000 0x00000000
f67c4050: 0x00000000 0x00000000 0x00000000 0x00000000
f67c4060: 0x0000ffff 0x00cf9a00 0x0000ffff 0x00cf9300
f67c4070: 0x0000ffff 0x00cffa00 0x0000ffff 0x00cff300
...

段映射样例,比如上述代码中,DS 为以下值,其中 007b 属于 16 位的 visible selector,后面几位属于 CPU 处理的 HIDDEN DESCRIPTOR。

其中具体 INDEX0x007b >> 3,并按照每个 Descriptor 为 8 字节(64 位)算,所以,以下 DS 样例的 Segment Descriptor 物理地址为:

[BASE] + [INDEX] * 8 = 0xf67c4000 + (0x7b >> 3) * 8 = 0xf67c4078

具体的命令如下:

(qemu) info registers
...
DS =007b 00000000 ffffffff 00cff300 DPL=3 DS   [-WA]
...

(qemu) x/12x 0xf67c4078
f67c4078: 0x0000ffff 0x00cff300 0xc740206b 0xf6008b7c
f67c4088: 0x00000000 0x00000000 0x0000ffff 0x00409a00
f67c4098: 0x0000ffff 0x00009a00 0x0000ffff 0x00009200

可以看到,相关的 Segment Descriptor 已经被加载到 DS 中的 HIDDEN Descriptor 中了。

References