ucore lab1实验报告
一.实验目的
了解:
1.计算机原理:
- CPU的编址与寻址: 基于分段机制的内存管理
- CPU的中断机制
- 外设:串口/并口/CGA,时钟,硬盘
2.Bootloader软件:
- 编译运行bootloader的过程
- 调试bootloader的方法
- PC启动bootloader的过程
- ELF执行文件的格式和加载
- 外设访问:读硬盘,在CGA上显示字符串
3.ucore OS软件:
- 编译运行ucore OS的过程
- ucore OS的启动过程
- 调试ucore OS的方法
- 函数调用关系:在汇编级了解函数调用栈的结构和处理过程
- 中断管理:与软件相关的中断处理
- 外设管理:时钟
二.实验内容
[练习1]
操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
运行make命令前,lab1文件夹下包含的源码文件夹有boot、kern、libs和tools,这四个文件夹的主要>内容如下:
① boot文件夹下包含bootasm.c、bootasm.S和asm.h三个文件,从这些文件编译生成我们需要的bootloader;
② kern文件夹下包含若干个小文件夹,小文件加包含的是ucore内核源码,这些代码被用来编译生成ucore操作系统内核;
③ libs是库文件夹,里面包含ucore源码要用到的几个头文件,以及string.c和printfmt.c两个c文件;
④ tools文件夹可直接按其字面含义理解成工具文件夹,包含了制作镜像盘中时用到的各种工具文件,例如有Makefile中用到的functiong.mk,以及用来生成主sign的sign.c,sign是一个可执行文件,执行make后它被放在/bin下,用来生成硬盘主引导扇区。
输入make v=
+ cc kern/init/init.c //编译init.c
gcc -c kern/init/init.c -o obj/kern/init/init.o
+ cc kern/libs/readline.c //编译readline.c
gcc -c kern/libs/readline.c -o
obj/kern/libs/readline.o
+ cc kern/libs/stdio.c //编译stdlio.c
gcc -c kern/libs/stdio.c -o obj/kern/libs/stdio.o
+ cc kern/debug/kdebug.c //编译kdebug.c
gcc -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o
+ cc kern/debug/kmonitor.c //编译komnitor.c
gcc -c kern/debug/kmonitor.c -o
obj/kern/debug/kmonitor.o
+ cc kern/debug/panic.c //编译panic.c
gcc -c kern/debug/panic.c -o obj/kern/debug/panic.o
+ cc kern/driver/clock.c //编译clock.c
gcc -c kern/driver/clock.c -o obj/kern/driver/clock.o
+ cc kern/driver/console.c //编译console.c
gcc -c kern/driver/console.c -o
obj/kern/driver/console.o
+ cc kern/driver/intr.c //编译intr.c
gcc -c kern/driver/intr.c -o obj/kern/driver/intr.o
+ cc kern/driver/picirq.c //编译prcirq.c
gcc -c kern/driver/picirq.c -o
obj/kern/driver/picirq.o
+ cc kern/trap/trap.c //编译trap.c
gcc -c kern/trap/trap.c -o obj/kern/trap/trap.o
+ cc kern/trap/trapentry.S //编译trapentry.S
gcc -c kern/trap/trapentry.S -o
obj/kern/trap/trapentry.o
+ cc kern/trap/vectors.S //编译vectors.S
gcc -c kern/trap/vectors.S -o obj/kern/trap/vectors.o
+ cc kern/mm/pmm.c //编译pmm.c
gcc -c kern/mm/pmm.c -o obj/kern/mm/pmm.o
+ cc libs/printfmt.c //编译printfmt.c
gcc -c libs/printfmt.c -o obj/libs/printfmt.o
+ cc libs/string.c //编译string.c
gcc -c libs/string.c -o obj/libs/string.o
+ ld bin/kernel //链接成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
+ cc boot/bootasm.S //编译bootasm.c
gcc -c boot/bootasm.S -o obj/boot/bootasm.o
+ cc boot/bootmain.c //编译bootmain.c
gcc -c boot/bootmain.c -o obj/boot/bootmain.o
+ cc tools/sign.c //编译sign.c
gcc -c tools/sign.c -o obj/sign/tools/sign.o
gcc -O2 obj/sign/tools/sign.o -o bin/sign
+ ld bin/bootblock //根据sign规范生成bootblock
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00
obj/boot/bootasm.o obj/boot/bootmain.o
-o obj/bootblock.o
//创建大小为10000个块的ucore.img,初始化为0,每个块为512字节
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
bin/ucore.img
生成ucore.img的相关代码为
$(UCOREIMG): $(kernel)
(
b
o
o
t
b
l
o
c
k
)
(
V
)
d
d
i
f
=
/
d
e
v
/
z
e
r
o
o
f
=
(
V
)
d
d
i
f
=
/
d
e
v
/
z
e
r
o
o
f
=
(
V
)
d
d
i
f
=
/
d
e
v
/
z
e
r
o
o
f
=
@
c
o
u
n
t
=
10000
(
V
)
d
d
i
f
=
(
V
)
d
d
i
f
=
(
V
)
d
d
i
f
=
(
b
o
o
t
b
l
o
c
k
)
o
f
=
(bootblock) (V)ddif=/dev/zeroof= (V)dd if=/dev/zero of=(V)ddif=/dev/zeroof=@ count=10000 (V)ddif= (V)dd if=(V)ddif=(bootblock) of=
(bootblock)(V)ddif=/dev/zeroof=(V)ddif=/dev/zeroof=(V)ddif=/dev/zeroof=@count=10000(V)ddif=(V)ddif=(V)ddif=(bootblock)of=@ conv=notrunc
(V)ddif= (V)dd if=(V)ddif=(kernel) of=$@ seek=1 conv=notrunc
为了生成ucore.img,首先需要生成bootblock、kernel
bin/bootblock
生成bootblock的相关代码为
$(bootblock): (calltoobj, (call toobj,(calltoobj,(bootfiles)) $(call totarget,sign)
@echo + ld $@
(V) (V)(V)(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 $^
-o (calltoobj,bootblock)@ (call toobj,bootblock)@(calltoobj,bootblock)@(OBJDUMP) -S $(call objfile,bootblock) >
(callasmfile,bootblock)@ (call asmfile,bootblock)@(callasmfile,bootblock)@(OBJCOPY) -S -O binary $(call objfile,bootblock)
(calloutfile,bootblock)@ (call outfile,bootblock)@(calloutfile,bootblock)@(call totarget,sign) $(call outfile,bootblock) $(bootblock)
为了生成bootblock,首先需要生成bootasm.o、bootmain.o、sign
查看makefile文件找到:
@$(call totarget,sign) $(call outfile,bootblock)
$(bootblock)
所以从上面可以看出ucore.img的生成过程:
1 编译所有生成bin/kernel所需的文件
2 链接生成bin/kernel
3 编译bootasm.S bootmain.c sign.c
4 根据sign规范生成obj/bootblock.o
5 生成ucore.img
[练习2]
1.从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
其实就是用qemu和gdb进行调试,需要了解一些简单的gdb命令
执行以下命令
make debug
将加载tools/gdbinit文件的配置设置gdb
#######tools/gdbinit#######
file bin/kernel
target remote:1234
break kern_init
continue
运行
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.在初始化位置0x7c00设置实地址断点,测试断点正常。
就是b *0x7c00
3.从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
define hook-stop
x/i $pc
end
[练习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便开启了保护模式
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的过程
阅读bootmain.c源码
这是一个简单的启动装载程序,唯一的工作就是启动来自第一个IDE硬盘的ELF内核映像。
磁盘布局
此程序(bootasm.S和bootmain.c)是引导加载程序.应该存储在磁盘的第一个扇区中。第二个扇区开始保存内核映像。内核映像必须为ELF格式。
启动步骤
当CPU启动时,它将BIOS加载到内存中并执行,BIOS初始化设备,中断例程集以及读取引导设备的第一个扇区(例如,硬盘驱动器)进入内存并跳转到它。假设此引导加载程序存储在该引导程序的第一个扇区中控制从bootasm.S开始-设置保护模式,和一个堆栈,然后运行C代码,然后调用bootmain()该文件中的bootmain()会接管,读取内核并跳转到该内核。
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);
}
在linux的驱动程序中,都会使用大量的outb、outw、inb、inw等等宏来訪问硬件或寄存器。outb() I/O 上写入 8 位数据 ( 1 字节 )。inb() I/O 上读入 8 位数据 ( 1 字节 )。
ELF文件格式
ELF(Executable and linking format)文件格式是Linux系统下的一种常用目标文件(object file)格式,有三种主要类型:
用于执行的可执行文件(executable file),用于提供程序的进程映像,加载的内存执行。 这也是本实验的OS文件类型。
用于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。
共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。
struct elfhdr {
uint magic; // must equal ELF_MAGIC
uchar elf[12];
ushort type;
ushort machine;
uint version;
uint entry; // 程序入口的虚拟地址
uint phoff; // program header 表的位置偏移
uint shoff;
uint flags;
ushort ehsize;
ushort phentsize;
ushort phnum; //program header表中的入口数目
ushort shentsize;
ushort shnum;
ushort shstrndx;
};
program header描述与程序执行直接相关的目标文件结构信息,用来在文件中定位各个段的映像,同时包含其他一些用来为程序创建进程映像所必需的信息。可执行文件的程序头部是一个program header结构的数组, 每个结构描述了一个段或者系统准备程序执行所必需的其它信息。目标文件的 “段” 包含一个或者多个 “节区”(section) ,也就是“段内容(Segment Contents)” 。程序头部仅对于可执行文件和共享目标文件有意义。可执行目标文件在ELF头部的e_phentsize和e_phnum成员中给出其自身程序头部的大小。程序头部的数据结构如下表所示:
struct proghdr {
uint type; // 段类型
uint offset; // 段相对文件头的偏移值
uint va; // 段的第一个字节将被放到内存中的虚拟地址
uint pa;
uint filesz;
uint memsz; // 段在内存映像中占用的字节数
uint flags;
uint align;
};
[练习5]实现函数调用堆栈跟踪函数
完成kdebug.c中函数print_stackframe
ebp为基址指针寄存器
esp为堆栈指针寄存器(指向栈顶)
ebp寄存器处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原ebp入栈后的栈顶),从该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的ebp值
举一个实际的例子查看ebp与esp两个寄存器如何构建出完整的函数栈:
leave等同于movl %ebp, %esp,popl %ebp两条指令
int i,j;
uint32_t ebp = read_ebp(), eip = read_eip();
for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {
cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);
// ebp向上移动4个字节为eip
uint32_t *args = (uint32_t *)ebp + 2;
// 再向上每4个字节都为输入的参数(这里只是假设4个参数,做实验)
for (j = 0; j < 4; j ++) {
cprintf("0x%08x ", args[j]);
}
cprintf("\n");
print_debuginfo(eip - 1);
// ebp指针指向的位置向上一个地址为上一个函数的eip
eip = ((uint32_t *)ebp)[1];
// ebp指针指向的位置存储的上一个ebp的地址
ebp = ((uint32_t *)ebp)[0];
}
[练习6]完善中断初始化和处理(需要编程)
请完成编码工作和回答如下问题:
1.中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
2.请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
3.请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。
1.中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成位移, 两者联合便是中断处理程序的入口地址。
#define SETGATE(gate, istrap, sel, off, dpl) {
(gate).gd_off_15_0 = (uint32_t)(off) & 0xffff;
(gate).gd_ss = (sel);
(gate).gd_args = 0;
(gate).gd_rsv1 = 0;
(gate).gd_type = (istrap) ? STS_TG32 : STS_IG32;
(gate).gd_s = 0;
(gate).gd_dpl = (dpl);
(gate).gd_p = 1;
(gate).gd_off_31_16 = (uint32_t)(off) >> 16;
}
参数:
gate:
istrap:陷阱门设为1,中断门设为0.
sel:段选择子,全局描述符表的代码段段选择子 //memlayout.h里面有宏定义GD_KTEXT
off:处理函数的入口地址,即__vectors[]中的内容。
dpl:特权级.从实验指导书中可知,ucore中的应用程序处于特权级3,内核态特权级为0.
void
idt_init(void) {
extern uintptr_t __vectors[];
for(int i = 0; i < sizeof(idt)/sizeof(struct gatedesc); i++)
{
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i]; 0);
}
SETGATE(idt[T_SYSCALL], 0, GD_KTEXT, __vectors[T_SYSCALL]; 3);
lidt(&idt_pd);
}
3.case IRQ_OFFSET+IRQ_TIMER:
ticks++;
if (ticks % TICK_NUM == 0) {
print_ticks();
}
break;
/*
make
make qemu
*/