lab1的实验练习答案
2016-06-28 10:48
337 查看
# Lab1 report ## [练习1] [练习1.1] 操作系统镜像文件 tos.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中 每一条相关命令和命令参数的含义,以及说明命令导致的结果)bin/tos.img| 生成tos.img的相关代码为| (TOSIMG):(kernel) (bootblock)|(V)dd if=/dev/zero of=@count=10000|(V)dd if=(bootblock)of=@ conv=notrunc| (V)ddif=(kernel) of=@seek=1conv=notrunc||为了生成tos.img,首先需要生成bootblock、kernel||>bin/bootblock||生成bootblock的相关代码为||(bootblock): (calltoobj,(bootfiles)) | (calltotarget,sign)||@echo+ld@| | (V)(LD) (LDFLAGS)−N−estart−Ttext0x7C00^ \| | -o (calltoobj,bootblock)||@(OBJDUMP) -S (callobjfile,bootblock)> ||(call asmfile,bootblock)| | @(OBJCOPY)−S−Obinary(call objfile,bootblock) \| | (calloutfile,bootblock)||@(call totarget,sign) (calloutfile,bootblock)(bootblock)| || | 为了生成bootblock,首先需要生成bootasm.o、bootmain.o、sign| || |> obj/boot/bootasm.o, obj/boot/bootmain.o| | | 生成bootasm.o,bootmain.o的相关makefile代码为| | | bootfiles = (calllistfcc,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 tos。| | | -m32 生成适用于32位环境的代码。我们用的模拟硬件是32bit的80386,所以tos也要是32位的软件。| | | -gstabs 生成stabs格式的调试信息。这样要tos的monitor可以显示出便于开发者阅读的函数调用栈信息| | | -nostdinc 不使用标准库。标准库是给应用程序用的,我们是编译tos内核,OS内核是提供服务的,所以所有的服务要自给自足。| | | -fno-stack-protector 不生成用于检测缓冲区溢出的代码。这是for 应用程序的,我们是编译内核,tos内核好像还用不到此功能。| | | -Os 为减小代码大小而进行优化。根据硬件spec,主引导扇区只有512字节,我们写的简单bootloader的最终大小不能大于510字节。| | | -I
添加搜索头文件的路径| | || | | 生成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代码为| | | (calladdfileshost,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 模拟为i386上的连接器| | -nostdlib 不使用标准库| | -N 设置代码段和数据段均可读写| | -e 指定入口| | -Ttext 制定代码段开始位置| || | 拷贝二进制代码bootblock.o到bootblock.out| | objcopy -S -O binary obj/bootblock.o obj/bootblock.out| | 其中关键的参数为| | -S 移除所有符号和重定位信息| | -O 指定输出格式| || | 使用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)−Ttools/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代码为| | | (calladdfilescc,(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 让连接器使用指定的脚本|| 生成一个有10000个块的文件,每个块默认512字节,用0填充| dd if=/dev/zero of=bin/tos.img count=10000|| 把bootblock中的内容写到第一个块| dd if=bin/bootblock of=bin/tos.img conv=notrunc|| 从第二个块开始写kernel中的内容| dd if=bin/kernel of=bin/tos.img seek=1 conv=notrunc
[练习1.2] 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么? 从sign.c的代码来看,一个磁盘主引导扇区只有512字节。且 第510个(倒数第二个)字节是0x55, 第511个(倒数第一个)字节是0xAA。 ## [练习2] [练习2.1] 从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。 练习2可以单步跟踪,方法如下: 1 修改 lab1/tools/gdbinit,内容为:set architecture i8086target remote :1234
2 在 lab1目录下,执行make debug
3 在看到gdb的调试界面(gdb)后,在gdb调试界面下执行如下命令si
即可单步跟踪BIOS了。 4 在gdb界面下,可通过如下命令来看BIOS的代码x /2i $pc //显示当前eip处的汇编指令
> [进一步的补充]改写Makefile文件debug: (TOSIMG)(V)(TERMINAL)−e“(QEMU) -S -s -d in_asm -D (BINDIR)/q.log−parallelstdio−hda< -serial null”(V)sleep2(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结尾加上
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"便可得到
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结尾加上
b *0x7c00 c x /10i $pc
便可以在q.log中读到"call bootmain"前执行的命令
---------------- 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
.code16 cli cld xorw %ax, %ax movw %ax, %ds movw %ax, %es movw %ax, %ss
开启A20:通过将键盘控制器上的A20线置于高电位,全部32条地址线可用, 可以访问4G的内存空间。
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表和其描述符已经静态储存在引导区中,载入即可
lgdt gdtdesc
进入保护模式:通过将cr0寄存器PE位置1便开启了保护模式
movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0
通过长跳转更新cs的基地址
ljmp $PROT_MODE_CSEG, $protcseg .code32 protcseg:
设置段寄存器,并建立堆栈
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主方法
call bootmain
## [练习4] 分析bootloader加载ELF格式的OS的过程。 首先看readsect函数, `readsect`从设备的第secno扇区读取数据到dst位置
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,可以从设备读取任意长度的内容。
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函数中,
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等是(可能的)参数。 输出中,堆栈最深一层为
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
tf->tf_cs = USER_CS; tf->tf_ds = USER_DS; tf->tf_es = USER_DS; tf->tf_ss = USER_DS;
对TO Kernel
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。
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。
asm volatile ( "int %0 \n" "movl %%ebp, %%esp \n" : : "i"(T_SWITCH_TOK) );
但这样不能正常输出文本。根据提示,在trap_dispatch中转User态时,将调用io所需权限降低。
tf->tf_eflags |= 0x3000;
相关文章推荐
- qduoj 91 来自kkun神的嘲讽
- android开发之滑动手势翻图 滑动手势监听
- android环境配置
- Hadoop安装教程_单机/伪分布式配置_Hadoop2.6.0/Ubuntu14.04
- C#使用sharppcap实现网络抓包
- 为什么要用Java泛型
- thinkphp not found 不能使用urlrewrite
- tensorflow示例代码注释2
- 2039三角形
- 记《浪潮之巅》-第一版-12.短暂的春秋-与机会失之交臂的公司之二Novell公司
- 框架模式MVC与MVP在Android中的应用
- js 设置和调用Cookie
- Android酷炫实用的开源框架(UI框架)
- __declspec(dllexport)和__declspec(dllimport)
- maven各种类的基本写法
- 百度贴吧图片区通用爬虫
- 练习四1002
- 分布式最终一致方案梳理
- tomcat异常之java.io.EOFException
- HorizontalScrollView 使用 实现textview和viewpager的联动