目录
ucore lab1 report
这个实验报告暂时还只是草稿,其中还存在格式错误,部分实验的报告还没有写完。
exercise 1: 生成ucore的过程
通过make V=输出的命令研究ucore生成的过程。
下面的命令是make实际执行的命令(23~24行除外)。
1 gcc -Ikern/init/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/ke
2 gcc -Ikern/libs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/k
3 gcc -Ikern/libs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o ob
4 gcc -Ikern/debug/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/panic.c -o obj
5 gcc -Ikern/debug/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kdebug.c -o ob
6 gcc -Ikern/debug/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kmonitor.c -o
7 gcc -Ikern/driver/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/clock.c -o o
8 gcc -Ikern/driver/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/console.c -o
9 gcc -Ikern/driver/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/picirq.c -o
10 gcc -Ikern/driver/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/intr.c -o ob
11 gcc -Ikern/trap/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trap.c -o obj/ke
12 gcc -Ikern/trap/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/vectors.S -o obj
13 gcc -Ikern/trap/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trapentry.S -o o
14 gcc -Ikern/mm/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/mm/pmm.c -o obj/kern/mm
15 gcc -Ilibs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/string.c -o obj/libs/string.o
16 gcc -Ilibs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/printfmt.c -o obj/libs/printfmt.o
17 ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel obj/kern/init/init.o obj/kern/libs/stdio.o obj/kern/libs/readline.o obj/kern/debug/panic.o obj/kern/debug/kdebug.o obj/kern/
18 gcc -Iboot/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o
19 gcc -Iboot/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o
20 gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o
21 gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign
22 ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
23 'obj/bootblock.out' size: 496 bytes
24 build 512 bytes boot sector: 'bin/bootblock' success!
25 dd if=/dev/zero of=bin/ucore.img count=10000
26 dd if=bin/bootblock of=bin/ucore.img conv=notrunc
27 dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
编译过程
编译ucore和编译应用程序的过程相同,但是:
- 不使用任何外部的库,比如C标准库;
- 不使用动态链接,不生成pic(position-independent code)代码;
- 不查找标准库头文件;
具体参数分析:
- 链接相关
- -no-stdlib: 不链接标准库和C初始化函数
由于ucore是操作系统,不应该使用外部的库,而且应该自己指定程序的入口,所以用这个选项。
- -fno-buitlin: 对于非__buitlin_开头的函数,不使用对应的GCC内置(built in)函数
除了外部的库,GCC内部内嵌的有库libgcc.a,这个库总是自动链接(即使使用了-no-stdlib选项)。大量的标准库函数都有对应的内置版本,比如strcpy,对应有__builtin_strcpy,调用strcpy时GCC实际调用__builtin_strcpy。ucore既需要内置函数实现的C语言特性(比如va_list),又希望自己实现部分标准库函数(比如strcpy),并且在调用时使用自定义版本而非内置版本。
- -no-stdinc: 不搜索系统默认的头文件目录
ucore不使用外部库,只是用libgcc.a和自定义的标准库。如果不开启这个选项,GCC会搜索到系统的标准库头文件,而非自定义的标准库头文件。
- -fno-PIC: 不生成位置无关代码(position-independent code)
GCC默认动态链接,生成pic代码。ucore不进行动态链接,不生成pic代码。
- 调试相关
- -ggdb: 生成GDB专用格式的调试信息
ucore使用GDB调试,生成GDB专用的调试信息可以最大限度的增强GDB的调试能力。
- -gstabs: 生成stabs格式的调试信息
ucore中内置了调试内核的函数,比如联系5完成的print_stackframe()函数,这些函数解析stabs格式的调试信息。
- 目标平台
- -m32: 生成IA-32位代码
ucore是运行在IA-32处理器上的操作系统,所以要用这个选项。
- 代码生成规格
- -fno-stack-protector: 禁用stack-protector
现代GCC编译时会使用stack-protector(在数组末尾添加金丝雀值(哨兵),如果金丝雀值被更改就会终止程序)防止缓冲区溢出。ucore添加这个选项可能是想简化内存布局或者减少运行时的内存消耗。
链接过程
bootloader和kernel都是ucore项目的一部分,但却是两个独立的程序(执行文件),所以分别链接。
ld选项:
- -m: 指定可执行文件格式与目标平台
ucore使用elf_i386格式。
- -nostdlib: 禁止链接标准库和C程序初始函数
ucore是操作系统,不依赖与外部函数库。
- -T: 指定linker script
生成kernel的过程中使用tools/kernel.ld作为linker script,其中包含了设置kernel内部各段的相关信息;生成bootblock的过程中使用tools/boot.ld作为linker scrip,其中包含程序入口点。
- -Ttext: 设置可执行文件的.text节的绝对地址
在生成bootblock的过程中使用了这个选项,使bootloader的.text节起始地址为绝对地址0x7c00。
- -e: 设置可执行文件入口点
在生成bootblock的过程中使用了这个选项,指定start标记为bootloader的入口点。
- -N: 将可执行文件的代码节和数据节设置为读/写、禁止代码节向页大小对齐、禁止动态链接
_
启动扇区的检验和生成
完成了全部的编译链接后,还需要生成启动扇区。使用tools/sign和objdump对目标文件bootblock.o进行修改得到。
在生成了bootblock.o,
- 调用objdump提取了bootblock.o中的代码输出到bootblock.out中;
- 调用tools/sign检查bootblock.out是否小于510字节,如果大于510字节就报错;
- tools/sign将bootblock.out输出到一个bin/bootblock,并将文件最后两个字节设置为0x55,0xAA。
@$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
@$(OBJDUMP) -t $(call objfile,bootblock) | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,bootblock)
@$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
@$(call totarget,sign) $(call outfile,bootblock) $(bootblock)
$(call create_target,bootblock)
通过以上步骤得到的bin/bootblock包含了启动扇区中的全部内容。
虚拟硬盘的制作
通过dd命令制作虚拟硬盘。
先将虚拟硬盘第一个扇区区初始化为0,在把bin/bootblock写入其中制成启动扇区,然后再将bin/kernel写入之后的扇区中。
dd if=/dev/zero of=bin/ucore.img count=10000
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
exercise 3: bootloader进入保护模式的过程
练习一要求分析bootloader进入保护模式的过程。bootloader由三个文件实现,分别为asm.h(包含常量,初始值)、bootasm.S(开启A20,设置GDT,并进入保护模式)、bootmain.c(加载kernel并执行)。这个练习仅涉及asm.h和bootasm.S。
bootasm.S完成了A20的开启、GDT的设置、内核栈的设置后进入保护模式,并call到bootmain执行,进行kernel的加载工作。
常量与宏
bootasm.S中定义了三个常量:
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector
.set CR0_PE_ON, 0x1 # protected mode enable flag
CRO_PE_ON作为切换到保护模式时修改CRO的掩码。
PROT_MODE_CSEG和PROT_MODE_DSEG分别作为内核代码段、数据段的选择子,指向GDT[1]和GDT[2],RPL(requested privilege level)均设置为ring 0。
asm.h中定义了描述段描述符的宏。
A20的开启
bootloader最初运行在16位实模式下。
bootloader首先关闭对中断的响应,并将段寄存器ds,es,ss置零,之后开始A20(在8042芯片上)的开启。
开启A20的思路很简单:等到8042芯片空闲时,将A20位设置为1即可。由于8042的设计,向8042写入数据被才分成向0x64端口发送写命令和向0x60端口发送数据两步。
最终操作步骤如下:
- 等待8042芯片空闲
- 向P2端口发送写命令
- 再次等待8042芯片空闲
- 像P2端口写入数据
等待8042芯片空闲
等待8042空闲通过循环读取8042的状态寄存器到CPU寄存器al,判断al是否为0x2(芯片初始系统状态)来实现。
seta20.1:
inb $0x64, %al # read status information into al
testb $0x2, %al
jnz seta20.1
像8042写入数据
向端口0x64发送写命令
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
向端口0x60写入数据
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
经过以上介绍的四步,A20就被开启了,系统可以使用高于1M的线性空间。
设置GDT
GDT被设置为4字节对齐,仅定义了GDT[0]、GDT[1](内核代码段)、GDT[2](内核数据段)。
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel(executable and read-only)
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel(writable)
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
```
其中的的宏定义在asm.h中,asm.h比较短,直接摘抄出来。
```
/* Normal segment */
#define SEG_NULLASM \
.word 0, 0; \
.byte 0, 0, 0, 0
#define SEG_ASM(type,base,lim) \
.word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \
.byte (((base) >> 16) & 0xff), (0x90 | (type)), \
(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
/* Application segment type bits */
#define STA_X 0x8 // Executable segment
#define STA_E 0x4 // Expand down (non-executable segments)
#define STA_C 0x4 // Conforming code segment (executable only)
#define STA_W 0x2 // Writeable (non-executable segments)
#define STA_R 0x2 // Readable (executable segments)
#define STA_A 0x1 // Accessed
分析以上两段代码可以发现:
- GDT[0]设置为空,未使用;
- GDT[1],GDT[2]分别作为内核代码段、数据段;
- 内核代码段、数据段都被设置为最长4G,且基地址均为0x00;
内核代码段、数据段被设置成这样是有意削弱(避免)X86分段内存模型的影响,在32位的CPU上实现类似64位上的平坦内存模型,方便页机制的实现。
加载GDT
lgdt gdtdesc
切换到保护模式
开启A20、设置并加载GDT后,bootloader已经完成了切换到保护模式的全部准备工作。
开启保护模式仅需要打开控制寄存器CR0中相应的标志位,通过异或之前定义的掩码CRO_PE_ON实现。
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
打开模式后,CPU真正进入了32位模式,默认使用分段内存模型,段寄存器中必须存放相应的选择子。
首先设置cs和 eip的值,通过ljmp实现。ljmp仅仅是跳转到了下一条指令(procseg处)
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg
.code32
procseg:
设置栈并跳转到bootmain加载kernel
将所有的寄存器设置为PROT_MODE_DSEG(指向内核数据段)。将栈设置为为0x00~0x7c00(bootloader之下都是栈的空间),然后使用call指令执行bootmain,开始加载kernel。
bootmain函数正常情况不会返回,如果返回肯定是bootloader产生错误,进入死循环。
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
movl $0x0, %ebp
movl $start, %esp
call bootmain
# bootmain should not return. if return, loop forever
spin:
jmp spin
栈和bootloader的位置关系如下:
+--------------------+
+ +
+ +
+ +
+ +
+ not in use +
+ +
+ +
+ +
+ +
+ +
+--------------------+
+ +
+ +
+ +
+ bootloader +
+ +
+ +
+ +
+ +
+--------------------+ <-- 0x7c00 beginning of bootloader
+ | +
+ | +
+ stack | +
+ | +
+ | +
+ V +
+--------------------+ <-- 0x00
至此,bootlader完成了初始化和各种设置,bootasm.S完成任务,剩下的任务交给bootmain函数完成。
execrise 4: bootloader加载ELF格式的OS的过程
在bootmain函数中,bootloader加载kernel到内存中。
先分析bootmain函数如何加载ELF格式的kernel,再分析具体读取磁盘的机制。
加载ELF格式的kernel
为了理解如何加载ELF格式的kernel,就必须知道ELF格式的结构。在这里只涉及到了ELF32格式中的已链接的可执行文件,所以忽略共享目标文件和可重定位目标文件。
实验手册给的资料不足,参考《深入理解计算机系统》第三版第7章《链接》图7-13“典型的ELF可执行目标文件”
图中ELF头和段头部对应的ucore中的数据类型为struct elfhdr和struct proghdr,均定义在libs/elf.h中。这里只摘抄在这里用到的结构体成员。
#define ELF_MAGIC 0x464C457FU // "\x7FELF" in little endian
struct elfhdr {
...
uint32_t e_magic; // must equal ELF_MAGIC
...
uint32_t e_entry; // entry point of executable
...
uint16_t e_phnum; // number of entries in program header or 0
...
}
struct proghdr {
...
uint32_t p_offset; // file offset of segment
uint32_t p_va; // virtual address to map segment
...
uint32_t p_memsz; // size of segment in memory (bigger if contains bss)
...
}
知道这些信息后,加载ELF格式的kernel的机制就很清楚了:
- 通过判断ELF_MAGIC是否等于ELF头中的e_magic确定kernel是否是合法的ELF32格式
- 段头部表是struct proghdr的数组,数组元素个数为e_phnum(在ELF头中)
- 内核各段应该加载到对应段头部中记录的p_va处,大小为p_memsz,位于于kernel文件的p_offset处。通过readseg实现。
bootmain逻辑流:
- 读取kernel的ELF头和段头部表加载到ELFHDR(0x10000)处
- 判断kerne是否是合法的ELF格式,如果不是则死循环
- 如果格式合法就根据kernel中的ELF头和段头部表中的信息将kernel各段加载到适当的位置
- 加载完成后,通过函数调用跳转到entry point
具体实现代码如下:
uintptr_t其实就是uint32_t代表32位地址,定义在lib/defs.h中。/**/风格注释是我添加的,//风格注释是源代码中的。
/* bootmain - the entry of bootloader */
void
bootmain(void) {
// read the 1st page off disk
/* read SECTSIZE*8 bytes at offset 0 from kernel into virtual address ELFHDR(0x10000) */
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
/* if the executable is not ELF, goto bad(infinite loop) */
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// load each program segment (ignores ph flags)
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
/* eph is the position after the end of the last program header */
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph ++) {
/* load executable code from kernel into corresponding virtual address according to infomation in program header */
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// call the entry point from the ELF header
// note: does not return
/* use function call to transfer control to kernel */
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
/* do nothing */
while (1);
}
读取硬盘的机制
ucore为了简化硬盘访问的实现,使用的是可编程IO(programed IO)方式,并且假设硬盘使用IDE。一个IDE通道可以接两个硬盘(主盘/从盘),ucore只读取主盘的数据。
基本思路:
- 等待硬盘空闲
- 发送要读取的扇区号为硬盘
- 等待硬盘空闲
- 读取扇区数据到某个内存位置
ucore通过readsg()、readsec()和insl()三个函数实现读取硬盘。readsg()和readsec()定义在bootmain.c中,insl()定义在libs/x86.h头文件中。
/* 从读取扇区号secno的数据到内存dst处 */
static void
readsect(void *dst, uint32_t secno);
/* 从kernel文件偏移offset处读取count字节到内存va处 */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset);
/* 内联汇编。从prot端口读取cnt*4字节数据到内存addr处 */
static inline
void insl(uint32_t port, void *addr, int cnt) __attribute__((always_inline));
函数调用关系: readsg() --> readsec() --> insl()
insl()实现:
*/当%ecx(cnt)不为0使,从端口%edi(port)处读取4字节到(%edi)(内存addr)处*/
static inline void
insl(uint32_t port, void *addr, int cnt) {
/* edi - addr
ecx - cnt
edi - port
*/
asm volatile (
"cld;"
"repne; insl;"
: "=D" (addr), "=c" (cnt)
: "d" (port), "0" (addr), "1" (cnt)
: "memory", "cc");
}
insl()函数是一个辅助函数,在读取之前还需要像硬盘发送读命令,读取硬盘扇区由readsect()完成。
磁盘IO地址与功能(摘自ucore实验手册)如下:
static void
readsect(void *dst, uint32_t secno) {
// wait for disk to be ready
waitdisk();
outb(0x1F2, 1); // count = 1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); /* 掩码0xF将扇区号28~31位置零,仅保留24~27位。
异或0xE0避开了IO端口第4位,目的是读取主盘。*/
outb(0x1F7, 0x20); // cmd 0x20 - read sectors
// wait for disk to be ready
waitdisk();
// read a sector
insl(0x1F0, dst, SECTSIZE / 4); /*SECTISE被定义为512。insl每次读取4字节,所以SECTISE要除4
}
最终bootmain读取磁盘使用的是readseg()函数。
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
/* uintptr_t在/libs/defs.h中定义为uint32_t;
uintprt_t避免了C语言指针运算时的自动伸展;
*/
uintptr_t end_va = va + count;
// round down to sector boundary
va -= offset % SECTSIZE;
// translate from bytes to sectors; kernel starts at sector 1
uint32_t secno = (offset / SECTSIZE) + 1;
// If this is too slow, we could read lots of sectors at a time.
// We'd write more to memory than asked, but it doesn't matter --
// we load in increasing order.
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
readseg()函数提供的功能是从磁盘读取count字节数据,但是读取硬盘却以512字节的扇区为为单位,所以实际读入的字节数很可能大于要求读入的字节数。
readseg()函数实际写入的内存地址是地址参数向下舍入到512字节的边界,所以实际写入的内存地址很可能低于要求写入的内存地址。
完成函数调用跟踪函数
这个练习要求我们kern/debug/kdebug.c中的print_stackframe()函数。在print——stackframe()函数中,注释已经给出了完整的步骤,难度不大,只要理解x86函数调用过程就可以做出来。
栈的设置是在启动阶段(跳转到bootmain函数之前完成的),所以初始时栈结构如下:
+--------------------+ <-- 0x7c00
+ bootmain frame + |g
+--------------------+ |r
+ + |o
+ + |w
+ + V
+ +
+ +
+--------------------+ <-- 0x00
bootmain函数是不会返回的,所有的函数栈帧都在bootmain函数下面。在调用bootmain()之前,%ebp被设置了0,这个值被压入了函数栈中,这个特殊的%ebp是跟踪函数栈帧的结束标志。
x86函数调用的具体过程(同特权级):
- 把函数参数压入栈中
- 把函数返回地址(%es,%eip)压入栈中
- 把旧的%ebp压入栈中,并把%ebp的值改为当前%esp
跟踪函数堆栈就是利用了x86函数调用后堆栈的结构。
/* stack */
+ %cs +
+ %eip +
+--------------------+<----------------------------------------------+
+ ... + local variables of calling function +
+--------------------+ +
+ parameters + parameters passed by calling function +
+--------------------+ <-----+ +
+ %cs + + +
+--------------------+ +---- return address +
+ %eip + + +
+--------------------+ <-----+ +
+ old %ebp +-----------------------------------------------+
+--------------------+<----------------------------------------------+
+ ... + local variables of calling function +
+--------------------+ +
+ parameters + parameters passed by calling function +
+--------------------+ <-----+ +
+ %cs + + +
+--------------------+ +---- return address +
+ %eip + + +
+--------------------+ <-----+ +
+ old %ebp +-----------------------------------------------+
+--------------------+ <----- current %ebp
+ ... +
+ +
在上面的图示是某时刻堆栈的布局。
每次函数调用,%ebp都被压入栈中,并修改为当时栈顶指针%esp的值。栈中保存的%ebp总是指向上一次保存的%ebp处,而且栈中保存的%ebp上面4字节处就是调用函数的返回地址,返回地址之上就是被调用函数的参数。
利用当前的%ebp值和栈中保存的%ebp值就可以实现跟踪堆栈的功能。
print_stackframe(void)函数要求了实现的具体步骤,所以最终补全的代码跟答案结构是完全一样的,这里直接给出了去掉步骤要求的答案代码。
void
print_stackframe(void) {
uint32_t ebp = read_ebp(), eip = read_eip();
int i, j;
for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {
cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);
uint32_t *args = (uint32_t *)ebp + 2;
for (j = 0; j < 4; j ++) {
cprintf("0x%08x ", args[j]);
}
cprintf("\n");
print_debuginfo(eip - 1);
eip = ((uint32_t *)ebp)[1];
ebp = ((uint32_t *)ebp)[0];
}
}
在print_stackframe中对调试信息的解析和显示由print_debuginfo()函数完成。这个函数接受一个代码段中地址,并显示相应的调试信息。
这个函数似乎有bug,只要把print_stackframe()函数的实现稍作改变,%eip的值偏移几个字节,就会无法正确分析出代码在kdebug.c文件中的位置,其他栈帧中函数所在文件似乎不受影响。
完善中断初始化和处理
发生中断后,x86根据终端去IDT中查找相应的描述符,并跳转到描述符指向的中断处理例程执行。
进行中断初始化和处理,就相当于设置IDT中的描述符和描述符指向的中断处理例程。
IDT描述符结构(摘自intel开发手册)如下:
IDT表项占8个字节,其中0~31位、47~63位代表中断处理例程的入口。