《操作系统》的实验代码。
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

469 lines
15 KiB

  1. Lab1 report
  2. [练习1]
  3. [练习1.1] 操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中
  4. 每一条相关命令和命令参数的含义,以及说明命令导致的结果)
  5. bin/ucore.img
  6. | 生成ucore.img的相关代码为
  7. | $(UCOREIMG): $(kernel) $(bootblock)
  8. | $(V)dd if=/dev/zero of=$@ count=10000
  9. | $(V)dd if=$(bootblock) of=$@ conv=notrunc
  10. | $(V)dd if=$(kernel) of=$@ seek=1 conv=notrunc
  11. |
  12. | 为了生成ucore.img,首先需要生成bootblock、kernel
  13. |
  14. |> bin/bootblock
  15. | | 生成bootblock的相关代码为
  16. | | $(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign)
  17. | | @echo + ld $@
  18. | | $(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^ \
  19. | | -o $(call toobj,bootblock)
  20. | | @$(OBJDUMP) -S $(call objfile,bootblock) > \
  21. | | $(call asmfile,bootblock)
  22. | | @$(OBJCOPY) -S -O binary $(call objfile,bootblock) \
  23. | | $(call outfile,bootblock)
  24. | | @$(call totarget,sign) $(call outfile,bootblock) $(bootblock)
  25. | |
  26. | | 为了生成bootblock,首先需要生成bootasm.o、bootmain.o、sign
  27. | |
  28. | |> obj/boot/bootasm.o, obj/boot/bootmain.o
  29. | | | 生成bootasm.o,bootmain.o的相关makefile代码为
  30. | | | bootfiles = $(call listf_cc,boot)
  31. | | | $(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),\
  32. | | | $(CFLAGS) -Os -nostdinc))
  33. | | | 实际代码由宏批量生成
  34. | | |
  35. | | | 生成bootasm.o需要bootasm.S
  36. | | | 实际命令为
  37. | | | gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs \
  38. | | | -nostdinc -fno-stack-protector -Ilibs/ -Os -nostdinc \
  39. | | | -c boot/bootasm.S -o obj/boot/bootasm.o
  40. | | | 其中关键的参数为
  41. | | | -ggdb 生成可供gdb使用的调试信息
  42. | | | -m32 生成适用于32位环境的代码
  43. | | | -gstabs 生成stabs格式的调试信息
  44. | | | -nostdinc 不使用标准库
  45. | | | -fno-stack-protector 不生成用于检测缓冲区溢出的代码
  46. | | | -Os 为减小代码大小而进行优化
  47. | | | -I<dir> 添加搜索头文件的路径
  48. | | |
  49. | | | 生成bootmain.o需要bootmain.c
  50. | | | 实际命令为
  51. | | | gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc \
  52. | | | -fno-stack-protector -Ilibs/ -Os -nostdinc \
  53. | | | -c boot/bootmain.c -o obj/boot/bootmain.o
  54. | | | 新出现的关键参数有
  55. | | | -fno-builtin 除非用__builtin_前缀,
  56. | | | 否则不进行builtin函数的优化
  57. | |
  58. | |> bin/sign
  59. | | | 生成sign工具的makefile代码为
  60. | | | $(call add_files_host,tools/sign.c,sign,sign)
  61. | | | $(call create_target_host,sign,sign)
  62. | | |
  63. | | | 实际命令为
  64. | | | gcc -Itools/ -g -Wall -O2 -c tools/sign.c \
  65. | | | -o obj/sign/tools/sign.o
  66. | | | gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign
  67. | |
  68. | | 首先生成bootblock.o
  69. | | ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 \
  70. | | obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
  71. | | 其中关键的参数为
  72. | | -m <emulation> 模拟为i386上的连接器
  73. | | -nostdlib 不使用标准库
  74. | | -N 设置代码段和数据段均可读写
  75. | | -e <entry> 指定入口
  76. | | -Ttext 制定代码段开始位置
  77. | |
  78. | | 拷贝二进制代码bootblock.o到bootblock.out
  79. | | objcopy -S -O binary obj/bootblock.o obj/bootblock.out
  80. | | 其中关键的参数为
  81. | | -S 移除所有符号和重定位信息
  82. | | -O <bfdname> 指定输出格式
  83. | |
  84. | | 使用sign工具处理bootblock.out,生成bootblock
  85. | | bin/sign obj/bootblock.out bin/bootblock
  86. |
  87. |> bin/kernel
  88. | | 生成kernel的相关代码为
  89. | | $(kernel): tools/kernel.ld
  90. | | $(kernel): $(KOBJS)
  91. | | @echo + ld $@
  92. | | $(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
  93. | | @$(OBJDUMP) -S $@ > $(call asmfile,kernel)
  94. | | @$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; \
  95. | | /^$$/d' > $(call symfile,kernel)
  96. | |
  97. | | 为了生成kernel,首先需要 kernel.ld init.o readline.o stdio.o kdebug.o
  98. | | kmonitor.o panic.o clock.o console.o intr.o picirq.o trap.o
  99. | | trapentry.o vectors.o pmm.o printfmt.o string.o
  100. | | kernel.ld已存在
  101. | |
  102. | |> obj/kern/*/*.o
  103. | | | 生成这些.o文件的相关makefile代码为
  104. | | | $(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,\
  105. | | | $(KCFLAGS))
  106. | | | 这些.o生成方式和参数均类似,仅举init.o为例,其余不赘述
  107. | |> obj/kern/init/init.o
  108. | | | 编译需要init.c
  109. | | | 实际命令为
  110. | | | gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 \
  111. | | | -gstabs -nostdinc -fno-stack-protector \
  112. | | | -Ilibs/ -Ikern/debug/ -Ikern/driver/ \
  113. | | | -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c \
  114. | | | -o obj/kern/init/init.o
  115. | |
  116. | | 生成kernel时,makefile的几条指令中有@前缀的都不必需
  117. | | 必需的命令只有
  118. | | ld -m elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel \
  119. | | obj/kern/init/init.o obj/kern/libs/readline.o \
  120. | | obj/kern/libs/stdio.o obj/kern/debug/kdebug.o \
  121. | | obj/kern/debug/kmonitor.o obj/kern/debug/panic.o \
  122. | | obj/kern/driver/clock.o obj/kern/driver/console.o \
  123. | | obj/kern/driver/intr.o obj/kern/driver/picirq.o \
  124. | | obj/kern/trap/trap.o obj/kern/trap/trapentry.o \
  125. | | obj/kern/trap/vectors.o obj/kern/mm/pmm.o \
  126. | | obj/libs/printfmt.o obj/libs/string.o
  127. | | 其中新出现的关键参数为
  128. | | -T <scriptfile> 让连接器使用指定的脚本
  129. |
  130. | 生成一个有10000个块的文件,每个块默认512字节,用0填充
  131. | dd if=/dev/zero of=bin/ucore.img count=10000
  132. |
  133. | 把bootblock中的内容写到第一个块
  134. | dd if=bin/bootblock of=bin/ucore.img conv=notrunc
  135. |
  136. | 从第二个块开始写kernel中的内容
  137. | dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
  138. [练习1.2] 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
  139. 从sign.c的代码来看,一个磁盘主引导扇区只有512字节。且
  140. 第510个(倒数第二个)字节是0x55,
  141. 第511个(倒数第一个)字节是0xAA。
  142. [练习2]
  143. [练习2.1] 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。
  144. 通过改写Makefile文件 ()
  145. debug: $(UCOREIMG)
  146. $(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D $(BINDIR)/q.log -parallel stdio -hda $< -serial null"
  147. $(V)sleep 2
  148. $(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"
  149. 在调用qemu时增加-d in_asm -D q.log参数,便可以将运行的汇编指令保存在q.log中。
  150. 为防止qemu在gdb连接后立即开始执行,删除了tools/gdbinit中的"continue"行。
  151. [练习2.2] 在初始化位置0x7c00 设置实地址断点,测试断点正常。
  152. 在tools/gdbinit结尾加上
  153. set architecture i8086 //设置当前调试的CPU是8086
  154. b *0x7c00 //在0x7c00处设置断点。此地址是bootloader入口点地址,可看boot/bootasm.S的start地址处
  155. c //continue简称,表示继续执行
  156. x /2i $pc //显示当前eip处的汇编指令
  157. set architecture i386 //设置当前调试的CPU是80386
  158. 运行"make debug"便可得到
  159. Breakpoint 2, 0x00007c00 in ?? ()
  160. => 0x7c00: cli
  161. 0x7c01: cld
  162. 0x7c02: xor %eax,%eax
  163. 0x7c04: mov %eax,%ds
  164. 0x7c06: mov %eax,%es
  165. 0x7c08: mov %eax,%ss
  166. 0x7c0a: in $0x64,%al
  167. 0x7c0c: test $0x2,%al
  168. 0x7c0e: jne 0x7c0a
  169. 0x7c10: mov $0xd1,%al
  170. [练习2.3] 在调用qemu 时增加-d in_asm -D q.log 参数,便可以将运行的汇编指令保存在q.log 中。
  171. 将执行的汇编代码与bootasm.S 和 bootblock.asm 进行比较,看看二者是否一致。
  172. 在tools/gdbinit结尾加上
  173. b *0x7c00
  174. c
  175. x /10i $pc
  176. 便可以在q.log中读到"call bootmain"前执行的命令
  177. ----------------
  178. IN:
  179. 0x00007c00: cli
  180. ----------------
  181. IN:
  182. 0x00007c01: cld
  183. 0x00007c02: xor %ax,%ax
  184. 0x00007c04: mov %ax,%ds
  185. 0x00007c06: mov %ax,%es
  186. 0x00007c08: mov %ax,%ss
  187. ----------------
  188. IN:
  189. 0x00007c0a: in $0x64,%al
  190. ----------------
  191. IN:
  192. 0x00007c0c: test $0x2,%al
  193. 0x00007c0e: jne 0x7c0a
  194. ----------------
  195. IN:
  196. 0x00007c10: mov $0xd1,%al
  197. 0x00007c12: out %al,$0x64
  198. 0x00007c14: in $0x64,%al
  199. 0x00007c16: test $0x2,%al
  200. 0x00007c18: jne 0x7c14
  201. ----------------
  202. IN:
  203. 0x00007c1a: mov $0xdf,%al
  204. 0x00007c1c: out %al,$0x60
  205. 0x00007c1e: lgdtw 0x7c6c
  206. 0x00007c23: mov %cr0,%eax
  207. 0x00007c26: or $0x1,%eax
  208. 0x00007c2a: mov %eax,%cr0
  209. ----------------
  210. IN:
  211. 0x00007c2d: ljmp $0x8,$0x7c32
  212. ----------------
  213. IN:
  214. 0x00007c32: mov $0x10,%ax
  215. 0x00007c36: mov %eax,%ds
  216. ----------------
  217. IN:
  218. 0x00007c38: mov %eax,%es
  219. ----------------
  220. IN:
  221. 0x00007c3a: mov %eax,%fs
  222. 0x00007c3c: mov %eax,%gs
  223. 0x00007c3e: mov %eax,%ss
  224. ----------------
  225. IN:
  226. 0x00007c40: mov $0x0,%ebp
  227. ----------------
  228. IN:
  229. 0x00007c45: mov $0x7c00,%esp
  230. 0x00007c4a: call 0x7d0d
  231. ----------------
  232. IN:
  233. 0x00007d0d: push %ebp
  234. 其与bootasm.S和bootblock.asm中的代码相同。
  235. [练习3] 分析bootloader 进入保护模式的过程。
  236. 从%cs=0 $pc=0x7c00,进入后
  237. 首先清理环境:包括将flag置0和将段寄存器置0
  238. .code16
  239. cli
  240. cld
  241. xorw %ax, %ax
  242. movw %ax, %ds
  243. movw %ax, %es
  244. movw %ax, %ss
  245. 开启A20:通过将键盘控制器上的A20线置于高电位,全部32条地址线可用,
  246. 可以访问4G的内存空间。
  247. seta20.1: # 等待8042键盘控制器不忙
  248. inb $0x64, %al #
  249. testb $0x2, %al #
  250. jnz seta20.1 #
  251. movb $0xd1, %al # 发送写8042输出端口的指令
  252. outb %al, $0x64 #
  253. seta20.1: # 等待8042键盘控制器不忙
  254. inb $0x64, %al #
  255. testb $0x2, %al #
  256. jnz seta20.1 #
  257. movb $0xdf, %al # 打开A20
  258. outb %al, $0x60 #
  259. 初始化GDT表:一个简单的GDT表和其描述符已经静态储存在引导区中,载入即可
  260. lgdt gdtdesc
  261. 进入保护模式:通过将cr0寄存器PE位置1便开启了保护模式
  262. movl %cr0, %eax
  263. orl $CR0_PE_ON, %eax
  264. movl %eax, %cr0
  265. 通过长跳转更新cs的基地址
  266. ljmp $PROT_MODE_CSEG, $protcseg
  267. .code32
  268. protcseg:
  269. 设置段寄存器,并建立堆栈
  270. movw $PROT_MODE_DSEG, %ax
  271. movw %ax, %ds
  272. movw %ax, %es
  273. movw %ax, %fs
  274. movw %ax, %gs
  275. movw %ax, %ss
  276. movl $0x0, %ebp
  277. movl $start, %esp
  278. 转到保护模式完成,进入boot主方法
  279. call bootmain
  280. [练习4] :分析bootloader加载ELF格式的OS的过程。
  281. 首先看readsect函数,
  282. readsect从设备的第secno扇区读取数据到dst位置
  283. static void
  284. readsect(void *dst, uint32_t secno) {
  285. waitdisk();
  286. outb(0x1F2, 1); // 设置读取扇区的数目为1
  287. outb(0x1F3, secno & 0xFF);
  288. outb(0x1F4, (secno >> 8) & 0xFF);
  289. outb(0x1F5, (secno >> 16) & 0xFF);
  290. outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
  291. // 上面四条指令联合制定了扇区号
  292. // 在这4个字节线联合构成的32位参数中
  293. // 29-31位强制设为1
  294. // 28位(=0)表示访问"Disk 0"
  295. // 0-27位是28位的偏移量
  296. outb(0x1F7, 0x20); // 0x20命令,读取扇区
  297. waitdisk();
  298. insl(0x1F0, dst, SECTSIZE / 4); // 读取到dst位置,
  299. // 幻数4因为这里以DW为单位
  300. }
  301. readseg简单包装了readsect,可以从设备读取任意长度的内容。
  302. static void
  303. readseg(uintptr_t va, uint32_t count, uint32_t offset) {
  304. uintptr_t end_va = va + count;
  305. va -= offset % SECTSIZE;
  306. uint32_t secno = (offset / SECTSIZE) + 1;
  307. // 加1因为0扇区被引导占用
  308. // ELF文件从1扇区开始
  309. for (; va < end_va; va += SECTSIZE, secno ++) {
  310. readsect((void *)va, secno);
  311. }
  312. }
  313. 在bootmain函数中,
  314. void
  315. bootmain(void) {
  316. // 首先读取ELF的头部
  317. readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
  318. // 通过储存在头部的幻数判断是否是合法的ELF文件
  319. if (ELFHDR->e_magic != ELF_MAGIC) {
  320. goto bad;
  321. }
  322. struct proghdr *ph, *eph;
  323. // ELF头部有描述ELF文件应加载到内存什么位置的描述表,
  324. // 先将描述表的头地址存在ph
  325. ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
  326. eph = ph + ELFHDR->e_phnum;
  327. // 按照描述表将ELF文件中数据载入内存
  328. for (; ph < eph; ph ++) {
  329. readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
  330. }
  331. // ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
  332. // ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000
  333. // 根据ELF头部储存的入口信息,找到内核的入口
  334. ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
  335. bad:
  336. outw(0x8A00, 0x8A00);
  337. outw(0x8A00, 0x8E00);
  338. while (1);
  339. }
  340. [练习5] 实现函数调用堆栈跟踪函数
  341. ss:ebp指向的堆栈位置储存着caller的ebp,以此为线索可以得到所有使用堆栈的函数ebp。
  342. ss:ebp+4指向caller调用时的eip,ss:ebp+8等是(可能的)参数。
  343. 输出中,堆栈最深一层为
  344. ebp:0x00007bf8 eip:0x00007d68 \
  345. args:0x00000000 0x00000000 0x00000000 0x00007c4f
  346. <unknow>: -- 0x00007d67 --
  347. 其对应的是第一个使用堆栈的函数,bootmain.c中的bootmain。
  348. bootloader设置的堆栈从0x7c00开始,使用"call bootmain"转入bootmain函数。
  349. call指令压栈,所以bootmain中ebp为0x7bf8。
  350. [练习6] 完善中断初始化和处理
  351. [练习6.1] 中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
  352. 中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成位移,
  353. 两者联合便是中断处理程序的入口地址。
  354. [练习6.2] 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。
  355. 见代码
  356. [练习6.3] 请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数
  357. 见代码
  358. [练习7] 增加syscall功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),
  359. 当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务
  360. 在idt_init中,将用户态调用SWITCH_TOK中断的权限打开。
  361. SETGATE(idt[T_SWITCH_TOK], 1, KERNEL_CS, __vectors[T_SWITCH_TOK], 3);
  362. 在trap_dispatch中,将iret时会从堆栈弹出的段寄存器进行修改
  363. 对TO User
  364. tf->tf_cs = USER_CS;
  365. tf->tf_ds = USER_DS;
  366. tf->tf_es = USER_DS;
  367. tf->tf_ss = USER_DS;
  368. 对TO Kernel
  369. tf->tf_cs = KERNEL_CS;
  370. tf->tf_ds = KERNEL_DS;
  371. tf->tf_es = KERNEL_DS;
  372. 在lab1_switch_to_user中,调用T_SWITCH_TOU中断。
  373. 注意从中断返回时,会多pop两位,并用这两位的值更新ss,sp,损坏堆栈。
  374. 所以要先把栈压两位,并在从中断返回后修复esp。
  375. asm volatile (
  376. "sub $0x8, %%esp \n"
  377. "int %0 \n"
  378. "movl %%ebp, %%esp"
  379. :
  380. : "i"(T_SWITCH_TOU)
  381. );
  382. 在lab1_switch_to_kernel中,调用T_SWITCH_TOK中断。
  383. 注意从中断返回时,esp仍在TSS指示的堆栈中。所以要在从中断返回后修复esp。
  384. asm volatile (
  385. "int %0 \n"
  386. "movl %%ebp, %%esp \n"
  387. :
  388. : "i"(T_SWITCH_TOK)
  389. );
  390. 但这样不能正常输出文本。根据提示,在trap_dispatch中转User态时,将调用io所需权限降低。
  391. tf->tf_eflags |= 0x3000;