ucore lab1 实验报告
练习一
题目1)操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
题目2)一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
1)
首先在Makefile文件中找到ucore.img相关代码,可以看出UCOREIMG这个文件依赖于另外两个文件kernel和bootblock。想要生成ucore.img要先生成kernel和bootblock
# create ucore.img
UCOREIMG := $(call totarget,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
$(call create_target,ucore.img)
bootblock,boot目录下bootasm.S和bootmain.c,会进行编译和链接
# create bootblock
bootfiles = $(call listf_cc,boot)
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))
bootblock = $(call totarget,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)
$(call create_target,bootblock)
执行的相应代码是:
+ 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 -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
链接部分会制定代码段开始位置 为 0x7c00
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
kernel
# create kernel target
kernel = $(call totarget,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)
$(call create_target,kernel)
这里会将kern目录下的.c文件进行相应的编译
+ 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
以上就是整个ucore.img生成的大致过程,
总结如下:
1 编译所有生成bin/kernel所需的文件
2 链接生成bin/kernel
3 编译bootasm.S bootmain.c sign.c
4 根据sign规范生成obj/bootblock.o
5 生成ucore.img
2)
观察sign.c文件中的部分源码
char buf[512]; //定义buf数组
memset(buf, 0, sizeof(buf));
buf[510] = 0x55;
buf[511] = 0xAA; // 把buf数组的最后两位置为 *0x55, 0xAA*
FILE *ofp = fopen(argv[2], "wb+");
size = fwrite(buf, 1, 512, ofp);
if (size != 512) { //大小为512字节
fprintf(stderr, "write '%s' error,
size is %d.\n", argv[2], size);
return -1;
}
则有特征:通过sign对bootblock进行了限制,大小为512字节,多余的字节填0;最后两个字节为0x55,0xAA
练习2
1.从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
2.在初始化位置0x7c00设置实地址断点,测试断点正常。
3.从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和bootblock.asm进行比较。
解答
问题1:从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行
tools/gdbinit的内容如下。可见,这里是对内核代码进行调试,并且将断点设置在内核代码的入口地址,即kern_init函数
file bin/kernel
target remote :1234
break kern_init
continue
为了从CPU加电后执行的第一条指令开始调试,需要修改tools/gdbinit的内容为:
set architecture i8086
file bin/bootblock
target remote :1234
break start
continue
执行make debug,这时会弹出一个QEMU窗口和一个Terminal窗口,这是正常的,因为我们在makefile中定义了debug的操作正是启动QEMU、启动Terminal并在其中运行gdb。
debug: $(UCOREIMG)
(
V
)
(V)
(V)(QEMU) -S -s -parallel stdio -hda $< -serial null &
$(V)sleep 2
(
V
)
(V)
(V)(TERMINAL) -e “gdb -q -tui -x tools/gdbinit”
Terminal窗口此时停在0x0000fff0的位置,这是eip寄存器的值,而cs寄存器的值为0xf000. (遇到一个问题:此时无法正确反汇编出代码,使用x来查询内存0xfff0处的值时显示全0,不知道什么原因)
The target architecture is assumed to be i8086
0x0000fff0 in ?? ()
Breakpoint 1 at 0x7c00: file boot/bootasm.S, line 16.
输入si,执行1步,程序会跳转到0xe05b的地方。查看寄存器也可以发现eip的值变为0xe05b,而cs的值不变,仍然是0xf000.
反复输入si,以单步执行。(由于BIOS中全是汇编代码,看不懂其功能)。
问题2:在初始化位置0x7c00设置实地址断点,测试断点正常
我直接在tools/gdbinit中设置了断点break start,由于boot loader的入口为start,其地址为0x7c00,因此这和break *0x7c00效果是相同的。
设置断点后,输入continue或c,可以看到程序在0x7c00处停了下来,说明断点设置成功。
问题3:从0x7c00开始, 将反汇编代码与bootasm.S和bootblock.asm进行比较
反汇编的代码与bootblock.asm基本相同,而与bootasm.S的差别在于:
反汇编的代码中的指令不带指示长度的后缀,而bootasm.S的指令则有。比如,反汇编 的代码是xor %eax, %eax,而bootasm.S的代码为xorw %ax, %ax
反汇编的代码中的通用寄存器是32位(带有e前缀),而bootasm.S的代码中的通用寄存器是16位(不带e前缀)。
练习3
分析从bootloader进入保护模式的过程。BIOS 将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行 bootloader。请分析bootloader是如何完成从实模式进入保护模式的
1、关闭中断,将各个段寄存器重置
它先将各个寄存器置0
cli # Disable interrupts
cld # String operations increment
xorw %ax, %ax # Segment number zero
movw %ax, %ds # -> Data Segment
movw %ax, %es # -> Extra Segment
movw %ax, %ss # -> Stack Segment
2、开启A20
然后就是将A20置1,这里简单解释一下A20,当 A20 地址线控制禁止时,则程序就像在 8086 中运行,1MB 以上的地是不可访问的。而在保护模式下 A20 地址线控制是要打开的,所以需要通过将键盘控制器上的A20线置于高电位,使得全部32条地址线可用。
seta20.1:
inb $0x64, %al # 读取状态寄存器,等待8042键盘控制器闲置
testb $0x2, %al # 判断输入缓存是否为空
jnz seta20.1
movb $0xd1, %al # 0xd1表示写输出端口命令,参数随后通过0x60端口写入
outb %al, $0x64
seta20.2:
inb $0x64, %al
testb $0x2, %al
jnz seta20.2
movb $0xdf, %al # 通过0x60写入数据11011111 即将A20置1
outb %al, $0x60
3、加载GDT表
lgdt gdtdesc
4、将CR0的第0位置1
movl %cr0, %eax
orl $CR0_PE_ON, %eax
movl %eax, %cr0
5、长跳转到32位代码段,重装CS和EIP
ljmp $PROT_MODE_CSEG, $protcseg
6、重装DS、ES等段寄存器等
movw $PROT_MODE_DSEG, %ax # Our data segment selector
movw %ax, %ds # -> DS: Data Segment
movw %ax, %es # -> ES: Extra Segment
movw %ax, %fs # -> FS
movw %ax, %gs # -> GS
movw %ax, %ss # -> SS: Stack Segment
7、转到保护模式完成,进入boot主方法
movl $0x0, %ebp
movl $start, %esp
call bootmain
练习4
分析bootloader加载ELF格式的OS的过程
- bootloader如何读取硬盘扇区的?
- bootloader是如何加载 ELF格式的 OS?
1.
在bootmain中,第一句就是读取硬盘扇区
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count;
// round down to sector boundary
va -= offset % SECTSIZE;
// translate from bytes to sectors; kernel starts at sector 1
uint32_t secno = (offset / SECTSIZE) + 1;
// If this is too slow, we could read lots of sectors at a time.
// We'd write more to memory than asked, but it doesn't matter --
// we load in increasing order.
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
readsect((void *)va, secno);
static void
readsect(void *dst, uint32_t secno) {
// wait for disk to be ready
waitdisk();
outb(0x1F2, 1); // count = 1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
outb(0x1F7, 0x20); // cmd 0x20 - read sectors
// wait for disk to be ready
waitdisk();
// read a sector
insl(0x1F0, dst, SECTSIZE / 4);
}
大致流程如下:
(1).等待磁盘准备好
(2).发出读取扇区的命令
(3).等待磁盘准备好
(4).把磁盘扇区数据读取到指定内存
2.
bootmain(void) {
//读取硬盘的第一个扇区
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头表中的入口信息,找到内核的入口并开始运行
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1);
}
练习5
题目:
实现函数调用堆栈跟踪函数
先建立一个栈模型
ss:[ebp-8] ;变量2
ss:[ebp-4] ;变量1
ss:[ebp] ;栈针
ss:[ebp+4] ;返回地址
ss:[ebp+8] ;第一个参数
函数实现
read_ebp()和read_eip()函数来获取当前ebp寄存器和eip 寄存器的信息。
查看print_stackframe函数注释
/* 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]
*/
for(i = 0; ebp!=0 && i < STACKFRAME_DEPTH; i++) {//STACKFRAME_DEPTH = 20 一直向上循环找到所有的调用函数为止,一开始没有判断栈针为空的条件
cprintf("ebp:0x%08x eip:0x%08x ",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 *)ebp+1);//调用函数的返回地址
ebp = *((uint32_t *)ebp);//上一个函数的栈针
//循环直到没有调用函数停止
}
我们发现ebp的值在0x7bf8之后就为零了,这说明上面没有了调用函数,直接加个判断
ebp!=0 就可以输出预期的结果。
练习6
题目:
1.中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
2.请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。注意除了系统调用中断(T_SYSCALL)以外,其它中断均使用中断门描述符,权限为内核态权限;而系统调用中断使用异常,权限为陷阱门描述符。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
3.请编程完善trap.c中的中断处理函数trap在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用 print_ticks子程序,向屏幕上打印一行文字100 ticks。
1.中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成位移,
两者联合便是中断处理程序的入口地址。
中断描述符表中一个表项占8个字节
其中015和4863分别为offset的低16位和高16位,16~31位是段选择子,通过段选择子得到段基址,再加上段内偏移量就可以得到中断处理代码的入口。
2.请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init
这里这里主要就是实现对中断向量表的初始化。
重点就是两步
第一步,声明__vertors[],其中存放着中断服务程序的入口地址。这个数组生成于vertor.S中。
第二步,填充中断描述符表IDT。
第三部,加载中断描述符表。
对应到代码中如下所示:
void idt_init(void) {
extern uintptr_t __vectors[];//声明__vertors[]
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);//使用lidt指令加载中断描述符表
}
3.请编程完善trap.c中的中断处理函数trap在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用 print_ticks子程序,向屏幕上打印一行文字100 ticks
根据指导书查看函数trap_dispatch,发现print_ticks()子程序已经被实现了,所以直接进行判断输出即可,如下:
…
…
case IRQ_OFFSET + IRQ_TIMER:
ticks ++; //每一次时钟信号会使变量ticks加1
if (ticks==TICK_NUM) {//TICK_NUM已经被预定义成了100,每到100便调用print_ticks()函数打印
ticks-=TICK_NUM;
print_ticks();
}
break;
…
…