os_kernel_lab/labcodes_answer/lab1_result/report.md

551 lines
16 KiB
Markdown
Raw Normal View History

2015-03-09 21:44:22 +08:00
# Lab1 erport
2015-03-09 21:41:20 +08:00
2015-03-09 21:44:22 +08:00
## [练习1]
2015-01-30 20:20:30 +08:00
[练习1.1] 操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中
每一条相关命令和命令参数的含义,以及说明命令导致的结果)
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
bin/ucore.img
| 生成ucore.img的相关代码为
| $(UCOREIMG): $(kernel) $(bootblock)
| $(V)dd if=/dev/zero of=$@ count=10000
| $(V)dd if=$(bootblock) of=$@ conv=notrunc
| $(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc
|
| 为了生成ucore.img首先需要生成bootblock、kernel
|
|> bin/bootblock
| | 生成bootblock的相关代码为
| | $(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
| | @echo + ld $@
| | $(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ \
| | -o $(call toobj,bootblock)
| | @$(OBJDUMP) -S $(call objfile,bootblock) > \
| | $(call asmfile,bootblock)
| | @$(OBJCOPY) -S -O binary $(call objfile,bootblock) \
| | $(call outfile,bootblock)
| | @$(call totarget,sign) $(call outfile,bootblock) $(bootblock)
| |
| | 为了生成bootblock首先需要生成bootasm.o、bootmain.o、sign
| |
| |> obj/boot/bootasm.o, obj/boot/bootmain.o
| | | 生成bootasm.o,bootmain.o的相关makefile代码为
| | | bootfiles = $(call listf_cc,boot)
| | | $(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),\
| | | $(CFLAGS) -Os -nostdinc))
| | | 实际代码由宏批量生成
| | |
| | | 生成bootasm.o需要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
| | | 其中关键的参数为
2015-03-11 08:21:50 +08:00
| | | -ggdb 生成可供gdb使用的调试信息。这样才能用qemu+gdb来调试bootloader or ucore。
| | | -m32 生成适用于32位环境的代码。我们用的模拟硬件是32bit的80386所以ucore也要是32位的软件。
| | | -gstabs 生成stabs格式的调试信息。这样要ucore的monitor可以显示出便于开发者阅读的函数调用栈信息
| | | -nostdinc 不使用标准库。标准库是给应用程序用的我们是编译ucore内核OS内核是提供服务的所以所有的服务要自给自足。
| | | -fno-stack-protector 不生成用于检测缓冲区溢出的代码。这是for 应用程序的我们是编译内核ucore内核好像还用不到此功能。
| | | -Os 为减小代码大小而进行优化。根据硬件spec主引导扇区只有512字节我们写的简单bootloader的最终大小不能大于510字节。
2015-01-30 20:20:30 +08:00
| | | -I<dir> 添加搜索头文件的路径
| | |
| | | 生成bootmain.o需要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
| | | 新出现的关键参数有
| | | -fno-builtin 除非用__builtin_前缀
| | | 否则不进行builtin函数的优化
| |
| |> bin/sign
| | | 生成sign工具的makefile代码为
| | | $(call add_files_host,tools/sign.c,sign,sign)
| | | $(call create_target_host,sign,sign)
| | |
| | | 实际命令为
| | | 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
| |
| | 首先生成bootblock.o
| | ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 \
| | obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
| | 其中关键的参数为
| | -m <emulation> 模拟为i386上的连接器
| | -nostdlib 不使用标准库
| | -N 设置代码段和数据段均可读写
| | -e <entry> 指定入口
| | -Ttext 制定代码段开始位置
| |
| | 拷贝二进制代码bootblock.o到bootblock.out
| | objcopy -S -O binary obj/bootblock.o obj/bootblock.out
| | 其中关键的参数为
| | -S 移除所有符号和重定位信息
| | -O <bfdname> 指定输出格式
| |
| | 使用sign工具处理bootblock.out生成bootblock
| | bin/sign obj/bootblock.out bin/bootblock
|
|> bin/kernel
| | 生成kernel的相关代码为
| | $(kernel): tools/kernel.ld
| | $(kernel): $(KOBJS)
| | @echo + ld $@
| | $(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
| | @$(OBJDUMP) -S $@ > $(call asmfile,kernel)
| | @$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; \
| | /^$$/d' > $(call symfile,kernel)
| |
| | 为了生成kernel首先需要 kernel.ld init.o readline.o stdio.o kdebug.o
| | kmonitor.o panic.o clock.o console.o intr.o picirq.o trap.o
| | trapentry.o vectors.o pmm.o printfmt.o string.o
| | kernel.ld已存在
| |
| |> obj/kern/*/*.o
| | | 生成这些.o文件的相关makefile代码为
| | | $(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,\
| | | $(KCFLAGS))
| | | 这些.o生成方式和参数均类似仅举init.o为例其余不赘述
| |> obj/kern/init/init.o
| | | 编译需要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
| |
| | 生成kernel时makefile的几条指令中有@前缀的都不必需
| | 必需的命令只有
| | 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
| | 其中新出现的关键参数为
| | -T <scriptfile> 使
|
| 生成一个有10000个块的文件每个块默认512字节用0填充
| dd if=/dev/zero of=bin/ucore.img count=10000
|
| 把bootblock中的内容写到第一个块
| dd if=bin/bootblock of=bin/ucore.img conv=notrunc
|
| 从第二个块开始写kernel中的内容
| dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
[练习1.2] 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
从sign.c的代码来看一个磁盘主引导扇区只有512字节。且
第510个倒数第二个字节是0x55
第511个倒数第一个字节是0xAA。
2015-03-09 21:44:22 +08:00
## [练习2]
2015-01-30 20:20:30 +08:00
[练习2.1] 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。
练习2可以单步跟踪方法如下
1 修改 lab1/tools/gdbinit,内容为:
```
set architecture i8086
target remote :1234
```
2 在 lab1目录下执行
```
make debug
```
3 在看到gdb的调试界面(gdb)后在gdb调试界面下执行如下命令
```
si
```
即可单步跟踪BIOS了。
4 在gdb界面下可通过如下命令来看BIOS的代码
```
x /2i $pc //显示当前eip处的汇编指令
```
> [进一步的补充]
2015-03-09 21:41:20 +08:00
```
改写Makefile文件
2015-01-30 20:20:30 +08:00
debug: $(UCOREIMG)
$(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -parallel stdio -hda $< -serial null"
$(V)sleep 2
$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"
2015-03-09 21:41:20 +08:00
```
在调用qemu时增加`-d in_asm -D q.log`参数便可以将运行的汇编指令保存在q.log中。
为防止qemu在gdb连接后立即开始执行删除了`tools/gdbinit`中的`continue`行。
2015-01-30 20:20:30 +08:00
[练习2.2] 在初始化位置0x7c00 设置实地址断点,测试断点正常。
在tools/gdbinit结尾加上
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
set architecture i8086 //设置当前调试的CPU是8086
b *0x7c00 //在0x7c00处设置断点。此地址是bootloader入口点地址可看boot/bootasm.S的start地址处
c //continue简称表示继续执行
x /2i $pc //显示当前eip处的汇编指令
set architecture i386 //设置当前调试的CPU是80386
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
运行"make debug"便可得到
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
Breakpoint 2, 0x00007c00 in ?? ()
=> 0x7c00: cli
0x7c01: cld
0x7c02: xor %eax,%eax
0x7c04: mov %eax,%ds
0x7c06: mov %eax,%es
0x7c08: mov %eax,%ss
0x7c0a: in $0x64,%al
0x7c0c: test $0x2,%al
0x7c0e: jne 0x7c0a
0x7c10: mov $0xd1,%al
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
[练习2.3] 在调用qemu 时增加-d in_asm -D q.log 参数便可以将运行的汇编指令保存在q.log 中。
将执行的汇编代码与bootasm.S 和 bootblock.asm 进行比较,看看二者是否一致。
在tools/gdbinit结尾加上
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
b *0x7c00
c
x /10i $pc
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
便可以在q.log中读到"call bootmain"前执行的命令
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
----------------
IN:
0x00007c00: cli
----------------
IN:
0x00007c01: cld
0x00007c02: xor %ax,%ax
0x00007c04: mov %ax,%ds
0x00007c06: mov %ax,%es
0x00007c08: mov %ax,%ss
----------------
IN:
0x00007c0a: in $0x64,%al
----------------
IN:
0x00007c0c: test $0x2,%al
0x00007c0e: jne 0x7c0a
----------------
IN:
0x00007c10: mov $0xd1,%al
0x00007c12: out %al,$0x64
0x00007c14: in $0x64,%al
0x00007c16: test $0x2,%al
0x00007c18: jne 0x7c14
----------------
IN:
0x00007c1a: mov $0xdf,%al
0x00007c1c: out %al,$0x60
0x00007c1e: lgdtw 0x7c6c
0x00007c23: mov %cr0,%eax
0x00007c26: or $0x1,%eax
0x00007c2a: mov %eax,%cr0
----------------
IN:
0x00007c2d: ljmp $0x8,$0x7c32
----------------
IN:
0x00007c32: mov $0x10,%ax
0x00007c36: mov %eax,%ds
----------------
IN:
0x00007c38: mov %eax,%es
----------------
IN:
0x00007c3a: mov %eax,%fs
0x00007c3c: mov %eax,%gs
0x00007c3e: mov %eax,%ss
----------------
IN:
0x00007c40: mov $0x0,%ebp
----------------
IN:
0x00007c45: mov $0x7c00,%esp
0x00007c4a: call 0x7d0d
----------------
IN:
0x00007d0d: push %ebp
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
其与bootasm.S和bootblock.asm中的代码相同。
2015-03-09 21:44:22 +08:00
## [练习3]
2015-03-09 21:41:20 +08:00
分析bootloader 进入保护模式的过程。
2015-01-30 20:20:30 +08:00
2015-03-09 21:41:20 +08:00
从`%cs=0 $pc=0x7c00`,进入后
2015-01-30 20:20:30 +08:00
首先清理环境包括将flag置0和将段寄存器置0
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
.code16
cli
cld
xorw %ax, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
开启A20通过将键盘控制器上的A20线置于高电位全部32条地址线可用
可以访问4G的内存空间。
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
seta20.1: # 等待8042键盘控制器不忙
inb $0x64, %al #
testb $0x2, %al #
jnz seta20.1 #
movb $0xd1, %al # 发送写8042输出端口的指令
outb %al, $0x64 #
seta20.1: # 等待8042键盘控制器不忙
inb $0x64, %al #
testb $0x2, %al #
jnz seta20.1 #
movb $0xdf, %al # 打开A20
outb %al, $0x60 #
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
初始化GDT表一个简单的GDT表和其描述符已经静态储存在引导区中载入即可
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
lgdt gdtdesc
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
进入保护模式通过将cr0寄存器PE位置1便开启了保护模式
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
通过长跳转更新cs的基地址
2015-03-09 21:41:20 +08:00
```
ljmp $PROT_MODE_CSEG, $protcseg
2015-01-30 20:20:30 +08:00
.code32
protcseg:
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
设置段寄存器,并建立堆栈
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
movw $PROT_MODE_DSEG, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
movw %ax, %gs
movw %ax, %ss
movl $0x0, %ebp
movl $start, %esp
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
转到保护模式完成进入boot主方法
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
call bootmain
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
2015-03-09 21:44:22 +08:00
## [练习4]
2015-03-09 21:41:20 +08:00
分析bootloader加载ELF格式的OS的过程。
2015-01-30 20:20:30 +08:00
首先看readsect函数
2015-03-09 21:41:20 +08:00
`readsect`从设备的第secno扇区读取数据到dst位置
```
2015-01-30 20:20:30 +08:00
static void
readsect(void *dst, uint32_t secno) {
waitdisk();
outb(0x1F2, 1); // 设置读取扇区的数目为1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
// 上面四条指令联合制定了扇区号
// 在这4个字节线联合构成的32位参数中
// 29-31位强制设为1
// 28位(=0)表示访问"Disk 0"
// 0-27位是28位的偏移量
outb(0x1F7, 0x20); // 0x20命令读取扇区
waitdisk();
insl(0x1F0, dst, SECTSIZE / 4); // 读取到dst位置
// 幻数4因为这里以DW为单位
}
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
readseg简单包装了readsect可以从设备读取任意长度的内容。
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count;
va -= offset % SECTSIZE;
uint32_t secno = (offset / SECTSIZE) + 1;
// 加1因为0扇区被引导占用
// ELF文件从1扇区开始
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
在bootmain函数中
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
void
bootmain(void) {
// 首先读取ELF的头部
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// 通过储存在头部的幻数判断是否是合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// ELF头部有描述ELF文件应加载到内存什么位置的描述表
// 先将描述表的头地址存在ph
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
// 按照描述表将ELF文件中数据载入内存
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
// ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000
// 根据ELF头部储存的入口信息找到内核的入口
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1);
}
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
2015-03-09 21:44:22 +08:00
## [练习5]
实现函数调用堆栈跟踪函数
2015-01-30 20:20:30 +08:00
ss:ebp指向的堆栈位置储存着caller的ebp以此为线索可以得到所有使用堆栈的函数ebp。
ss:ebp+4指向caller调用时的eipss:ebp+8等是可能的参数。
输出中,堆栈最深一层为
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
ebp:0x00007bf8 eip:0x00007d68 \
args:0x00000000 0x00000000 0x00000000 0x00007c4f
<unknow>: -- 0x00007d67 --
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
其对应的是第一个使用堆栈的函数bootmain.c中的bootmain。
bootloader设置的堆栈从0x7c00开始使用"call bootmain"转入bootmain函数。
call指令压栈所以bootmain中ebp为0x7bf8。
2015-03-09 21:44:22 +08:00
## [练习6]
2015-03-09 21:41:20 +08:00
完善中断初始化和处理
2015-01-30 20:20:30 +08:00
[练习6.1] 中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
中断向量表一个表项占用8字节其中2-3字节是段选择子0-1字节和6-7字节拼成位移
两者联合便是中断处理程序的入口地址。
[练习6.2] 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。
2015-03-09 21:41:20 +08:00
2015-01-30 20:20:30 +08:00
见代码
[练习6.3] 请编程完善trap.c中的中断处理函数trap在对时钟中断进行处理的部分填写trap函数
2015-03-09 21:41:20 +08:00
2015-01-30 20:20:30 +08:00
见代码
2015-03-09 21:44:22 +08:00
## [练习7]
2015-03-09 21:41:20 +08:00
增加syscall功能即增加一用户态函数可执行一特定系统调用获得时钟计数值
2015-01-30 20:20:30 +08:00
当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务
在idt_init中将用户态调用SWITCH_TOK中断的权限打开。
SETGATE(idt[T_SWITCH_TOK], 1, KERNEL_CS, __vectors[T_SWITCH_TOK], 3);
在trap_dispatch中将iret时会从堆栈弹出的段寄存器进行修改
对TO User
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
tf->tf_cs = USER_CS;
tf->tf_ds = USER_DS;
tf->tf_es = USER_DS;
tf->tf_ss = USER_DS;
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
对TO Kernel
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
tf->tf_cs = KERNEL_CS;
tf->tf_ds = KERNEL_DS;
tf->tf_es = KERNEL_DS;
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
在lab1_switch_to_user中调用T_SWITCH_TOU中断。
注意从中断返回时会多pop两位并用这两位的值更新ss,sp损坏堆栈。
所以要先把栈压两位并在从中断返回后修复esp。
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
asm volatile (
"sub $0x8, %%esp \n"
"int %0 \n"
"movl %%ebp, %%esp"
:
: "i"(T_SWITCH_TOU)
);
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
在lab1_switch_to_kernel中调用T_SWITCH_TOK中断。
注意从中断返回时esp仍在TSS指示的堆栈中。所以要在从中断返回后修复esp。
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
asm volatile (
"int %0 \n"
"movl %%ebp, %%esp \n"
:
: "i"(T_SWITCH_TOK)
);
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
但这样不能正常输出文本。根据提示在trap_dispatch中转User态时将调用io所需权限降低。
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00
tf->tf_eflags |= 0x3000;
2015-03-09 21:41:20 +08:00
```
2015-01-30 20:20:30 +08:00