调试分析 Linux 0.00 引导程序

请简述 head.s 的工作原理

boot.s 通过 jmpi 0,8 刷新控制寄存器进入保护模式并跳转至 0x0008:0x0000(CS:EIP) 即物理地址 0x00000000 处进入 head.s 程序。head.s 首先初始化段寄存器 DS、SS,堆栈指针 esp 指向 head.s 中事先预留的 init_stack,随后调用函数 setup_idt、setup_gdt 初始化 IDT、GDT 表及 IDTR、GDTR。其中 IDT 的 256 个描述符被设置为指向函数 ignore_int 的中断门,该函数负责向显示终端打印字符 ‘C’,GDT 中设置了 8 个描述符,分别为空描述符[0]、内核代码段描述符[1]、内核数据段描述符[2]、显示终端所需描述符[3]、任务段描述符 TSS0[4]、LDT 描述符 LDT0[5]、任务段描述符 TSS1[6]、LDT 描述符 LDT1[7]。随后,重新设置 DS、ES 等段寄存器以确保其使用新设置的 GDT 并重置 SS:ESP 指向原始堆栈的栈底以清空堆栈,紧接着通过 outb 指令设置 8253 芯片时钟中断信号频率为 100 HZ。在此之后,改写 IDT 表中的 0x08 及 0x80 号描述符分别将其设置为时钟中断及系统中断(系统调用)。在正确设置寄存器 EFLAGS、TR、LDT 后打开中断。为模拟曾经由用户程序 task0 发出系统调用进入内核,手动在内核栈中压入 task0 对应 SS、ESP、EFLAGS、CS:EIP,随后使用 iret 指令隐式进入 task0 代码起始处执行。task0、task1 循环使用系统调用 int 0x80 分别在屏幕中打印字符 ‘A’、’B’,每当时钟中断到来时由 IDT 中 0x08 号指向的中断门陷入 timer_interrupt 中断处理函数,中断处理函数负责交替切换任务 0、1 并正确设置对应任务所需段寄存器,随后使用 iret 隐式跳转至目标任务代码继续执行。如图 1,最终效果呈现为任务 0、1 交替执行,屏幕中循环交替出现由若干个连续 ‘A’、’B’ 所组成的字符串。

图1

请记录 head.s 的内存分布状况,写明每个数据段,代码段,栈段的起始与终止的内存地址

Symbol Type Start Addr End Addr
startup_32 Code 0x00000000 0x000000ad
setup_gdt Code 0x000000ad 0x000000b5
setup_idt Code 0x000000b5 0x000000e5
write_char Code 0x000000e5 0x00000114
ignore_int Code 0x00000114 0x00000130
timer_interrupt Code 0x0000012a 0x00000166
system_interrupt Code 0x00000166 0x0000017d
current Data 0x0000017d 0x00000181
scr_loc Data 0x00000181 0x00000185
lidt_opcode Data 0x00000186 0x0000018c
lgdt_opcode Data 0x0000018c 0x00000192
idt Data 0x00000198 0x00000998
gdt Data 0x00000998 0x000009d8
end_gdt Stack 0x000009d8 0x00000bd8
init_stack Data 0x00000bd8 0x00000bde
ldt0 Data 0x00000be0 0x00000bf8
tss0 Data 0x00000bf8 0x00000c60
krn_stk0 Stack 0x00000c60 0x00000e60
ldt1 Data 0x00000e60 0x00000e78
tss1 Data 0x00000e78 0x00000ee0
krn_stk1 Stack 0x00000ee0 0x000010e0
task0 Code 0x000010e0 0x000010f4
task1 Code 0x000010f4 0x00001108
usr_stk1 Stack 0x00001108 0x00001308

简述 head.s 57 至 62 行在做什么?

如图 2,x86 架构在执行中断及系统调用时,会隐式将相关寄存器压栈以保存现场。对于特权级改变的中断及系统调用,硬件会依次向栈中压入调用前的 SS、ESP、EFLAGS、CS、EIP 寄存器并更新 SS、ESP、EFLAGS、CS、EIP 至中断处理函数起始状态,函数执行后使用 iret 命令隐式弹栈恢复 SS、ESP、EFLAGS、CS、EIP 至调用前状态。为实现用户与内核间的隔离,需使用 iret 指令模拟曾经由用户程序 task0 发出系统调用进入内核,因此需手动在内核栈中压入 task0 起始状态对应 SS、ESP、EFLAGS、CS:EIP,随后使用 iret 指令进入 task0 代码起始处执行。
图2

简述 iret 执行后, pc 如何找到下一条指令?

图3

如图 3,iret 执行前(内核)栈中被压入 SS、ESP、EFLAGS、CS、EIP,单步执行后如图 4 所示,CS:EIP 被设置为先前栈中压入的 0x000f:0x000010e0,指向 task0 代码起始地址处。

图4

记录 iret 执行前后,栈是如何变化的?

图5

如图 5、6,SS:ESP 由 0x10:0x00000bc4 变为先前栈中压入的 0x0017:0x00000bd8,指向 task0 用户栈栈底(同时也是先前内核程序所用栈)。

图6

当任务进行系统调用时,即 int 0x80 时,记录栈的变化情况。

图7

如图 7、8,task0 在执行 int 0x80 前,将欲打印字符 ‘A’ 的 ASCII 码 65 放入 eax 寄存器中作为 int 0x80 中断处理函数 system_interrupt 的参数,task0 内核栈krn_stk0 的栈顶 0x00000E60 附近未压入值。

图8

如图 9,执行 int 0x80 中断指令后,SS:ESP 切换至 TSS0 中预设的 0x10:0x00000e60 (krn_stk0) 并压入 SS、ESP、EFLAGS、CS、EIP,对应栈中的值 0x0017、0x00000bd8、0x00000246、0x000f、0x000010eb。

图9

如图 10,由 iret 指令返回时,SS、ESP、EFLAGS、CS、EIP 隐式弹栈填充对应寄存器,指向 int 0x80 下条指令 movl $0xfff, %ecx 地址处执行。

图10

最终效果如图 11,bochs 终端处首字符由 ‘B’ 变为 ‘A’。

图11