思维之海

——在云端,寻找我的星匙。

ucore实验

ucore实验记录。

实验指导书

因为时间不够所以实验没来得及做了~建议留出足够时间。

实验0 环境

https://www.shiyanlou.com/courses/reports/1368923/

https://github.com/kiukotsu/ucore

https://zhanghuimeng.github.io/post/os-mooc-lecture-2-summary/

qemu的安装需要特别注意。

1
sudo apt-get install qemu

在完成安装以后,直接qemu可能找不到命令。运行以下语句进行链接:

1
sudo ln -s /usr/bin/qemu-system-i386 /usr/bin/qemu

实验环境(5种)

踩坑:x86架构的unbantu不能装在arm架构的树莓派上。

装虚拟机太慢。弃。

在树莓派上跑,gcc版本不对,装gcc遇到极大困难。弃。

使用了树莓派而window平台上搭载虚拟机的方法来做实验。

Raspbian需要更换软件下载源(更新源)。搜索即可。

OS系统:Raspbian。(基于Debian的操作系统,linux系)

准备在电脑上装ubantu

成功。

  • 网上下载ubuntu14.04 iso文件
  • 准备一个U(闪)盘
    • 利用SD Card Formatter 5.0.1 Setup(格式化工具) 进行格式化
    • Universal-USB(刻录工具) 将ubuntu14.04刻录到闪盘
    • 插入U盘,重启电脑
  • 在开机完成前,按快捷键进入BOOT模式
    • 在设置中找到,修改BOOT启动顺序,将从U盘硬件启动设到第1位
    • 保存设置,退出,开始装ubuntu
  • 实验环境(5种)一键安装相应软件
  • 开始做实验

关于understand:

  • 此处下载4.0版本
  • 安装教程:Here
  • registery code: D59E41A3360B
    E-mail: a123@163.com

实验1 启动

关于ucore实验一的资料查找

Makefile代码分析

操作系统 uCore Lab 1 含 Challenge 比较完整 and Here[sim]

ucore lab1 实验报告 比较通俗

uCore实验1笔记整理 比较通俗

等设备~到了~


**Caution:lab1的答案里面都有`report`文件,挺详细,记录参考答案。**

附录 lab1参考答案

[练习1]

[练习1.1] 操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中
每一条相关命令和命令参数的含义,以及说明命令导致的结果)

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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
| | | 其中关键的参数为
| | | -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字节。
| | | -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

[练习1.2] 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

从sign.c的代码来看,一个磁盘主引导扇区只有512字节。且
第510个(倒数第二个)字节是0x55,
第511个(倒数第一个)字节是0xAA。

[练习2]

[练习2.1] 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。

练习2可以单步跟踪,方法如下:

1 修改 lab1/tools/gdbinit,内容为:

1
2
set architecture i8086
target remote :1234

2 在 lab1目录下,执行

1
make debug

3 在看到gdb的调试界面(gdb)后,在gdb调试界面下执行如下命令

1
si

即可单步跟踪BIOS了。

4 在gdb界面下,可通过如下命令来看BIOS的代码

1
x /2i $pc  //显示当前eip处的汇编指令

[进一步的补充]

1
2
3
4
5
改写Makefile文件
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"

在调用qemu时增加-d in_asm -D q.log参数,便可以将运行的汇编指令保存在q.log中。
为防止qemu在gdb连接后立即开始执行,删除了tools/gdbinit中的continue行。

[练习2.2] 在初始化位置0x7c00 设置实地址断点,测试断点正常。

在tools/gdbinit结尾加上

1
2
3
4
5
   set architecture i8086  //设置当前调试的CPU是8086
b *0x7c00 //在0x7c00处设置断点。此地址是bootloader入口点地址,可看boot/bootasm.S的start地址处
c //continue简称,表示继续执行
x /2i $pc //显示当前eip处的汇编指令
set architecture i386 //设置当前调试的CPU是80386

运行”make debug”便可得到

1
2
3
4
5
6
7
8
9
10
11
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

[练习2.3] 在调用qemu 时增加-d in_asm -D q.log 参数,便可以将运行的汇编指令保存在q.log 中。
将执行的汇编代码与bootasm.S 和 bootblock.asm 进行比较,看看二者是否一致。

在tools/gdbinit结尾加上

1
2
3
b *0x7c00
c
x /10i $pc

便可以在q.log中读到”call bootmain”前执行的命令

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
----------------
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

其与bootasm.S和bootblock.asm中的代码相同。

[练习3]

分析bootloader 进入保护模式的过程。

%cs=0 $pc=0x7c00,进入后

首先清理环境:包括将flag置0和将段寄存器置0

1
2
3
4
5
6
7
.code16
cli
cld
xorw %ax, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss

开启A20:通过将键盘控制器上的A20线置于高电位,全部32条地址线可用,
可以访问4G的内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 ##

初始化GDT表:一个简单的GDT表和其描述符已经静态储存在引导区中,载入即可

1
lgdt gdtdesc

进入保护模式:通过将cr0寄存器PE位置1便开启了保护模式

1
2
3
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0

通过长跳转更新cs的基地址

1
2
3
 ljmp $PROT_MODE_CSEG, $protcseg
.code32
protcseg:

设置段寄存器,并建立堆栈

1
2
3
4
5
6
7
8
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

转到保护模式完成,进入boot主方法

1
call bootmain

[练习4]

分析bootloader加载ELF格式的OS的过程。

首先看readsect函数,
readsect从设备的第secno扇区读取数据到dst位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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为单位
}

readseg简单包装了readsect,可以从设备读取任意长度的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}
}

在bootmain函数中,

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
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);
}

[练习5]

实现函数调用堆栈跟踪函数

ss:ebp指向的堆栈位置储存着caller的ebp,以此为线索可以得到所有使用堆栈的函数ebp。
ss:ebp+4指向caller调用时的eip,ss:ebp+8等是(可能的)参数。

输出中,堆栈最深一层为

1
2
3
ebp:0x00007bf8 eip:0x00007d68 \
args:0x00000000 0x00000000 0x00000000 0x00007c4f
<unknow>: -- 0x00007d67 --

其对应的是第一个使用堆栈的函数,bootmain.c中的bootmain。
bootloader设置的堆栈从0x7c00开始,使用”call bootmain”转入bootmain函数。
call指令压栈,所以bootmain中ebp为0x7bf8。

[练习6]

完善中断初始化和处理

[练习6.1] 中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成位移,
两者联合便是中断处理程序的入口地址。

[练习6.2] 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。

见代码

[练习6.3] 请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数

见代码

[练习7]

增加syscall功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),
当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务

在idt_init中,将用户态调用SWITCH_TOK中断的权限打开。
SETGATE(idt[T_SWITCH_TOK], 1, KERNEL_CS, __vectors[T_SWITCH_TOK], 3);

在trap_dispatch中,将iret时会从堆栈弹出的段寄存器进行修改
对TO User

1
2
3
4
tf->tf_cs = USER_CS;
tf->tf_ds = USER_DS;
tf->tf_es = USER_DS;
tf->tf_ss = USER_DS;
对TO Kernel
1
2
3
tf->tf_cs = KERNEL_CS;
tf->tf_ds = KERNEL_DS;
tf->tf_es = KERNEL_DS;

在lab1_switch_to_user中,调用T_SWITCH_TOU中断。
注意从中断返回时,会多pop两位,并用这两位的值更新ss,sp,损坏堆栈。
所以要先把栈压两位,并在从中断返回后修复esp。

1
2
3
4
5
6
7
asm volatile (
"sub $0x8, %%esp \n"
"int %0 \n"
"movl %%ebp, %%esp"
:
: "i"(T_SWITCH_TOU)
);

在lab1_switch_to_kernel中,调用T_SWITCH_TOK中断。
注意从中断返回时,esp仍在TSS指示的堆栈中。所以要在从中断返回后修复esp。

1
2
3
4
5
6
asm volatile (
"int %0 \n"
"movl %%ebp, %%esp \n"
:
: "i"(T_SWITCH_TOK)
);

但这样不能正常输出文本。根据提示,在trap_dispatch中转User态时,将调用io所需权限降低。

1
tf->tf_eflags |= 0x3000;