湖南大学操作系统实验ucore lab1

阅读前注意事项:

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,同时输出一次。

 

运行结果:

猜你喜欢

转载自blog.csdn.net/yyd19981117/article/details/86692073
今日推荐