Lab 1
实验介绍
操作系统也是一个软件,需要通过Bootloader(小于512字节,这样才能放入硬盘的主引导扇区)加载启动,lab1自带了一个bootloader和微型ucore。
Bootloader具备如下功能:
- 切换到X86保护模式
- 读取磁盘并加载ELF格式(Executable and Linkable Format)的文件
- 显示字符
lab1 的ucore具备如下功能:
- 处理时钟中断
- 显示字符
实验内容
练习1、make生成img镜像过程
make命令将自动执行当前目录下的MakeFile,常用命令:
- make V= 执行MakeFile,并显示具体执行内容
- make clean 清理通过make命令生成的各种文件
ucore.img生成流程如下(为不影响阅读体验,make输出的内容贴在附录一):
- 生成kernel:编译kern和libs目录下的所有.c文件,生成同名.o文件;然后通过模拟elf_i386链接器,并使用tools/kernel.ld作为链接器脚本将上述.o文件链接成bin/kernel可执行文件
- 生成sign:编译tools/sign.c生成bin/sign可执行文件;外部执行程序,用于生成虚拟硬盘主引导扇区;
- 生成bootblock:编译boot目录(这个目录即bootloader)下的bootmain.c文件和bootasm.S,生成同名.o文件;通过模拟elf_i386链接器,并使用使用-Ttext 0x7c00(不太理解,恳请大佬解惑)作为链接器脚本,将boot目录下的.o文件链接成512字节大小(一个块)bin/bootblock(真实计算机中引导块一般烧写在BIOS中,不可更改,其中包含用于引导的最小指令集)
- 生成ucore.img:使用/dev/zero设备文件将ucore.img扩充为10000个块的空文件;将bootblock和kernel依次写入ucore.img
查看sign.c和生产的bin/bootblock文件,可知符合系统规范的硬盘主引导扇区(即引导块)特征如下:
- 大小为一个块(512字节)
- 最后两个字符为0x55 0xAA
练习2、使用qemu执行并调试lab1的ucore.img
lab1目录下执行如下指令即可正常启动镜像:
qemu-system-i386 bin/ucore.img
如需边运行qemu,边单步调试,在lab1目录下执行如下指令:
make debug
该命令会在启动qemu的同时进入gdb的调试模式,调试模式的断电设置以及其他信息在tools/gdbinit文件中定义:
file bin/kernel
target remote :1234
set architecture i8086
b *0x7c00
continue
x /2i $pc # gdb x(examine)查看内存地址的值,i(info)查看寄存器信息,pc为指令计数器(存储下一条要执行的指令)
# break kern_init
# continue
执行后跳出qemu和新的terminal窗口,此时qemu在第一条输出暂停"Booting from Hard Disk…",新terminal上输出如下
The target architecture is assumed to be i8086
Breakpoint 1 at 0x7c00
Breakpoint 1, 0x00007c00 in ?? ()
=> 0x7c00: cli # Clear interrupt 屏蔽中断,与sti相对
0x7c01: cld # Clear direction 使传送方向从低地址向高地址,与std相对
# 接下来交替输入si和x/i $pc,逐条指令执行,可以看到输出内容和boot/bootasm.s从0x7c00开始的汇编代码相同(源码分析参见附录四、bootasm.s)
练习3、分析bootloader进入保护模式过程
以下练习答案均可基于bootasm.S得知
为何开启A20,如何开启A20
开启A20地址线后才可以完成从实模式到保护模式的转换。A20本来是为了对下兼容20位地址线设备,最多访问到1MB,超出即回滚。
开启A20 Gate后,关闭上述功能,才能在实模式下访问高端内存区,保护模式同样需要开启A20 Gate。
如何初始化GDT表
GDT (Global Descriptor Table) 全局描述符表,进入保护模式后使用分段式内存需要GDT,所有任务可见且唯一,含有2^13个段描述符,段描述符维护了每个分段的基址,引用段描述符时必须知道GDT入口。
每个分段包含逻辑地址VA、段描述符(表)、段选择子(段寄存器,用于定位段描述符在GDT中索引)。
VA转换为PA过程如下图,如果不采用分页机制,则下面的线性地址即物理地址;否则需要再一次转换。线性地址长度为32位。
如何使能和进入保护模式
进入32位模式,通过ljmp指令将保护模式代码段长跳转到相应逻辑地址($ PROT_MODE_CSEG = 0X8)。将数据段选择子放到DS ES FS GS SS段寄存器上,设置栈指针ebp esp开辟好0~0x7c00的栈后,最后调用bootmain函数。cr0寄存器置1($CR0_PE_ON = 0x1)即进入保护模式。
练习4、分析bootloader加载ELF格式的OS的过程
参见附录bootmain.c源码的注释解析
练习5、实现函数调用堆栈跟踪函数
kern/debug/kdebug.c用于内核调试,提供源码和二进制对应关系的查询功能,显示调用栈关系,练习任务为补全print_stackframe函数。
void
print_stackframe(void) {
/* 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]
*/
// 获取ebp、eip寄存器内的值
uint32_t ebp = read_ebp();
uint32_t eip = read_eip();
int i, j;
for(i = 0; i < STACKFRAME_DEPTH && ebp != 0; i++) {
cprintf("ebp:0x%08x eip:0x%08x", ebp, eip);
uint32_t *arg = (uint32_t *)ebp + 2; // 参数列表在ebp + 2往栈底(高地址)方向
cprintf(" arg:");
for(j = 0; j < 4; j++) {
cprintf("0x%08x ", arg[j]);
}
cprintf("\n");
print_debuginfo(eip - 1);·// 输出C调用函数名称和行号等
eip = ((uint32_t *)ebp)[1]; // 将下一条指令设为ebp + 1,即返回地址
ebp = ((uint32_t*)ebp)[0]; // ebp读取上一级栈基址,回到调用函数栈
}
}
qemu运行镜像时,相关输出如下:
ebp:0x00007b08 eip:0x001009a6 arg:0x00010094 0x00000000 0x00007b38 0x00100092
kern/debug/kdebug.c:306: print_stackframe+21
ebp:0x00007b18 eip:0x00100ca1 arg:0x00000000 0x00000000 0x00000000 0x00007b88
kern/debug/kmonitor.c:125: mon_backtrace+10
ebp:0x00007b38 eip:0x00100092 arg:0x00000000 0x00007b60 0xffff0000 0x00007b64
kern/init/init.c:48: grade_backtrace2+33
ebp:0x00007b58 eip:0x001000bb arg:0x00000000 0xffff0000 0x00007b84 0x00000029
kern/init/init.c:53: grade_backtrace1+38
ebp:0x00007b78 eip:0x001000d9 arg:0x00000000 0x00100000 0xffff0000 0x0000001d
kern/init/init.c:58: grade_backtrace0+23
ebp:0x00007b98 eip:0x001000fe arg:0x0010349c 0x00103480 0x0000130a 0x00000000
kern/init/init.c:63: grade_backtrace+34
ebp:0x00007bc8 eip:0x00100055 arg:0x00000000 0x00000000 0x00000000 0x00010094
kern/init/init.c:28: kern_init+84
ebp:0x00007bf8 eip:0x00007d68 arg:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
<unknow>: -- 0x00007d67 --
注意到最后两行的ebp=0x7bf8,刚好在0x7c00前4位,由之前bootloader(确切来说是)设置的堆栈从0x7c00开始,转入bootmain函数,可知call bootmain的call指令压栈,故bootmain()栈帧的ebp为0x7bf8。
练习6、完善中断初始化和处理
中断相关功能在kern/trap下,其下有如下文件:
- vectors.s中定义了256个(保护模式下允许的最大值)中断服务例程的入口地址,其中[0,31]用于异常和NMI,[32, 255]保留给用户定义。该文件由tools/vector.c编译ucore时动态生成
- trapentry.s(中断入口)将ds es fs gs段寄存器压栈构成类似struct的中断栈帧,将GD_KDATA存入ds es来为内核配置数据段,并将esp压栈作为参数以调用trap.c->trap(esp),调用结束后逆过程依次出栈、恢复各段寄存器(恢复中断上下文),最后通过addl $0x8 %esp避开中断号和错误代码
- trap.c/trap.h中由trapentry.s调用的trap(struct trapframe *tf)函数只有一个操作:调用trap_dispatch(tf),该函数会基于中断类型(tf->tf_trapno,有时钟中断、COM1串口中断、键盘输入中断)分配不同操作,如果不是已知类型,则会调用print_trapframe(tf)并报错
- idt_init()初始化中断描述符表,即初始化在vectors.S定义的各个中断入口;中断描述符表把每个中断或异常编号和一个指向中断服务例程的描述符联系起来。同GDT一样,IDT是一个8字节的描述符数组,但IDT的第一项可以包含一个描述符。CPU把中断(异常)号乘以8做为IDT的索引。IDT可以位于内存的任意位置,CPU通过IDT寄存器(IDTR)的内容来寻址IDT的起始地址。
- lab1中trapentry.s发来的中断类型为IRQ_OFFSET+IRQ_TIMER(时钟中断),此时会调用print_ticks(),即在qemu输出TICK_NUM
中断描述符表(保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
查看kern/mm/mmu.h中gatedesc定义(interrupt和trap的门描述符),将其中各值相加得64bit,故为8字节;并且得知gd_ss所在的[16, 31]为段选择子,由此得知GDT中对应段的基地址,加上gd_off_15_0, gd_off_31_16组成的偏移量获得中断处理程序的入口地址。
/* Gate descriptors for interrupts and traps */
struct gatedesc {
unsigned gd_off_15_0 : 16; // low 16 bits of offset in segment
unsigned gd_ss : 16; // segment selector
unsigned gd_args : 5; // # args, 0 for interrupt/trap gates
unsigned gd_rsv1 : 3; // reserved(should be zero I guess)
unsigned gd_type : 4; // type(STS_{TG,IG32,TG32})
unsigned gd_s : 1; // must be 0 (system)
unsigned gd_dpl : 2; // descriptor(meaning new) privilege level
unsigned gd_p : 1; // Present
unsigned gd_off_31_16 : 16; // high bits of offset in segment
};
完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init
idt_init()初始化中断描述符表,即初始化在vectors.S定义的各个中断入口
/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
void
idt_init(void) {
/* LAB1 YOUR CODE : STEP 2 */
/* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)?
* All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ?
* __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c
* (try "make" command in lab1, then you will find vector.S in kern/trap DIR)
* You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later.
* (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT).
* Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT
* (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction.
* You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more.
* Notice: the argument of lidt is idt_pd. try to find it!
*/
extern uintptr_t __vectors[]; // 声明已在vectors.S初始化完毕的中断入口
int i;
for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL); // 使用各中断入口配置idt表项,DPL_KERNEL表示描述符优先级为内核级,并且将中断例程设置为GD_KTEXT(在kern/mm/memlayout.h中定义)
}
SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
lidt(&idt_pd); // lidt为x86.h中函数,作用是告知CPU IDT所在地址,只能在特权级0(DPL_KERNEL)执行
}
trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序
static void print_ticks() {
cprintf("%d ticks\n",TICK_NUM);
#ifdef DEBUG_GRADE
cprintf("End of Test.\n");
panic("EOT: kernel seems ok.");
#endif
}
/* trap_dispatch - dispatch based on what type of trap occurred */
static void
trap_dispatch(struct trapframe *tf) {
char c;
switch (tf->tf_trapno) {
case IRQ_OFFSET + IRQ_TIMER:
/* LAB1 YOUR CODE : STEP 3 */
/* handle the timer interrupt */
/* (1) After a timer interrupt, you should record this event using a global variable (increase it), such as ticks in kern/driver/clock.c
* (2) Every TICK_NUM cycle, you can print some info using a funciton, such as print_ticks().
* (3) Too Simple? Yes, I think so!
*/
ticks++;
if (ticks % TICK_NUM == 0) {
print_ticks();
}
break;
case IRQ_OFFSET + IRQ_COM1:
c = cons_getc();
cprintf("serial [%03d] %c\n", c, c);
break;
case IRQ_OFFSET + IRQ_KBD:
c = cons_getc();
cprintf("kbd [%03d] %c\n", c, c);
break;
//LAB1 CHALLENGE 1 : YOUR CODE you should modify below codes.
case T_SWITCH_TOU:
case T_SWITCH_TOK:
panic("T_SWITCH_** ??\n");
break;
case IRQ_OFFSET + IRQ_IDE1:
case IRQ_OFFSET + IRQ_IDE2:
/* do nothing */
break;
default:
// in kernel, it must be a mistake
if ((tf->tf_cs & 3) == 0) {
print_trapframe(tf);
panic("unexpected trap in kernel.\n");
}
}
}
/* *
* trap - handles or dispatches an exception/interrupt. if and when trap() returns,
* the code in kern/trap/trapentry.S restores the old CPU state saved in the
* trapframe and then uses the iret instruction to return from the exception.
* */
void
trap(struct trapframe *tf) {
// dispatch based on what type of trap occurred
trap_dispatch(tf);
}
扩展练习 Challenge 1
练习内容为实现kern/init/init.c的lab1_switch_to_user()和lab1_switch_to_user()函数,并修改kern/trap/trap.c使得能够在用户态和内核态之间切换。
asm用于内联汇编代码,volatile表示它声明的变量允许通过编译器未知的方式进行修改(如OS、硬件、其他线程等),编译器不会对它声明的代码段进行优化,保证对特殊地址的稳定访问。
利用软中断来发出系统调用请求,init.c答案实现如下:
static void
lab1_switch_to_user(void) {
//LAB1 CHALLENGE 1 : TODO
asm volatile (
"sub $0x8, %%esp \n" // 访问调用者栈顶
"int %0 \n" // interrupt软中断指令,操作数为中断向量ISR号(来自于vectors.s)
"movl %%ebp, %%esp" // 回到调用函数
:
: "i"(T_SWITCH_TOU) // 中断类型设置为T_SWITCH_TOU(To User)
);
}
static void
lab1_switch_to_kernel(void) {
//LAB1 CHALLENGE 1 : TODO
asm volatile (
"int %0 \n"
"movl %%ebp, %%esp \n"
:
: "i"(T_SWITCH_TOK) // 中断类型设置为T_SWITCH_TOU(To Kernel)
);
}
trap.c答案实现如下:
//LAB1 CHALLENGE 1 : YOUR CODE you should modify below codes.
case T_SWITCH_TOU:
if (tf->tf_cs != USER_CS) {
switchk2u = *tf; // 缓存tf
switchk2u.tf_cs = USER_CS; // 中断帧代码段设为用户代码段
switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS; // 数据段设为用户数据段
switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;
// set eflags, make sure ucore can use io under user mode.
// if CPL > IOPL, then cpu will generate a general protection.
switchk2u.tf_eflags |= FL_IOPL_MASK; // IO Privilege Level,小于等于IOPL时才允许使用IO指令,否则出现保护性故障中断
// 用户态中断的相关栈紧邻原中断栈,通过iret返回将自动跳入该栈,从而进入用户态
*((uint32_t *)tf - 1) = (uint32_t)&switchk2u;
}
break;
case T_SWITCH_TOK:
if (tf->tf_cs != KERNEL_CS) {
tf->tf_cs = KERNEL_CS;
tf->tf_ds = tf->tf_es = KERNEL_DS;
tf->tf_eflags &= ~FL_IOPL_MASK; // 关闭IOPL
switchu2k = (struct trapframe *)(tf->tf_esp - (sizeof(struct trapframe) - 8));
memmove(switchu2k, tf, sizeof(struct trapframe) - 8);
*((uint32_t *)tf - 1) = (uint32_t)switchu2k;
}
break;
扩展练习 Challenge 2
用键盘实现用户模式内核模式切换。具体目标是:“键盘输入3时切换到用户模式,键盘输入0时切换到内核模式”。 基本思路是借鉴软中断(syscall功能)的代码,并且把trap.c中软中断处理的设置语句拿过来。
参考答案没有提供源码,故不在此处赘述。
笔者make grade不知为何无法正常运行,终端输出如下,希望有大佬指点一二
feli@ubuntu:~/Desktop/ucore/labcodes_answer/lab1_result$ make grade
: not found.sh: 2: tools/grade.sh:
tools/grade.sh: 209: tools/grade.sh: Syntax error: word unexpected (expecting "do")
make: *** [grade] Error 2
附录一、 make V= 命令行输出
# cc(compile c)将c文件编译为.o文件
+ cc kern/init/init.c
# gcc -Idir在指定目录dir下查找头文件
# gcc -Wall生成所有警告信息
# gcc -ggdb尽可能生成gdb可用调试信息
# gcc -m32针对x86_32进行优化
# gcc -gstabs以stabs格式生成非gdb调试信息
# gcc -nostdinc不在系统默认目录查找头文件,配合-Idir使用
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
kern/init/init.c:95:1: warning: ‘lab1_switch_test’ defined but not used [-Wunused-function]
lab1_switch_test(void) {
^
+ 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(链接器)将.o目标文件转化为可执行文件
# ld -m 模拟指定链接器,常见选项:elf_i386, elf_x86_64, 32, 64等
# ld -nostdlib 配套 gcc -nostdinc使用
# ld -T 使用指定文件作为链接器脚本,替换ld的默认链接器脚本
+ 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
+ cc boot/bootasm.S
# gcc -O 优化级别
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 -g 仅编译,并在.o中包含调试的相关符号信息
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
+ ld bin/bootblock
# ld -N 指定读/写文本和数据段
# ld -e 使用指定符号作为程序初始执行点
ld -m elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
'obj/bootblock.out' size: 472 bytes
build 512 bytes boot sector: 'bin/bootblock' success!
# dd 读取、转换并输出,if输入文件名,of输出文件名,count仅拷贝指定数目的块(在ibs指定),此处为512字节
dd if=/dev/zero of=bin/ucore.img count=10000
10000+0 records in
10000+0 records out
5120000 bytes (5.1 MB) copied, 0.0144993 s, 353 MB/s
# dd conv=notrunc 转换参数=不截短输出
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
1+0 records in
1+0 records out
512 bytes (512 B) copied, 0.000557475 s, 918 kB/s
# dd seek 跳过指定块数开始
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
146+1 records in
146+1 records out
74879 bytes (75 kB) copied, 0.000370649 s, 202 MB/s
附录二、bootmain.c
#include <defs.h>
#include <x86.h>
#include <elf.h>
/* *********************************************************************
bootmain.c和bootasm.s组成简单的bootloader,用于从磁盘引导ELF内核镜像
* 镜像结构
* 镜像第一个块为引导块,第二个块往后为内核,内核必须是ELF格式;
*
* 启动流程
* CPU启动,加载BIOS到内存并执行
* BIOS初始化设备、中断程序,并读取引导设备(如硬盘)的第一个扇区到内存,再跳转执行
* 跳转到第一个扇区后,控制权首先交给bootasm.s,它启动保护模式和一个用于C代码执行的栈,然后调用bootmain.c的bootmain()函数读入kernel,并进入kernel
*/
#define SECTSIZE 512 // 扇区大小
#define ELFHDR ((struct elfhdr *)0x10000) // ELF Header 暂存空间
/* waitdisk - 等待磁盘就绪 */
static void
waitdisk(void) {
while ((inb(0x1F7) & 0xC0) != 0x40);
}
/* readsect - 将secno号扇区读取到dst位置 */
static void
readsect(void *dst, uint32_t secno) {
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 - 读取扇区
waitdisk();
insl(0x1F0, dst, SECTSIZE / 4); // 读取扇区
}
/* readseg - 从内核的offset位置读取count个字节到va虚拟地址,可能会拷贝比count更多字节 */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
uintptr_t end_va = va + count;
// 向下取到扇区边界
va -= offset % SECTSIZE;
// 将offset字节数转化为扇区号secno; +1是因为内核从扇区1开始
uint32_t secno = (offset / SECTSIZE) + 1;
// 依次读入内核扇区,并发读可能写入超出预期数目的扇区到内存,但是无伤大雅
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
/* bootmain - bootloader入口 */
void
bootmain(void) {
// 读取磁盘第一页
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// 验证是否合法的ELF
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// 读取各程序段
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// 从ELF Header调用入口点,该函数不会返回任何信息
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1);
}
附录三、sign.c
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
int
main(int argc, char *argv[]) {
struct stat st;
if (argc != 3) {
fprintf(stderr, "Usage: <input filename> <output filename>\n");
return -1;
}
if (stat(argv[1], &st) != 0) {
fprintf(stderr, "Error opening file '%s': %s\n", argv[1], strerror(errno));
return -1;
}
printf("'%s' size: %lld bytes\n", argv[1], (long long)st.st_size);
if (st.st_size > 510) {
fprintf(stderr, "%lld >> 510!!\n", (long long)st.st_size);
return -1;
}
char buf[512];
// 填充0,写入到文件流指针
memset(buf, 0, sizeof(buf));
FILE *ifp = fopen(argv[1], "rb");
int size = fread(buf, 1, st.st_size, ifp);
if (size != st.st_size) {
fprintf(stderr, "read '%s' error, size is %d.\n", argv[1], size);
return -1;
}
fclose(ifp);
// 最后两个字符为0x55和0xAA
buf[510] = 0x55;
buf[511] = 0xAA;
FILE *ofp = fopen(argv[2], "wb+");
size = fwrite(buf, 1, 512, ofp);
if (size != 512) {
fprintf(stderr, "write '%s' error, size is %d.\n", argv[2], size);
return -1;
}
fclose(ofp);
printf("build 512 bytes boot sector: '%s' success!\n", argv[2]);
return 0;
}
附录四、bootasm.S
#include <asm.h>
# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 (code segment代码段) %ip (instruction pointer指令指针)=7c00.
# 8086中,(cs << 4) | ip 组成下一条指令地址
.set PROT_MODE_CSEG, 0x8 # kernel 代码段选择子
.set PROT_MODE_DSEG, 0x10 # kernel 数据段选择子
.set CR0_PE_ON, 0x1 # 保护模式启动标记
# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
# 实模式中bootloader开始运行的地址为0x7c00,注意实模式均在16位模式下执行汇编代码
# 关闭中断,初始化各段为0
.globl start
start:
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
# Set up the important data segment registers (DS, ES, SS).
xorw %ax, %ax # ax存储段号,初始化为0
movw %ax, %ds # -> Data Segment
movw %ax, %es # -> Extra Segment
movw %ax, %ss # -> Stack Segment
# Enable A20:
# For backwards compatibility with the earliest PCs, physical
# address line 20 is tied low, so that addresses higher than
# 1MB wrap around to zero by default. This code undoes this.
seta20.1:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al # 如果 %al 第低2位为1,则ZF = 0, 则跳转至seta20.1
jnz seta20.1 # 如果 %al 第低2位为0,则ZF = 1, 则不跳转
movb $0xd1, %al # 0xd1 -> port 0x64
outb %al, $0x64 # 0xd1 means: write data to 8042's P2 port
seta20.2:
inb $0x64, %al # Wait for not busy(8042 input buffer empty).
testb $0x2, %al # 第低二位还是1,重新执行
jnz seta20.2
movb $0xdf, %al # 0xdf -> port 0x60
outb %al, $0x60 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
# 全局描述符表:存放8字节的段描述符,段描述符包含段的属性。
# 段选择符:总共16位,高13位用作全局描述符表中的索引位,GDT的第一项总是设为0,
# 因此孔断选择符的逻辑地址会被认为是无效的,从而引起一个处理器异常。GDT表项
# 最大数目是8191个,即2^13 - 1.
# Switch from real to protected mode, using a bootstrap GDT
# and segment translation that makes virtual addresses
# identical to physical addresses, so that the
# effective memory map does not change during the switch.
lgdt gdtdesc # 设定gdt的基址和限制
movl %cr0, %eax # 读取控制寄存器ce0
orl $CR0_PE_ON, %eax # 保护模式标记为启动
movl %eax, %cr0
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
# 跳转到 kernel 代码段选择子,进入32位模式
ljmp $PROT_MODE_CSEG, $protcseg
.code32 # Assemble for 32-bit mode
protcseg:
# Set up the protected-mode data segment registers
# 告知各寄存器数据段选择子
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
# Set up the stack pointer and call into C. The stack region is from 0--start(0x7c00)
# 初始化栈帧,准备调用bootmain.c
movl $0x0, %ebp
movl $start, %esp
call bootmain
# If bootmain returns (it shouldn't), loop.
spin:
jmp spin
# Bootstrap GDT
.p2align 2 # force 4 byte alignment
gdt:
SEG_NULLASM # null seg
SEG_ASM(STA_X|STA_R, 0x0, 0xffffffff) # code seg for bootloader and kernel
SEG_ASM(STA_W, 0x0, 0xffffffff) # data seg for bootloader and kernel
gdtdesc:
.word 0x17 # sizeof(gdt) - 1
.long gdt # address gdt