80386 模拟器源码分析 (4)
段描述符、段描述符表
段描述符 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-only001
Data, read/write010
Stack, read-only011
Stack, read/write100
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 位
参考:
- https://pdos.csail.mit.edu/6.828/2005/readings/i386/s05_01.htm
- http://ece-research.unm.edu/jimp/310/slides/micro_arch2.html
- https://tldp.org/LDP/khg/HyperNews/get/memory/80386mm.html
描述符表 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 0x00cf9a00
与 0x0000ffff 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。
其中具体 INDEX
为 0x007b >> 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 中了。