阅读前注意事项:
1、我的博客从lab2之后,如果没有特殊说明,所有标注的代码行数位置,以labcodes_answer(答案包)里的文件为准!!!因为你以后会发现做实验用meld软件比较费时费力,对于咱们学校的验收不如直接对着答案来。
2、感谢网上的各路前辈大佬们,本人在这学期初次完成实验的过程中,各位前辈们的博客给了我很多有用的指导;本人的博客内容在现有的内容上,做了不少细节的增补内容,有些地方属个人理解,如果有错在所难免,还请各位大佬们批评指正。
3、所有实验的思考题,我把它规整到了文章最后;
4、所有实验均默认不做challenge,对实验评分无影响。
一、实验内容
lab1中包含一个bootloader和一个OS。 这个bootloader可以切换到X86保护模式, 能够读磁盘并加载ELF执行文件格式, 并显示字符。 而这lab1中的OS只是一个可以处理时钟中断和显示字符的幼儿园级别OS
二、目的
操作系统是一个软件, 也需要通过某种机制加载并运行它。 在这里我们将通过另外一个更加简单的软件-bootloader来完成这些工作。 为此, 我们需要完成一个能够切换到x86的保护模式并显示字符的bootloader, 为启动操作系统ucore做准备。 lab1提供了一个非常小的bootloader和ucore OS, 整个bootloader执行代码小于512个字节, 这样才能放到硬盘的主引导扇区中。通过分析和实现这个bootloader和ucore OS, 读者可以了解到:
计算机原理,CPU的编址与寻址: 基于分段机制的内存管理,CPU的中断机制,外设:串口/并口/CGA, 时钟, 硬盘,Bootloader软件,编译运行bootloader的过程,调试bootloader的方法,PC启动bootloader的过程,ELF执行文件的格式和加载,外设访问:读硬盘, 在CGA上显示字符串,ucore OS软件,编译运行ucore OS的过程,ucore OS的启动过程,调试ucore OS的方法,函数调用关系:在汇编级了解函数调用栈的结构和处理过程,中断管理:与软件相关的中断处理,外设管理:时钟。
三、实验设计思想和流程
练习1:理解通过make生成执行文件的过程。
第一步:运行make “V=”,观察每一步的make指令,得到了以下所有的输出:
+ 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 //生成init.o
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 //生成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 //生成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 //生成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 //生成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
+ 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
+ ld bin/bootblock
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=/dev/zero of=bin/ucore.img count=10000
10000+0 records in
10000+0 records out
5120000 bytes (5.1 MB) copied, 0.189825 s, 27.0 MB/s
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
1+0 records in
1+0 records out
512 bytes (512 B) copied, 0.000529561 s, 967 kB/s
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
146+1 records in
146+1 records out
74871 bytes (75 kB) copied, 0.000827937 s, 90.4 MB/s
第二步:分析make生成ucore.img的全过程:(阅读Makefile代码)
正式生成ucore.img:Makefile第181行——184行:
$(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
从这里,我们观察到,若要生成ucore.img,需要生成kernel和bootblock,ucore依赖这两个文件。
生成kernel前的准备:Makefile第120行——149行:
# kernel
KINCLUDE += kern/debug/ \
kern/driver/ \
kern/trap/ \
kern/mm/
KSRCDIR += kern/init \
kern/libs \
kern/debug \
kern/driver \
kern/trap \
kern/mm
//122行——133行:将kern目录的目录前缀定义为kinckude,ksrcdir
KCFLAGS += $(addprefix -I,$(KINCLUDE))
//134行:为这些目录前缀加上-I 指令,提供交互模式
$(call add_files_cc,$(call listf_cc,$(KSRCDIR)),kernel,$(KCFLAGS))
//136行:通过call函数将后面变量依次连接赋值给add_files_cc
KOBJS = $(call read_packet,kernel libs)
//138行:通过call函数定义kobjs变量,它连接了read_pocket与kernl lib
# create kernel target
kernel = $(call totarget,kernel)
//139行——140行:call正式生成内核的函数
这里是生成Makefile之前的准备过程。
正式生成kernel:Makefile第143行——149行
$(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)
要生成kernel,观察上面make信息中标红的部分,需要用GCC编译器将kern目录下所有的.c文件全部编译生成的.o文件的支持。
生成bootblock:Makefile第155行——166行
# create bootblock
bootfiles = $(call listf_cc,boot)
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))
//157:生成bootblock需要的bootasm.o bootmain.o和sign
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)
生成bootblock之前的准备:生成bootblock需要bootasm.o、bootmain.o、sign,其过程分别为蓝、绿、紫部分。有了这三,就可以生成bootblock,如上面make信息中,标橙的部分。
Makefile其他部分的解释:
第188行——201行:收尾工作finish
$(call finish_all)
IGNORE_ALLDEPS = clean \
dist-clean \
grade \
touch \
print-.+ \
handin
ifeq ($(call match,$(MAKECMDGOALS),$(IGNORE_ALLDEPS)),0)
-include $(ALLDEPS)
endif
第201行之后:定义各种make目标
make中,相关的参数解释:
-ggdb 生成可供gdb使用的调试信息。这样才能用qemu+gdb来调试bootloader or ucore。
-m32 生成适用于32位环境的代码。我们用的模拟硬件是32bit的80386,所以ucore也要是32位的软件。
-gstabs 生成stabs格式的调试信息。这样要ucore的monitor可以显示出便于开发者阅读的函数调用栈信息
-nostdinc 不使用标准库。标准库是给应用程序用的,我们是编译ucore内核,OS内核是提供服务的,所以所有的服务要自给自足。
-fno-stack-protector 不生成用于检测缓冲区溢出的代码。这是for 应用程序的,我们是编译内核,ucore内核好像还用不到此功能。
-Os 为减小代码大小而进行优化。根据硬件spec,主引导扇区只有512字节,我们写的简单bootloader的最终大小不能大于510字节。
-I<dir> 添加搜索头文件的路径
-fno-builtin 除非用__builtin_前缀,否则不进行builtin函数的优化。
第三步:分析一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
问题一的Makefile中曾经提到,bootloader.o文件经过sign.o的操作后,变成符合规范的引导文件。
分析源代码,tools/sign.c(行数:17——34)
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];
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);
buf[510] = 0x55;
buf[511] = 0xAA;
FILE *ofp = fopen(argv[2], "wb+");
size = fwrite(buf, 1, 512, ofp);
答案:一个符合规范的引导扇区应当不大于512字节,且最后两个位一定是0x55和0xAA。
练习2:使用qemu执行并调试lab1中的软件。
我们主要通过硬件模拟器qemu来进行各种实验。在实验的过程中我们可能会遇上各种各样的问题,调试是必要的。qemu支持使用gdb进行的强大而方便的调试。所以用好qemu和gdb是完成各种实验的基本要素。默认的gdb需要进行一些额外的配置才进行qemu的调试任务。qemu和gdb之间使用网络端口1234进行通讯。
第一步:调试0x7c00处代码指令并和bootasm比较
将tools/gdbinit改为:
set architecture i8086 //设置i8086执行模式
target remote :1234 //qemu和gdb使用本地端口1234通信
break *0x7c00 //设置断点
c
x/20i $pc //显示20条指令
continue
执行make debug,即可观察到从0x7c00处开始的20条指令,输入si可以继续单步调试。
如果要观察bootloader函数中的位置,直接break bootmain,就在bootmain函数处设置了一个断点。
如图所示:
因为20条指令太长,这里只节选了一部分,按下enter键,gdb会显示出余下的所有。
除一些表达上的差异,从0x7c00开始,代码没有发现明显区别。
表达差异一般是:操作的寄存器名称,有的用了更低的位表示(比如,mov eax 0x00,会变成movb al 0x00,改成更低位的表示)。
第二步:自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
改成break kern_init,其他一样:
需要加入反汇编指令,能看到当前位置的汇编。
练习3:分析bootloader进入保护模式的过程。
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 %ip=7c00.
.set PROT_MODE_CSEG, 0x8 # kernel code segment selector
.set PROT_MODE_DSEG, 0x10 # kernel data segment selector
.set CR0_PE_ON, 0x1 # protected mode enable flag
# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
.globl start
start:
.code16 # Assemble for 16-bit mode
cli # Disable interrupts
cld # String operations increment
//在16位下关闭中断,并设置字符串操作是递增方向
# Set up the important data segment registers (DS, ES, SS).
xorw %ax, %ax # Segment number zero
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.
//接下来是A20操作:为了兼容早期的PC机,第20根地址线在实模式下不能使用,所以超过1MB的地址,默认就会返回到地址0,重新从0循环计数,而下面的代码能打开A20地址线
//具体操作步骤
1. 等待8042 Input buffer为空;
2. 发送Write 8042 Output Port (P2)命令到8042 Input buffer;
3. 等待8042 Input buffer为空;
4. 将8042 Output Port(P2)得到字节的第2位置1,然后写入8042 Input buffer
seta20.1: //总体功能,通过将键盘控制器上的A20线置于高电位,全部32条地址线可用,可以访问4G的内存空间
inb $0x64, %al //从0x64端口读入一个字节的数据到al(eax寄存器的低8位)
testb $0x2, %al //检查最低的第2位是否为1,即键盘缓冲区是否为空
理论依据:我们只要操作8042芯片的输出端口(64h)的bit 1,就可以控制A20 Gate,但实际上,当你准备向8042的输入缓冲区里写数据时,可能里面还有其它数据没有处理,所以,我们要首先禁止键盘操作——来自参考书
jnz seta20.1 //如果上面的测试中发现al的第2位为0(00000010,代表键盘缓冲区为空),就不执行该指令,否则就循环检查(),即等待为空操作
movb $0xd1, %al //发送写8042输出端口的指令
outb %al, $0x64
seta20.2: //继续等待8042键盘控制器不忙
inb $0x64, %al
testb $0x2, %al
jnz seta20.2 //和之前一样,不忙了就可以出来
movb $0xdf, %al
outb %al, $0x60 //将al中的数据写入到0x60端口中,将全局描述符表描述符加载到全局描述符表寄存器
# 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
orl $CR0_PE_ON, %eax
movl %eax, %cr0
//cr0的第0位为1表示处于保护模式,为0表示处于实时模式,这里将CR0的第0位置1【在这里转换了保护模式】
# Jump to next instruction, but in 32-bit code segment.
# Switches processor into 32-bit mode.
ljmp $PROT_MODE_CSEG, $protcseg
.code32 //长跳转到32位代码段,重装CS、EIP、DS、ES等段寄存器等
# 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)
movl $0x0, %ebp
movl $start, %esp
call bootmain //转到保护模式完成,进入boot主函数
# 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
总结流程概况:
先设置寄存器ax,ds,es,ss寄存器值清0;地址线20被封锁,高于1MB的地址都默认回卷到0。激活A20的方法是,由于历史原因A20地址位由键盘控制器芯片8042管理。所以要给8042发命令激活A20。
8042有两个IO端口:0x60和0x64,激活流程位:发送0xd1命令到0x64端口之后,发送0xdf到0x60。
从实模式转换到保护模式,用到了全局描述符表和段表,使得虚拟地址和物理地址匹配,保证转换时有效的内存映射不改变;lgdt汇编指令把GDTR描述符表的大小和起始位置存入gdtr寄存器中;将CR0的最后一位设置为1,进入保护模式;指令跳转由代码段跳到protcseg的起始位置。
设置保护模式下数据段寄存器;设置堆栈寄存器并调用main函数;对GDT作处理。
开启A20的用途和方法:通过将键盘控制器上的A20线置于高电位,全部32条地址线可用,可以访问4G的内存空间。
初始化GDT表:一个简单的GDT表和其描述符已经静态储存在引导区中,载入即可 lgdt gdtdesc
进入保护模式:通过将cr0寄存器PE位置1便开启了保护模式 movl %cr0, %eax orl $CR0_PE_ON, %eax movl %eax, %cr0
练习4:分析bootloader加载ELF格式的OS的过程。
第一步:分析bootloader如何读取硬盘扇区的?
从bootmain源代码分析
void bootmain(void) {
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
//从硬盘读取第一页(读到内存的位置,大小,ELF文件偏移)
//判断是否为合法的ELF文件
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
//定义两个程序头表段,其中ph表示ELF段表首地址;eph表示ELF段表末地址
// load each program segment (ignores ph flags)
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);
} //循环读每个段
// call the entry point from the ELF header
// note: does not return
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
//调用头表中的内核入口地址实现内核链接地址转化为加载地址,无返回值
bad: //这里是读取过程中如果出现了错误,如何处理
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
/* do nothing */
while (1);
}
读取文件的模式为,readseg函数首先读取硬盘扇区,而readseg函数则循环调用了真正读取硬盘扇区的函数readsect来每次读出一个扇区,而readsect如下:
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); //读取一个扇区
}
第二步:分析bootloader是如何加载ELF格式的OS?
过程类似于刚刚分析过的:
//第一步:wait_disk检查硬盘是否就绪
(检查0x1F7的最高两位,如果是01,则跳出循环;否则等待)
static void waitdisk(void) {
while ((inb(0x1F7) & 0xC0) != 0x40)
/* do nothing */;
}
static void readsect(void *dst, uint32_t secno) {
//等待磁盘准备就绪
waitdisk();
outb(0x1F2, 1); #count = 1 //第二步:读取一个扇区的相关信息
outb(0x1F3, secno & 0xFF); //要读取的扇区编号
outb(0x1F4, (secno >> 8) & 0xFF); //用来存放读写柱面的低 8位字节
outb(0x1F5, (secno >> 16) & 0xFF); //用来存放读写柱面的高 2位字节
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); //用来存放要读/写的磁盘号及磁头号
outb(0x1F7, 0x20);
waitdisk();
insl(0x1F0, dst, SECTSIZE / 4);
}
bootmain(void) {
//首先判断是不是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;
//第三步:按照程序头表的描述,将ELF文件中的数据载入内存
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
//根据ELF头表中的入口信息,找到内核的入口并开始运行
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
}
总结:读取ELF是一个三层的调用过程
1、等待磁盘准备就绪;
2、读取一个扇区的编号和数据信息;
3、按照程序头表的描述,将ELF文件中的数据载入内存;
练习5:实现函数调用堆栈跟踪函数 (需要编程)
栈是一个很重要的编程概念(编译课和程序设计课都讲过相关内容),与编译器和编程语言有紧密的联系。理解调用栈最重要的两点是:栈的结构,EBP寄存器的作用。一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作(由硬件完成)。
一般而言,ss:[ebp+4]处为返回地址,ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用4字节内存),ss:[ebp-4]处为第一个局部变量,ss:[ebp]处为上一层ebp值。由于ebp中的地址处总是“上一层函数调用时的ebp值”,而在每一层函数调用中,都能通过当时的ebp值“向上(栈底方向)”能获取返回地址、参数值,“向下(栈顶方向)”能获取函数局部变量值。如此形成递归,直至到达栈底。这就是函数调用栈。
代码实现:
uint32_t ebp=read_ebp(); //调用read ebp访问当前ebp的值,数据类型为32位。
uint32_t eip=read_eip(); //调用read eip访问eip的值,数据类型同。
int i; //这里有个细节问题,就是不能for int i,这里面的C标准似乎不允许
for(i=0;i<STACKFRAME_DEPTH&&ebp!=0;i++)
{
//(3) from 0 .. STACKFRAME_DEPTH
cprintf("ebp:0x%08x eip:0x%08x ",ebp,eip);//(3.1)printf value of ebp, eip
uint32_t *tmp=(uint32_t *)ebp+2;
cprintf("arg :0x%08x 0x%08x 0x%08x 0x%08x\n",*(tmp+0),*(tmp+1),*(tmp+2),*(tmp+3));
//(3.2)(uint32_t)calling arguments [0..4] = the contents in address (unit32_t)ebp +2 [0..4]
//因为使用的是栈数据结构,因此可以直接根据ebp就能读取到各个栈帧的地址和值,ebp+4处为返回地址,ebp+8处为第一个参数值(最后一个入栈的参数值,对应32位系统),ebp-4处为第一个局部变量,ebp处为上一层 ebp 值。
//而这里,*代表指针,指针也是占用4个字节,因此可以直接对于指针加一,地址加4。
print_debuginfo(eip-1);
eip=((uint32_t *)ebp)[1];
ebp=((uint32_t *)ebp)[0];
//最后更新ebp:ebp=ebp[0],更新eip:eip=ebp[1],因为ebp[0]=ebp,ebp[1]=ebp[0]+4=eip。
为什么最后需要更新一下ebp和eip的值:因为这里在对栈进行一个操作,有可能会有出栈入栈等行为,导致栈指针或寄存器内的值发生变化,因此上一次栈操作的指针不能用到下一次,因此需要及时更新,且eip=ebp+4,理论依据是:在代码的第253行read_eip()函数中,eip的值就是基于ebp读出的,而读取eip的位置,正是eip+4。
运行结果如下:
练习6:完善中断初始化和处理 (需要编程)
第一步:分析中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
答:中断描述符表一个表项占8字节。其中0~15位和48~63位分别为offset的低16位和高16位。16~31位为段选择子。通过段选择子获得段基址,加上段内偏移量即可得到中断处理代码的入口。
第二步:请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
Init:
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指令加载中断描述符表
解释:
1、定义宏:查看mmu.h中的SETGATE宏:#define SETGATE(gate, istrap, sel, off, dpl)
主要使用这个宏进行段选择符的构造:
gate:为相应的idt数组内容,处理函数的入口地址
istrap:系统段设置为1,中断门设置为0
sel:段选择子,这里是GO——KTEXT
#define GD_KTEXT ((SEG_KTEXT) << 3) // kernel text
off:为__vectors数组内容,存在vectors.s中,支持256个中断
dpl:设置优先级,0为内核级,3为用户级
2、其他部分难度较小,按照注释来即可。
第三步:请编程完善trap.c中的中断处理函数trap
补充trap:
case IRQ_OFFSET + IRQ_TIMER:
ticks ++;
if (ticks ==TICK_NUM)
{
ticks-=TICK_NUM;
print_ticks();
}
break;
解释:
这里要每中断到100次就调用一个TICK_NUM输出信息,但是print ticks函数已经是现成的了,因此可以直接用,补充switch即可。
每次中断ticks计数加一,到了100,就回到0,同时输出一次。
运行结果: