ucore lab1

ucore lab 1 练习1~6

列出本实验各练习中对应的OS原理的知识点,并说明本实验中的实现部分如何对应和体现了原理中的基本概念和关键知识点。
操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
等完成lab1后再回头总结。
首先具体分析Makefile的执行流程,然后再回答题目所问的ucore.img的生成过程。
第1~139行大部分是设置环境变量、编译选项等,其中关键是第117行和第136行,分别设置了libs和kern目录下的obj文件名,两者合并即为 ( K O B J S ) 。 第 117 行 语 句 是 (KOBJS)。 第117行语句是 (KOBJS)117(call add_files_cc, ( c a l l l i s t f c c , (call listf_cc, (calllistfcc,(LIBDIR)),libs,),可见是调用了add_files_cc函数,输入参数有2个,第2个是libs(目录名),第1个是调用另一个函数listf_cc的返回值

listf_cc函数的定义为listf_cc = ( c a l l l i s t f , (call listf, (calllistf,(1), ( C T Y P E ) ) , 可 见 l i s t f c c 又 调 用 了 l i s t f 函 数 , 调 用 时 传 入 的 第 1 个 参 数 为 (CTYPE)),可见listf_cc又调用了listf函数,调用时传入的第1个参数为 (CTYPE))listfcclistf1(1) = ( L I B D I R ) = l i b s , 第 2 个 参 数 为 (LIBDIR) = libs,第2个参数为 (LIBDIR)=libs2(CTYPE) = c S

listf函数的定义为listf = $(filter $(if ( 2 ) , (2), (2),(addprefix %.,$(2)),%), $(wildcard $(addsuffix ( S L A S H ) ∗ , (SLASH)*, (SLASH),(1)))),将输入参数代入得:listf = $(filter %.c %.S, libs/*),可见此处调用listf的返回结果为libs目录下的所有.c和.S文件。由于lab1的libs目录下只有.h和.c文件,因此最终返回.c文件。

这时,第117行语句可化简为add_files_cc(libs/*.c, libs)

add_files_cc的定义为add_files_cc = ( c a l l a d d f i l e s , (call add_files, (calladdfiles,(1), ( C C ) , (CC), (CC),(CFLAGS) ( 3 ) , (3), (3),(2),$(4)),结合4可化简为add_files(libs/*.c, gcc, $(CFLAGS), libs)

add_files的定义为add_files = $(eval ( c a l l d o a d d f i l e s t o p a c k e t , (call do_add_files_to_packet, (calldoaddfilestopacket,(1), ( 2 ) , (2), (2),(3), ( 4 ) , (4), (4),(5))),而do_add_files_to_packet的定义为:

define do_add_files_to_packet
__temp_packet__ := $(call packetname,$(4))
ifeq ($$(origin $$(__temp_packet__)),undefined)
$$(__temp_packet__) :=
endif
__temp_objs__ := $(call toobj,$(1),$(5))
$$(foreach f,$(1),$$(eval $$(call cc_template,$$(f),$(2),$(3),$(5))))
$$(__temp_packet__) += $$(__temp_objs__)
endef

packetname的定义为packetname = (if KaTeX parse error: Expected group after '_' at position 60: …,其中(OBJPREFIX)=_̲_objs_,而(1)=libs,因此__temp_packet_ = _objs_libs
toobj的定义为toobj = (addprefix (OBJDIR)(SLASH)(if $(2),(2)(SLASH)), (addsuffix .o,basename (1)))),其中(OBJDIR)=obj, (SLASH)=/,而输入参数为(1)=libs/*.c, $(5)=’’,因此__temp_objs
= obj/libs/.o
综上,第117行的最终效果是__objs_libs = obj/libs/**/
.o

生成过程与第117行类似,不再赘述。第136行的实际效果是__objs_kernel = obj/kern/**/*.o
第140~151行是生成kernel文件。由于脚本中的语句往往会引用到前面定义的变量,而前面定义的变量又可能引用到其他文件的变量,为便于分析,下面会将所有相关的语句集中放在一起。
第141行设置生成的kernel目标名为bin/kernel

kernel = $(call totarget,kernel)
totarget = $(addprefix $(BINDIR)$(SLASH),$(1))
BINDIR  := bin
SLASH   := /

第143行指出kernel目标文件需要依赖tools/kernel.ld文件,而kernel.ld文件是一个链接脚本,其中设置了输出的目标文件的入口地址及各个段的一些属性,包括各个段是由输入文件的哪些段组成、各个段的起始地址等。

$(kernel): tools/kernel.ld

第145行指出kernel目标文件依赖的obj文件。最终效果为KOBJS=obj/libs/.o obj/kern/**/.o。

$(kernel): $(KOBJS)
KOBJS   = $(call read_packet,kernel libs)
read_packet = $(foreach p,$(call packetname,$(1)),$($(p)))
packetname = $(if $(1),$(addprefix $(OBJPREFIX),$(1)),$(OBJPREFIX))
OBJPREFIX   := __objs_

第146行打印kernel目标文件名

@echo + ld $@
// output: `+ ld bin/kernel`

第147行是链接所有生成的obj文件得到kernel文件

$(V)$(LD) $(LDFLAGS) -T tools/kernel.ld -o $@ $(KOBJS)
V       := @
LD      := $(GCCPREFIX)ld
// GCCPREFIX = 'i386-elf-' or ''
// output: ld -m    elf_i386 -nostdlib -T tools/kernel.ld -o bin/kernel  obj/kern/init/init.o obj/kern/libs/stdio.o obj/kern/libs/readline.o obj/kern/debug/panic.o obj/kern/debug/kdebug.o obj/kern/debug/kmonitor.o obj/kern/driver/clock.o obj/kern/driver/console.o obj/kern/driver/picirq.o obj/kern/driver/intr.o obj/kern/trap/trap.o obj/kern/trap/vectors.o obj/kern/trap/trapentry.o obj/kern/mm/pmm.o  obj/libs/string.o obj/libs/printfmt.o

第148行是使用objdump工具对kernel目标文件反汇编,以便后续调试。首先toobj返回obj/kernel.o,然后cgtype返回obj/kernel.asm,所以第148行相当于执行objdump -S bin/kernel > obj/kernel.asm,objdump的-S选项是交替显示将C源码和汇编代码。

@$(OBJDUMP) -S $@ > $(call asmfile,kernel)
OBJDUMP := $(GCCPREFIX)objdump
// GCCPREFIX = 'i386-elf-' or ''
asmfile = $(call cgtype,$(call toobj,$(1)),o,asm)
cgtype = $(patsubst %.$(2),%.$(3),$(1))
toobj = $(addprefix $(OBJDIR)$(SLASH)$(if $(2),$(2)$(SLASH)),\
        $(addsuffix .o,$(basename $(1))))
OBJDIR  := obj
SLASH   := /

第149行是使用objdump工具来解析kernel目标文件得到符号表。如果不关注格式处理,实际执行语句等效于objdump -t bin/kernel > obj/kernel.sym。

@$(OBJDUMP) -t $@ | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call sy    mfile,kernel)
OBJDUMP := $(GCCPREFIX)objdump
SED     := sed
symfile = $(call cgtype,$(call toobj,$(1)),o,sym)

第151行是调用create_target函数:$(call create_target,kernel),而create_target的定义为create_target = $(eval ( c a l l d o c r e a t e t a r g e t , (call do_create_target, (calldocreatetarget,(1), ( 2 ) , (2), (2),(3), ( 4 ) , (4), (4),(5))),可见create_target只是进一步调用了do_create_target的函数:do_create_target(kernel)

// add packets and objs to target (target, #packes, #objs[, cc, flags])
define do_create_target
__temp_target__ = $(call totarget,$(1))
__temp_objs__ = $$(foreach p,$(call packetname,$(2)),$$($$(p))) $(3)
TARGETS += $$(__temp_target__)
ifneq ($(4),)
$$(__temp_target__): $$(__temp_objs__) | $$$$(dir $$$$@)
    $(V)$(4) $(5) $$^ -o $$@
else
$$(__temp_target__): $$(__temp_objs__) | $$$$(dir $$$$@)
endif
endef

第156行:bootfiles = $(call listf_cc,boot),前面已经知道listf_cc函数是过滤出对应目录下的.c和.S文件,因此bootfiles=boot/*.c boot/*.S
第157行:从字面含义也可以看出是编译bootfiles生成.o文件。

$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))
cc_compile = $(eval $(call do_cc_compile,$(1),$(2),$(3),$(4)))
define do_cc_compile
$$(foreach f,$(1),$$(eval $$(call cc_template,$$(f),$(2),$(3),$(4))))
endef

第159行:bootblock = (call totarget,bootblock),前面已经知道totarget函数是给输入参数增加前缀"bin/",因此bootblock=“bin/bootblock”
第161行声明bin/bootblock依赖于obj/boot/*.o 和bin/sign文件:(bootblock): (call toobj,(bootfiles)) | (call totarget,sign)。注意toobj函数的作用是给输入参数增加前缀obj/,并将文件后缀名改为.o
第163行链接所有.o文件以生成obj/bootblock.o:(V)(LD) (LDFLAGS) -N -e start -Ttext 0x7C00 ^ -o (call toobj,bootblock)。这里要注意链接选项中的-e start -Ttext 0x7C00,大致意思是设置bootblock的入口地址为start标签,而且start标签的地址为0x7C00.(未理解-Ttext的含义)
第164行反汇编obj/bootblock.o文件得到obj/bootblock.asm文件:@(OBJDUMP) -S (call objfile,bootblock) > (call asmfile,bootblock)
第165行使用objcopy将obj/bootblock.o转换生成obj/bootblock.out文件,其中-S表示转换时去掉重定位和符号信息:@(OBJCOPY) -S -O binary (call objfile,bootblock) (call outfile,bootblock)
第166行使用bin/sign工具将obj/bootblock.out转换生成bin/bootblock目标文件:@(call totarget,sign) (call outfile,bootblock) (bootblock),从tools/sign.c代码中可知sign工具其实只做了一件事情:将输入文件拷贝到输出文件,控制输出文件的大小为512字节,并将最后两个字节设置为0x55AA(也就是ELF文件的magic number)
第168行调用了create_target函数(call create_target,bootblock),根据上文的分析,由于只有一个输入参数,此处函数调用应该也是直接返回,啥也没干。
第173行调用了add_files_host函数:(call add_files_host,tools/sign.c,sign,sign)
add_files_host的定义为add_files_host = (call add_files(1),(HOSTCC),(HOSTCFLAGS),(2),(3)),可见是调用了add_files函数:add_files(tools/sign.c, gcc, (HOSTCFLAGS), sign, sign)
add_files的定义为add_files = (eval $(call do_add_files_to_packet,(1),(2),(3),(4),(5))),根据前面的分析,do-add-filesto–packet的作用是生成obj文件,因此这里调用add_files的作用是设置__objs_sign = obj/sign/tools/sign.o
第174行调用了create_target_host函数:(call create_target_host,sign,sign)
create_target_host的定义为create_target_host = (call create_target,(1),(2),(3),(HOSTCC),(HOSTCFLAGS)),可见是调用了create_target函数:create_target(sign, sign, gcc, $(HOSTCFLAGS))
create_target的定义为create_target = $(eval $(call do_create_target,(1),(2),(3),(4),(5)))。根据前面的分析,do_create_target的作用是生成目标文件,因此这里调用create_target的作用是生成obj/sign/tools/sign.o
第179行设置了ucore.img的目标名:UCOREIMG := (call totarget,ucore.img),前面已经知道totarget的作用是添加bin/前缀,因此UCOREIMG = bin/ucore.img
第181行指出bin/ucore.img依赖于bin/kernel和bin/bootblock:(UCOREIMG): $(kernel) ( b o o t b l o c k ) 第 182 行 : (bootblock) 第182行: (bootblock)182(V)dd if=/dev/zero of=@ count=10000。这里为bin/ucore.img分配10000个block的内存空间,并全部初始化为0。由于没指定block的大小,因此为默认值512字节,则总大小为5000M,约5G。
备注:在类UNIX 操作系统中, /dev/zero 是一个特殊的文件,当你读它的时候,它会提供无限的空字符(NULL, ASCII NUL, 0x00)。其中的一个典型用法是用它提供的字符流来覆盖信息,另一个常见用法是产生一个特定大小的空白文件。BSD就是通过mmap把/dev/zero映射到虚地址空间实现共享内存的。可以使用mmap将/dev/zero映射到一个虚拟的内存空间,这个操作的效果等同于使用一段匿名的内存(没有和任何文件相关)。
第183行:(V)dd if=(bootblock) of=@ conv=notrunc。这里将bin/bootblock复制到bin/ucore.img
第184行:(V)dd if=(kernel) of=@ seek=1 conv=notrunc。继续将bin/kernel复制到bin/ucore.img,这里使用了选项seek=1,意思是:复制时跳过bin/ucore.img的第一个block,从第2个block也就是第512个字节后面开始拷贝bin/kernel的内容。原因是显然的:ucore.img的第1个block已经用来保存bootblock的内容了。
第186行:$(call create_target,ucore.img),由于只有一个输入参数,因此这里会直接返回。
编译libs和kern目录下所有的.c和.S文件,生成.o文件,并链接得到bin/kernel文件
编译boot目录下所有的.c和.S文件,生成.o文件,并链接得到bin/bootblock.out文件
编译tools/sign.c文件,得到bin/sign文件
利用bin/sign工具将bin/bootblock.out文件转化为512字节的bin/bootblock文件,并将bin/bootblock的最后两个字节设置为0x55AA
为bin/ucore.img分配5000MB的内存空间,并将bin/bootblock复制到bin/ucore.img的第一个block,紧接着将bin/kernel复制到bin/ucore.img第二个block开始的位置

练习二实验报告
1.从CPU加电后执行的第一条指令开始,单步跟踪BIOS:
1.1默认的gdb需要进行一些额外的配置才能进行qemu的调试任务,qemu和gdb之间使用网络端口1234进行通信。
lab1/tools/gdbinit的内容如下。可见,这里是对内核代码进行调试,并且将断点设置在内核代码的入口地址,即kern_init函数:
在这里插入图片描述
为了从CPU加电后执行的第一条指令开始调试,修改lab1/tools/gdbinit,内容为:
在这里插入图片描述
1.2 通过上一个步骤gdb可以连接qemu,此时qemu会进入停止状态,听从gdb的命令。另外,可能需要qemu在一开始便进入等待模式,则不再使用make qemu开始系统的运行,而使用make debug来完成这项工作,并且将gdbinit中的‘continue’指令删掉。这样qemu便不会在gdb未连接的时候擅自运行了。在lab1目录下执行’make debug’ :
在这里插入图片描述
然后弹出qemu和debug窗口(在Makefile文件中定义):
在这里插入图片描述
但我这里在本应该显示反汇编代码的地方显示为‘??’,在网上查了之后觉得可能的原因是qemu和gdb不兼容,解决办法是在gdb中添加:
在这里插入图片描述
![在这里插入图片描述](https://img-blog.csdnimg.cn/20191110201753807.png
该命令可以强制反汇编当前ip所指向的指令,效果如下:
在这里插入图片描述
使用si指令单步执行一条机器指令,实现单步跟踪BIOS:
在这里插入图片描述
2.在初始化位置0x7c00设置实地址断点,测试断点正常:
在gdb中输入:‘b *0x7c00’ //设置断点
      ’c‘ //继续正在调试的程序
     在这里插入图片描述
可以看到程序运行到0x7c00处停下来,并显示当ip指向的汇编指令–’cli‘。
3.从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和bootblock.asm进行比较:
在断点0x7c00处多次执行’x /10i $pc’ //查看后面10条汇编指令
在这里插入图片描述
发现在执行’bootmain‘之前的指令与bootasm.S和bootblock.asm中的指令一致。
4.自己找一个bootloader或内核中的代码位置,设置断点并进行测试:
4.1将gdbinit改成原来的命令,即对内核代码进行调试,并且将断点设置在内核代码的入口地址,即kern_init函数;
4.2执行’make debug‘; //程序进入init.c中的kern_init()
4.3‘b cons_init’ //设置断点,这是初始化console的函数
’c‘ //继续执行正在调试的程序
4.4遇到断点,在console.c程序的cons_init()函数处终止:
在这里插入图片描述
练习3.分析bootload进入保护模式的过程
0. BIOS通过读取硬盘主引导扇区到内存,并跳转到对应内存中的位置,也就是从’%cs=0 $pc=0x7c00‘进入并执行bootloader,bootloader要完成:
(1) 切换到保护模式,启用分段机制;
(2) 读取磁盘中的ELF执行文件格式的ucore操作系统到内存;
(3) 显示字符串信息;
(4) 把控制权交给ucore操作系统;
对应于boot目录下的 asm.h,bootasm.S,bootmain.c文件。bootasm.S的start函数文件是最先执行的,此函数进行了一定的初始化,完成从实模式到保护模式的转换,并调用bootmain.c中的bootmain函数。bootmain函数实现了屏幕、串口和并口显示字符串,加载ucore到内存,然后跳转到ucore的入口处执行。asm.h中是bootasm.S所需要的头文件,主要是一些与x86保护模式的段访问方式相关的宏定义。
所谓实模式,是将整个物理内存看成分段的取余,程序的数据和代码位于不同区域,操作系统和用户程序没有区别对待,而且每个指针都是指向实际的物理地址,若通过指针更改了操作系统或其他用户程序取余,会带来很大的灾难,此模式下的80386只是一个快速的8086。
所谓保护模式,80386的全部32根地址线全部有效,可寻址高达4GB的线性地址空间和物理地址空间,可访问64TB的逻辑地址空间,可采用分段存储管理机制和分页存储管理机制,提供4个特权级和完善的特权检查机制,既能实现资源共享又能保证代码数据的安全及任务的隔离。

  1. 初始化:
    首先调用bootasm.S中的start函数进行初始化,屏蔽中断,置位向量标志位,置位几个重要的段寄存器。
    在这里插入图片描述

  2. 开启A20:
    当A20地址线控制禁止时,程序就像运行在8086上,1MB以上的地址是不可访问的,为了使能所有地址位的寻址能力,必须向键盘控制器8082发送一个命令,键盘控制器8042会将A20线置于高电位,使全部32条地址线可用,实现访问4GB内存。开启A20的具体步骤如下:
    (1) 等待8042 Input Buffer为空。
    (2) 发送Write 8042 Output Port (P2)命令到8042 Input Buffer。
    (3) 等待8042 Input Buffer为空。
    (4) 将8042 Outpput Port (P2)得到字节的第2位置1,然后写入8042 Input Buffer。
    具体实现在bootasm.S文件中:
    在这里插入图片描述

  3. 初始化全局描述符表:
    为了使分段存储管理机制正常运行,需要建立好段描述符和段描述符表,全局描述符表是一个保存多个段描述符的“数组”,其起始地址保存在全局描述符表寄存器GDTR中。GDTR长48位,其中高32位为基地址,低16位为段界限。这里只需要载入已经静态存储在引导区的GDT表和其描述符到GDTR寄存器:
    在这里插入图片描述

  4. 进入保护模式:
    CR0的位0(PE)是启用保护(Protection Enable)标志。当设置该位时即开启了保护模式;当复位时即进入实地址模式。这个标志仅开启段级保护,而并没有启用分页机制。若要启用分页机制,那么PE和PG标志都要置位。所以这里需要将cr0的PE位置1:
    在这里插入图片描述

  5. 通过长跳转指令进入保护模式:
    在这里插入图片描述

  6. 置位段寄存器,建立堆栈:在这里插入图片描述

  7. 完成实模式到保护模式的转换,调用bootmain.c中的bootmain函数:在这里插入图片描述

实验4–分析bootloader加载ELF格式的OS的过程
通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,理解:
1.bootloader如何读取硬盘扇区的?
2.bootloader是如何加载ELF格式的OS?
阅读材料其实已经给出了读一个扇区的大致流程:
1.等待磁盘准备好
2.发出读取扇区的命令
3.等待磁盘准备好
4.把磁盘扇区数据读到指定内存
实际操作中,需要知道怎样与硬盘交互。阅读材料中同样给出了答案:所有的IO操作是通过CPU访问硬盘的IO地址寄存器完成。硬盘共有8个IO地址寄存器,其中第1个存储数据,第8个存储状态和命令,第3个存储要读写的扇区数,第4~7个存储要读写的起始扇区的编号(共28位)。了解这些信息,就不难编程实现啦。
bootloader读取扇区的功能是在boot/bootmain.c的readsect函数中实现的,先贴代码:

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.等待硬盘空闲。waitdisk的函数实现只有一行:while ((inb(0x1F7) & 0xC0) != 0x40),意思是不断查询读0x1F7寄存器的最高两位,直到最高位为0、次高位为1(这个状态应该意味着磁盘空闲)才返回。
2.硬盘空闲后,发出读取扇区的命令。对应的命令字为0x20,放在0x1F7寄存器中;读取的扇区数为1,放在0x1F2寄存器中;读取的扇区起始编号共28位,分成4部分依次放在0x1F3~0x1F6寄存器中。
3.发出命令后,再次等待硬盘空闲。
4.硬盘再次空闲后,开始从0x1F0寄存器中读数据。注意insl的作用是"That function will read cnt dwords from the input port specified by port into the supplied output array addr.",是以dword即4字节为单位的,因此这里SECTIZE需要除以4.
首先从原理上分析加载流程。
1.bootloader要加载的是bin/kernel文件,这是一个ELF文件。其开头是ELF header,ELF Header里面含有phoff字段,用于记录program header表在文件中的偏移,由该字段可以找到程序头表的起始地址。程序头表是一个结构体数组,其元素数目记录在ELF Header的phnum字段中。
2.程序头表的每个成员分别记录一个Segment的信息,包括以下加载需要用到的信息:
uint offset; // 段相对文件头的偏移值,由此可知怎么从文件中找到该Segment
uint va; // 段的第一个字节将被放到内存中的虚拟地址,由此可知要将该Segment加载到内存中哪个位置
uint memsz; // 段在内存映像中占用的字节数,由此可知要加载多少内容
3.根据ELF Header和Program Header表的信息,我们便可以将ELF文件中的所有Segment逐个加载到内存中
bootloader加载os的功能是在bootmain函数中实现的,先贴代码:

void bootmain(void) {
    // read the 1st page off disk
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // is this a valid ELF?
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }

    struct proghdr *ph, *eph;

    // 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))();
}

1.首先从硬盘中将bin/kernel文件的第一页内容加载到内存地址为0x10000的位置,目的是读取kernel文件的ELF Header信息。
2.校验ELF Header的e_magic字段,以确保这是一个ELF文件
3.读取ELF Header的e_phoff字段,得到Program Header表的起始地址;读取ELF Header的e_phnum字段,得到Program Header表的元素数目。
4.遍历Program Header表中的每个元素,得到每个Segment在文件中的偏移、要加载到内存中的位置(虚拟地址)及Segment的长度等信息,并通过磁盘I/O进行加载
5.加载完毕,通过ELF Header的e_entry得到内核的入口地址,并跳转到该地址开始执行内核代码
调试代码
输入make debug启动gdb,并在bootmain函数入口处即0x7d0d设置断点,输入c跳到该入口
单步执行几次,运行到call readseg处,由于该函数会反复读取硬盘,为节省时间,可在下一条语句设置断点,避免进入到readseg函数内部反复执行循环语句。(或者直接输入n即可,不用这么麻烦)
执行完readseg后,可以通过x/xw 0x10000查询ELF Header的e_magic的值,查询结果如下,确实与0x464c457f相等,所以校验成功。注意,我们的硬件是小端字节序(这从asm文件的汇编语句和二进制代码的对比中不难发现),因此0x464c45实际上对应字符串"elf",最低位的0x7f字符对应DEL。

(gdb) x/xw 0x10000
0x10000:        0x464c457f

继续单步执行,由0x7d2f mov 0x1001c,%eax可知ELF Header的e_phoff字段将加载到eax寄存器,0x1001c相对0x10000的偏移为0x1c,即相差28个字节,这与ELF Header的定义相吻合。执行完0x7d2f处的指令后,可以看到eax的值变为0x34,说明program Header表在文件中的偏移为0x34,则它在内存中的位置为0x10000 + 0x34 = 0x10034.查询0x10034往后8个字节的内容如下所示:

(gdb) x/8xw 0x10034
0x10034:        0x00000001      0x00001000      0x00100000      0x00100000
0x10044:        0x0000dac4      0x0000dac4      0x00000005      0x00001000

可以结合代码中定义的Program Header结构来理解这8个字节的含义。

struct proghdr {
    uint32_t p_type;   // loadable code or data, dynamic linking info,etc.
    uint32_t p_offset; // file offset of segment
    uint32_t p_va;     // virtual address to map segment
    uint32_t p_pa;     // physical address, not used
    uint32_t p_filesz; // size of segment in file
    uint32_t p_memsz;  // size of segment in memory (bigger if contains bss)
    uint32_t p_flags;  // read/write/execute bits
    uint32_t p_align;  // required alignment, invariably hardware page size
};

还可以使用readelf -l bin/kernel来查询kernel文件各个Segment的基本信息,以作对比。查询结果如下所示,可见与gdb调试结果是一致的。

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x001000 0x00100000 0x00100000 0x0dac4 0x0dac4 R E 0x1000
  LOAD           0x00f000 0x0010e000 0x0010e000 0x00aac 0x01dc0 RW  0x1000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10

继续单步执行,由0x7d34 movzwl 0x1002c,%esi可知ELF Header的e_phnum字段将加载到esi寄存器,执行完x07d34处的指令后,可以看到esi的值变为3,这说明一共有3个segment。
后面是通过磁盘I/O完成三个Segment的加载,不再赘述。
实验5–实现函数调用堆栈跟踪函数
需要完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。
一.函数堆栈
主要的两点在于栈的结构和ebp寄存器的作用。一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作(由硬件完成)。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:
在这里插入图片描述
这样在程序执行到一个函数的实际命令前,已经有以下数据顺序入栈:参数,返回地址,ebp寄存器。
函数调用的步骤:
1.参数入栈:将参数从右向左依次压入栈中。
   2. 返回地址入栈:call指令内部隐含的动作,将call的下一条指令入栈,由硬件 完成。
   3. 代码区跳转:跳转到被调用函数入口处。
   4. 函数入口处前两条指令,为本地编译器自动插入的指令,即将ebp寄存器入栈, 然后将栈顶指针esp赋值给ebp。
相反的,函数返回的步骤为:
1.保存返回值,通常将函数返回值保存到寄存器EAX中。
   2. 将当前的ebp赋给esp。
   3. 从栈中弹出一个值给ebp。
   4. 弹出返回地址,从返回地址处继续执行。
并且在函数调用过程中的ebp起着关键作用,从该地址向上(栈底方向)能依次获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的ebp的值,于是以此为线索可以形成递归,直至到达栈底。这就是函数调用栈。
二.print_stackframe函数的实现
由以上知识和源代码文件中的注释实现print_stackframe():
在这里插入图片描述
执行’make qemu’指令得到的结果为:
可以观察到显示结果与实验指导书上一致。对于最后一行:其对应的是第一个调用堆栈的函数,即bootmain.c中的bootmain函数,因为bootloader设置的堆栈从0x7c00开始,执行’call bootmain’转入bootmain函数。其ebp=0x00007bf8,此时的eip=0x00007d6e,其压入的4个参数分别为0xc031fcfa, 0xc08ed88e, 0x64e4d08e, 0xfa7502a8。
练习6–完善中断初始化和处理

  1. 中断向量表中一个表项占多少个字节?其中哪几位代表中断处理代码的入口?
    答:系统将所有的中断事件统一进行编号(0~255),这个编号称为中断向量。中断向量表的一个表项占8个字节,其结构如下:
    在这里插入图片描述

0~15位:偏移地址的0~15位;
16~31位:段选择子;
32~47位:属性信息(包括DPL等);
48~63位:偏移地址的16~31位。
其中第16~32位是段选择子,用于索引全局描述符表GDT来获取中断处理代码对应的 段地址,再加上第015、4863位构成的偏移地址,即可得到中断处理代码的入口。
2. 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。
答:分析如下,

  1. Ucore启动后,通过idt_init函数初始化IDT表,IDT表的每个元素均为門描述符,记录一个中断向量对应的中断处理函数的段选择子、偏移量和属性(门类型、DPL等),所以初始化IDT表就是初始化每个中断向量的这些属性。
  2. 除了系统调用(T_SYSCALL)的门类型为陷阱門、DPL=3(用户级权限)以外,其它终端的门类型均为中断門、DPL=0(内核级权限,即仅能够使用int 0x30指令)。
  3. vectors中存储了中断处理程序的入口程序和入口地址,即该数组中第i个元素对应第i个中断向量的中断处理函数地址。vectors定义在vector.S文件中,通过一个工具程序vector.c生成。而且由vector.S文件开头可知,中断处理函数属于.text的内容。因此,中断处理函数的段选择子即.text的段选择子GD_KTEXT。从kern/mm/pmm.c可知.text的段基址为0,因此中断处理函数地址的偏移量等于其地址本身。
  4. 使用mmu.h中的SETGATE宏来填充idt数组的内容,传递的参数有向量的首地址、門的类型、是否为系统调用、段选择子、偏移地址和DPL。
  5. 完成IDT表的初始化之后,还需执行’LIDT’命令将IDT表的起始地址加载到IDTR寄存器中。LIDT指令的作用:使用一个包含线性地址基址和界限的内存操作数来加载IDT。用来在OS创建IDT时设定IDT的起始地址。该指令只能在特权级0执行。
    根据上面分析,写出idt_init函数的源代码:
    在这里插入图片描述
  1. 请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。
    答:分析如下,
  1. Trap函数只是调用了trap_dispatch函数,而trap_dispatch函数实现了对各种中断的处理,这题只要我们完成对时钟中断的处理,也就是trap_dispatch函数中第一个case语句。
  2. 可以使用kern/driver/clock.c中的全局变量ticks记录当前始终中断次数,每次发生时钟中断则将ticks加一,如果加一之后ticks==100,则调用print_ticks子函数打印相关信息,并将ticks置0.
    经过以上分析,写出如下源代码:
    在这里插入图片描述
    实现效果:
    在lab1目录下执行’make qemu’,观察到如下结果,发现每过1s屏幕打印一次’100 ticks’,并且按下的键也会在屏幕上显示:
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值