内核头程序就是内核程序中的一小段汇编代码。内核的线性地址0xffff800000000000对应物理地址0处,内核程序的起始线性地址位 0xffff800000000000 + 0x100000处。

描述符和段结构信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// GDT Table
.section .data

.global GDT_Table

GDT_Table:
.quad 0x0000000000000000 // 0 null descriptor 00
.quad 0x0020980000000000 // 1 kernel code 64bit segment 08
.quad 0x0000920000000000 // 2 kernel code 64bit segment 10
.quad 0x0020f80000000000 // 3 user code 64bit segment 18
.quad 0x0000f20000000000 // 4 user data 64bit segment 20
.quad 0x00cf9a000000ffff // 5 kernel code 32bit segment 28
.quad 0x00cf92000000ffff // 6 kernel data 32bit segment 30
.fill 10, 8, 0 // 8~9 TSS(jmp one segment 7) in long-mode 128bit
GDT_END:

GDT_POINTER:
GDT_LIMIT: .word GDT_END - GDT_Table - 1
GDT_BASE: .quad GDT_Table

// IDT Table
.global IDT_Table

IDT_Table:
.fill 512, 8, 0
IDT_END:

IDT_POINTER:
IDT_LIMIT: .word IDT_END - IDT_Table - 1
IDT_BASE: .quad IDT_Table

// TSS64 Table
.global TSS64_Table

TSS64_Table:
.fill 13, 8, 0
TSS64_END:

TSS64_POINTER:
TSS64_LIMIT: .word TSS64_END - TSS64_Table - 1
TSS64_BASE: .quad TSS64_Table

这段程序将全局描述符GDT、中断描述符IDT、任务状态段TSS刚在内核程序的数据段内,并且手动配置全局描述符GDT内的各个段描述符。

通过伪指令.global来修饰标识符GDT_TableIDT_TableTSS64_Table表示这三个标识符可以被外部程序引用或访问。它可以保证本程序可以正确配置描述符,同时内核程序其他部分也能够操作这些表舒服表项。

初始化页表及页表项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// init page
.align 8
.org 0x1000

__PML4E:
// bit0,1,2 设置为1,允许读写,不允许用户模式操作
.quad 0x102007
.fill 255, 8, 0
.quad 0x102007
.fill 255, 8, 0

.org 0x2000

__PDPTE:
// bit0,1 设置为1,允许读写
.quad 0x103003
.fill 511, 8, 0

.org 0x3000
__PDE:
// bit0,1 设置为1,允许读写,bit7设置1,映射2MB
// 0
.quad 0x000083
.quad 0x200083
.quad 0x400083
.quad 0x600083
.quad 0x800083
// 10M
.quad 0xe0000083 // 0xa00000
.quad 0xe0200083
.quad 0xe0400083
.quad 0xe0600083
.quad 0xe0800083 // 0x1000000
.quad 0xe0a00083
.quad 0xe0c00083
.quad 0xe0e00083
// 16M
.fill 499, 8, 0

在64位的IA-32e模式下,页表可分为4个等级,每个页表项由原来的4B扩展到8B,而且分页机制处理提供4KB大小的物理页外,还提供2MB和1G大小的物理页。

名称 英文名称 英文简称 对应的地址位 表项名称
第四级页表 page map level 4 table PML4T 40到48,共9位 PML4E(Entry)
第三级页表 page directory pointer table PDPT 第31到39,共9位 PDPTE
第二级页表 page directory table PDT 第22到30,共9位 PDTE
页表 page table PT 第13到21位,共9位 PTE

.org 0x1000定位页目录,将页表置于内核指向头程序的0x1000偏移处,然后链接器再根据链接脚本描述,将内核执行头程序的起始线性地址设置在0xffff800000000000 + 0x100000处,因此可以推断处页目录的起始地址位于0xffff800000100000 + 0x1000处。此页表将线性地址0和0xffff800000100000映射为同一物理页以方便页表切换,即程序在配置页表前运行于线性地址0x100000处,经过跳转后运行于线性地址0xffff800000000000附近。

这段程序将前10MB的物理地址内存分别映射到线性地址0处和0xffff800000000000处,接着把物理地址0xe0000000开始的16MB内存映射到线性地址0xa00000处和0xffff800000a00000处,最后使用伪指令.fill将数值0填充到页表剩余的499个页表项里。

寄存器初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
.section .text

.globl _start

_start:
mov $0x10, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %ss
mov $0x7e00, %esp

// load GDTR
lgdt GDT_POINTER(%rip)

// load IDTR
lidt IDT_POINTER(%rip)

mov $0x10, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
mov %ax, %ss
movq $0x7e00, %rsp

// load cr3
movq $0x101000, %rax
movq %rax, %cr3
movq switch_seg(%rip), %rax
pushq $0x08
pushq %rax
lretq

switch_seg:
.quad entry64

entry64:
movq $0x10, %rax
movq %rax, %ds
movq %rax, %es
movq %rax, %gs
movq %rax, %ss
movq $0xffff800000007e00, %rsp

movq go_to_kernel(%rip), %rax
pushq $0x08
pushq %rax
lretq

.go_to_kernel:
.quad start_kernel

在GAS编译器中,使用标识符_start作为程序的默认起始位置,同时还要使用伪指令.globl_start标识符添加修饰。

lgdt GDT_POINTER(%rip)采用RIP-Relative寻址模式,这是为IA-32e模式新引入的寻址方式。基于 RIP 计算目标地址时,目标地址等于当前指令的下一条指令所在地址加上偏移量。

Screenshot_20220716_203811

表中displacement是一个有32位整数值,而目标地址值又依赖RIP寄存器(指令指针寄存器),那么displacement将提供RIP范围内2GB的寻址范围。

GAS编译器不支持远跳转JMP、调用CALL指令,所以只能借助lretq来间接跳转,此处先模仿远调用汇编代码lcall的执行过程,伪造了程序执行现场,并结合RIP-Relative寻址模式将段选择子和段内偏移保存到栈中,然后执行代码lretq恢复调用现场,即返回到目标代码段的程序地址中。借助汇编代码lretq跳转到模块entry64的起始地址处,从而完成了从线性地址0x100000向地址0xffff800000100000的切换工作

编译

1
2
3
head.o: head.S
gcc -E head.S > heas.s
as --64 -o head.o head.s