内核头程序就是内核程序中的一小段汇编代码。内核的线性地址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_Table、IDT_Table、TSS64_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 计算目标地址时,目标地址等于当前指令的下一条指令所在地址加上偏移量。

表中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
|