前言
最近觉得自己之前蛮多基础课学的并不咋滴,便想再补补。前段时间突然看到清华的操作系统实验,于是乎就打算试试,一边学一边做实验,然后通过博客来记录记录。
实验内容
lab1 中包含一个 bootloader 和一个 OS。这个 bootloader 可以切换到 X86 保护模式,能够读磁盘并加载 ELF 执行文件格式,并显示字符。而这 lab1 中的 OS 只是一个可以处理时钟中断和显示字符的幼儿园级别 OS。
练习
练习1:理解通过make生成执行文件的过程。
1.操作系统镜像文件ucore.img是如何一步一步生成的?
打开命令行,执行以下命令
cd /home/moocos/ucore_lab/labcodes_answer/lab1_result
make V=
结果如下
# 构建 Kernel 文件
+ cc kern/init/init.c
gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o
+ cc kern/libs/readline.c
gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o obj/kern/libs/readline.o
+ cc kern/libs/stdio.c
gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/kern/libs/stdio.o
+ cc kern/debug/kdebug.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o
+ cc kern/debug/kmonitor.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kmonitor.c -o obj/kern/debug/kmonitor.o
+ cc kern/debug/panic.c
gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/panic.c -o obj/kern/debug/panic.o
+ cc kern/driver/clock.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/clock.c -o obj/kern/driver/clock.o
+ cc kern/driver/console.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/console.c -o obj/kern/driver/console.o
+ cc kern/driver/intr.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/intr.c -o obj/kern/driver/intr.o
+ cc kern/driver/picirq.c
gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/picirq.c -o obj/kern/driver/picirq.o
+ cc kern/trap/trap.c
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trap.c -o obj/kern/trap/trap.o
+ cc kern/trap/trapentry.S
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trapentry.S -o obj/kern/trap/trapentry.o
+ cc kern/trap/vectors.S
gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/vectors.S -o obj/kern/trap/vectors.o
+ cc kern/mm/pmm.c
gcc -Ikern/mm/ -fno-builtin -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/pmm.o
+ cc libs/printfmt.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/printfmt.c -o obj/libs/printfmt.o
+ cc libs/string.c
gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -c libs/string.c -o obj/libs/string.o
+ ld bin/kernel
ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel obj/kern/init/init.o obj/kern/libs/readline.o obj/kern/libs/stdio.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/debug/panic.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/intr.o obj/kern/driver/picirq.o obj/kern/trap/trap.o obj/kern/trap/trapentry.o obj/kern/trap/vectors.o obj/kern/mm/pmm.o obj/libs/printfmt.o obj/libs/string.o
# 构建 BootLoader
+ cc boot/bootasm.S
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o
+ cc boot/bootmain.c
gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o
+ cc tools/sign.c
# 使用 gcc 将 sign.c 编译成可执行文件
gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o
gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign
# 使用 ld 命令链接 bootasm.o、bootmain.o 至 bootblock.out
+ ld bin/bootblock
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
'obj/bootblock.out' size: 488 bytes
build 512 bytes boot sector: 'bin/bootblock' success!
# 构建 ucore.img
# 使用 dd 工具创建 ucore.img 空文件夹
dd if=/dev/zero of=bin/ucore.img count=10000
10000+0 records in
10000+0 records out
5120000 bytes (5.1 MB) copied, 0.113584 s, 45.1 MB/s
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
1+0 records in
1+0 records out
512 bytes (512 B) copied, 0.0018245 s, 281 kB/s
# 使用dd工具将文件bin/bootblock写入bin/ucore.img, 参数conv=notrunc表示不截断输出文件
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
146+1 records in
146+1 records out
74923 bytes (75 kB) copied, 0.00668055 s, 11.2 MB/s
由以上输出大致为
- 编译了若干内核文件,构建出内核 kernel
- 生成 bootblock 引导程序
- 生成 ucore.img 虚拟磁盘
2.一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
在课程中有提示可通过 tools 目录下的 sign.c 文件了解,因此,查看 sign.c 文件
#include
#include
#include
#include
int main(int argc, char *argv[]) {
struct stat st;
if (argc != 3) {
fprintf(stderr, "Usage:
由以上代码大致可知
- 大小为 512 字节,空余部分用 0 填充
- 文件内容不超过 510 bytes
- 最后两位为 0x55 0xAA
练习2:使用qemu执行并调试lab1中的软件
- 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。
- 在初始化位置 0x7c00 设置实地址断点,测试断点正常。
- 从 0x7c00 开始跟踪代码运行,将单步跟踪反汇编得到的代码与 bootasm.S 和 bootblock.asm 进行比较。
- 自己找一个 bootloader 或内核中的代码位置,设置断点并进行测试。
修改 tools/gdbinit 如下
set architecture i8086
target remote :1234
在 /home/moocos/ucore_lab/labcodes_answer/lab1_result 目录下执行命令 make debug
分别输入 i r cs 、 i r eip 以及 x /2i 0xffff0 可以看到如下结果
可知:
- CS 中地址为 0xF000, PC 中地址为 0xFFF0
- 由课程内容可知,第一条指令执行位置在 0xffff0 处,并且为长跳转指令
- BIOS 实例存储在 cs:ip 为 0xf000:0xe05b 处
接着,修改 gdbinit 如下
file obj/bootblock.o
set architecture i8086
target remote :1234
b *0x7c00
continue
继续 make debug
可发现 0x7C00 为主引导程序的入口地址,代码与 bootasm.S 一致。
练习3:分析bootloader进入保护模式的过程。
BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
提示:需要阅读小节“保护模式和分段机制”和lab1/boot/bootasm.S源码,了解如何从实模式切换到保护模式,需要了解:
- 为何开启A20,以及如何开启A20
- 如何初始化GDT表
- 如何使能和进入保护模式
1.为何开启A20,以及如何开启A20
先来说说 CPU 是如何寻址获取数据,i8086 处理器数据总线是 16 bit ,地址总线是 20 bit,寄存器 16 bit,因此,CPU 只能访问 1MB 以内的空间。CPU 想获取数据,需对 segment 左移 4 位,再加上 offest,最终形成一个 20bit 的地址
address = segment << 4 | offset
因此,理论上 20bit 地址可以访问 1MB 的内存空间。但在实模式下,这 20bit 的地址可以访问 (0x00000 ~ (0xffff0 + 0xffff = 0x10ffef)) 的存空间,大于 1MB。虽然大于 1MB,但对于 8086 来说,是没有任何影响,但是对于现在有更宽的地址总线的 CPU 来说,就会出现问题。在实模式下,这些 CPU 就能访问超过 1MB 的空间。为了解决这个问题,CPU 中添加了一个可控制 A20 地址线的模块,通过这个模块我们在实模式下将第 20 bit 的地址线限制为 0,这样 CPU 就无法访问超过 1MB 的空间了。
具体代码在 bootasm.S 文件中,然而没怎么看懂 orz
# Enable A20:
# For backwards compatibility with the earliest PCs, physical
# address line 20 is tied low, so that addresses higher than
# 1MB wrap around to zero by default. This code undoes this.
seta20.1:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.1
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
2.如何初始化GDT表
GDT(Global Descriptor Table, 全局描述表)。同实模式一样,在保护模式下,对内存的访问仍采用短地址加偏移地址的方式。其内存的管理方式有两种,段模式和页模式。在保护模式下,对于一个段的描述包括:Base Address,Limit(段的最大长度),Access(权限),这三个数据加在一起被放在一个 64 bit 的数据结构中,被称为段描述符。而由于寄存器为 16 bit,很明显,我们无法直接通过 16 bit 长度的寄存器来直接使用 64 bit 的段描述符。而对此的解决方案便是将这些段描述符放入一个全局数组中,将段寄存器中的值作为下标索引(段寄存器中的高 13 bit 的内容作为索引)来间接引用。而这个全局数组便是 GDT。
GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以 Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过 LGDT 指令将 GDT 的入口地址装入此积存器,从此以后,CPU 就根据此积存器中的内容作为 GDT 的入口来访问GDT了。
代码中关于 GDT 表的定义以及初始化
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)
bootasm.S 文件
lgdt gdtdesc
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt
3.如何使能和进入保护模式
开启 A20,初始化 GDT 后,将控制寄存器 CR0 的 PE 置为 1 即可
movl %cr0, %eax
orl 0x1, %eax
movl %eax, %cr0
由此,可总结出 bootloader 从实模式进入保护模式的过程
- bootloader 开始运行在实模式,物理地址为 0x7C00
- bootloader 关闭所有中断,方向标志位复位,ds,es,ss段寄存器清零
- 打开 A20 使之能够使用高位地址线
- 使用 lgdt 指令把 GDT 描述符表的大小和起始地址存入 GDT 寄存器,修改寄存器CR0的最低位(orl $CR0PEON, %eax)完成从实模式到保护模式的转换,通过 ljmp 指令跳转到32位指令模式
- 进入保护模式后,设置ds,es,fs,gs,ss段寄存器,堆栈指针,便可以进入 c 程序 bootmain
练习4:分析bootloader加载ELF格式的OS的过程
通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,
- bootloader如何读取硬盘扇区的?
- bootloader是如何加载ELF格式的OS?
-
定义指针 ELFHDR,指向 0x10000
-
读取 8 个扇区大小的 ELF 头到 0x10000
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
-
检验文件是否符合格式
// is this a valid ELF? if (ELFHDR->e_magic != ELF_MAGIC) { goto bad; }
-
循环调用readseg函数加载每一个程序段
-
跳转到操作系统入口
练习5:实现函数调用堆栈跟踪函数
我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。
void
print_stackframe(void) {
/* LAB1 YOUR CODE : STEP 1 */
/* (1) call read_ebp() to get the value of ebp. the type is (uint32_t);
* (2) call read_eip() to get the value of eip. the type is (uint32_t);
* (3) from 0 .. STACKFRAME_DEPTH
* (3.1) printf value of ebp, eip
* (3.2) (uint32_t)calling arguments [0..4] = the contents in address (unit32_t)ebp +2 [0..4]
* (3.3) cprintf("\n");
* (3.4) call print_debuginfo(eip-1) to print the C calling function name and line number, etc.
* (3.5) popup a calling stackframe
* NOTICE: the calling funciton's return addr eip = ss:[ebp+4]
* the calling funciton's ebp = ss:[ebp]
*/
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*)eip)[1];
ebp = ((uint32_t*)ebp)[0];
}
}
在 lab1 目录下,命令行执行 make qemu
练习6:完善中断初始化和处理
请完成编码工作和回答如下问题:
- 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
- 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
- 请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。
1.中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
- 一个表项占 8 个字节
2.请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init
void idt_init(void) {
extern uintptr_t __vectors[];
int i;
for(i = 0; i < 256; i ++){
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
lidt(&idt_pd);
}
3.请编程完善trap.c中的中断处理函数trap
if (++ticks == TICK_NUM) {
print_ticks();
ticks = 0;
}
扩展练习 Challenge 1
扩展proj4,增加syscall功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务。
在中断表中,可以看到 T_SWITCH_TOU 和 T_SWITCH_TOK,一个是切换到用户态,另一个是切换回内核态。切换时只需要修改段寄存器即可。
static void
switch_to_user(struct trapframe *tf){
if((tf->tf_cs & 3) == 3) return;
tf->tf_ds = tf->tf_es = tf->tf_fs = tf->tf_gs = tf->tf_ss = USER_DS;
tf->tf_cs = USER_CS;
tf->tf_eflags |= FL_IOPL_3;
}
static void
switch_to_kernel(struct trapframe *tf) {
if ((tf->tf_cs & 3) == 0) return;
tf->tf_ds = tf->tf_es = tf->tf_fs = tf->tf_gs = tf->tf_ss = KERNEL_DS;
tf->tf_cs = KERNEL_CS;
tf->tf_eflags &= ~FL_IOPL_3;
}
...
...
case T_SWITCH_TOU:
switch_to_user(tf);
break;
case T_SWITCH_TOK:
switch_to_kernel(tf);
break;
这样的话,只要触发 T_SWITCH_TOU 和 T_SWITCH_TOK 编号的中断, CPU 指令流就会通过 ISR 执行到这里,并进行内核态和用户态的切换。
接下来只需要在 kern/init/init.c 中将 lab1_switch_test() 解注释,然后实现两个函数即可。
static void
lab1_switch_to_user(void) {
//LAB1 CHALLENGE 1 : TODO
asm volatile (
"sub $0x8, %%esp \n"
"int %0 \n"
"movl %%ebp, %%esp"
:
: "i"(T_SWITCH_TOU)
);
}
static void
lab1_switch_to_kernel(void) {
//LAB1 CHALLENGE 1 : TODO
asm volatile (
"int %0 \n"
"movl %%ebp, %%esp \n"
:
: "i"(T_SWITCH_TOK)
);
}
扩展练习 Challenge 2
用键盘实现用户模式内核模式切换。具体目标是:“键盘输入3时切换到用户模式,键盘输入0时切换到内核模式”。 基本思路是借鉴软中断(syscall功能)的代码,并且把trap.c中软中断处理的设置语句拿过来。
击键会触发中断,对其的处理在 IRQ_KBD case 处,直接判断是不是 '0' 或 '3' 即可。
c = cons_getc();
switch (c) {
case '0':
switch_to_kernel(tf);
print_trapframe(tf);
break;
case '3':
switch_to_user(tf);
print_trapframe(tf);
break;
}
cprintf("kbd [%03d] %c\n", c, c);
break;
小节
感觉 lab1 跟计组和汇编挂钩蛮多,做了快一周,之前这两门课就学的不认真,很多东西都忘的差不多了,做起实验来真的是要命,各种看不懂 orz。感觉自己这两年摸鱼摸得有点多。。。。
参考
-
https://www.cnblogs.com/lsgxeva/p/8948153.html
-
https://bravey.github.io/2019-09-08-%E5%AE%9E%E6%A8%A1%E5%BC%8F%E5%AF%BB%E5%9D%80%E6%96%B9%E5%BC%8F.html#%E5%AE%9E%E6%A8%A1%E5%BC%8F%E5%AF%BB%E5%9D%80%E6%96%B9%E5%BC%8F
-
https://zhuanlan.zhihu.com/p/25867829
-
https://wiki.osdev.org/IDT
-
https://xr1s.me/2018/05/15/ucore-lab1-report/
-
https://www.cnblogs.com/ECJTUACM-873284962/p/11178427.html