OS课程 ucore_lab1实验报告


练习一:理解通过make生成执行文件的过程。

    列出本实验各练习中对应的OS原理的知识点,并说明本实验中的实现部分如何对应和体现了原理中的基本概念和关键知识点。
在此练习中,大家需要通过静态分析代码来了解:

  1. 操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地>解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致>的结果)
  1. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

1.1

  1. 生成<font color="#dd00dd">ucore.img</font>需要<font color="#dd00dd">kernel</font>和<font color="#dd00dd">bootblock</font>

生成<font color="#dd00dd">ucore.img</font>的代码如下:


$(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  
$(call create_target,ucore.img)  

首先先创建一个大小为1000字节的块,然后再将bootblock 复制过去。
生成<font color="#dd00dd">ucore.img</font>需要先生成<font color="#dd00dd">kernel</font>和<font color="#dd00dd">bootblock</font>
  2. 生成<font color="#dd00dd">kernel</font>的代码如下:

$(kernel): tools/kernel.ld
$(kernel): $(KOBJS)
    @echo "bbbbbbbbbbbbbbbbbbbbbb$(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)

通过<font color="#df00df">make V=</font>指令得到执行的具体命令如下:

+ 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

根据其中可以看到,要生成<font color="#dd00dd"> kernel</font>,需要GCC编译器将<font color="#dd00dd"> kern</font>目录下的.c文件全部编译生成层的.o文件的支持。具体声明:

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

  3.生成<font color="#dd00dd"> bootblock</font>:
代码如下:

$(bootblock): $(call toobj,$(bootfiles)) | $(call totarget,sign) 
    @echo "========================$(call toobj,$(bootfiles))"
    @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)

同样根据<font color="#0000df">make V=</font>指令打印的结果,得到要生成的<font color="#df00df">bootblock</font>,首先要生成<font color="#df00df">bootasm.o、bootmain.o、sign</font>,
代码如下:

bootfiles = $(call listf_cc,boot)
$(foreach f,$(bootfiles),$(call cc_compile,$(f),$(CC),$(CFLAGS) -Os -nostdinc))

由宏定义批量实现了。
而实际的命令在<font color="#df00df">make V=</font>的指令结果里可以看到。
下面是<font color="#df00df">bootasm.S</font>生成<font color="#df00df">bootasm.o</font>的具体命令:

gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o

下面是<font color="#df00df">bootmain.c</font>生成<font color="#df00df">bootmain.o</font>的具体命令:

gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o
查阅资料:

  --ggdb 生成可供fdb使用的调试信息
  --m32 生成适用于32位环境的代码
  --gstabs 生成stabs格式的调试信息
  -- nostdinc 不是有标准库
  --fno-stack-protector 不生成用于检测缓冲区溢出的代码
  --Os 为减少代码大小而进行优化
添加搜索头文件的路径
  --fno-builtin 不进行builtin函数的优化
下列代码为<font color="#df00df">sign</font>生成的代码:

$(call add_files_host,tools/sign.c,sign,sign)
$(call create_target_host,sign,sign)

下列是生成<font color="#df00df">sign</font>的具体的命令:

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

有了上述的<font color="#df00df">bootasm.o、bootmain.o、sign</font>。接下来就可以生成<font color="#df00df">block</font>了,实际命令如下:

ld -m    elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
参数解释:

  --m 模拟为i386上的连接器
  --N 设置代码段和数据段均为可读写
  --e 指定入口
  --Ttext 制定代码段开始位置

总结:

编译所有生成bin/kernel所需的文件
链接生成bin/kernel
编译bootasm.S  bootmain.c  sign.c
根据sign规范生成obj/bootblock.o
生成ucore.img

1.2

截取sign.c文件中的部分源码:

    char buf[512];  //定义buf数组
    memset(buf, 0, sizeof(buf));
      // 把buf数组的最后两位置为 0x55, 0xAA
    buf[510] = 0x55;  
    buf[511] = 0xAA;
    FILE *ofp = fopen(argv[2], "wb+");
    size = fwrite(buf, 1, 512, ofp);
    if (size != 512) {       //大小为512字节
        fprintf(stderr, "write '%s' error, 
                         size is %d.\n", argv[2], size);
        return -1;
    }

可知一个被系统认为是符合规范的硬盘主引导扇区的特征有以下几点:
  --磁盘主引导扇区只有512字节
  --磁盘最后两个字节为0x55AA
  --由不超过466字节的启动代码和不超过64字节的硬盘分区表加上两个字节的结束符构成。


练习二 使用qemu执行并调试lab1中的软件

为了熟悉使用qemu和gdb进行的调试工作,我们进行如下的小练习:

  1. 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
  2. 在初始化位置0x7c00设置实地址断点,测试断点正常。
  3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。
  4. 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
    从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。

  首先在CPU加电之后,CPU里面的ROM存储器会将其里面保存的初始值传给各个寄存器,其中CS:IP = 0Xf000 : fff0(CS:代码段寄存器;IP:指令寄存器),这个值决定了我们从内存中读数据的位置,PC = 16*CS + IP。

<div align=center>
图2.11

  此时系统处于实模式,并且截止到目前为止系统的总线还不是我们平常的32位,这时的地址总线只有20位,所以地址空间的总大小只有1M,而我们的BIOS启动固件就在这个1M的空间里面。
BIOS启动固件需要提供以下的一些功能:
  ☆基本输入输出的程序
  ☆系统设置信息
  ☆开机后自检程序
  ☆系统自启动程序
  在此我们需要找到CPU加电之后的第一条指令的位置,然后在这里break,单步跟踪BIOS的执行,根据PC = 16*CS + IP,我们可以得到PC = 0xffff0,所以BIOS的第一条指令的位置为0xffff0(在这里因为此时我们的地址空间只有20位,所以是0xffff0)。
  在这里我们利用make debug来观察BIOS的单步执行:

2.1

修改<font color="#df00df">lab1/tools/gdbinit</font>,内容为:

set architecture i8086
target remote :1234

然后在lab1执行:

make debug

在gdb的调试界面,执行如下命令:

si

来单步跟踪,在gdb的调试界面,执行如下命令来查看BIOS代码:

x /2i$pc

得到如下截图:
<div align=center>[图片上传失败...(image-b3508b-1571536103241)]

2.2

修改gdbinit文件:

set architecture i8086
target remote :1234
b *0x7c00
c
x/2i $pc

得到如下结果,正常:
<div align=center>[图片上传失败...(image-dde4bf-1571536103241)]

2.3

改写makefile文件:

debug: $(UCOREIMG)
        $(V)$(TERMINAL) -e "$(QEMU) -S -s -d in_asm -D  $(BINDIR)/q.log -parallel stdio -hda $< -serial null"
        $(V)sleep 2
        $(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"

然后执行 make debug:
得到<font color="#dd00ee">q.log</font>文件:

<div align=center>
图2.3
查看<font color="#dd00ee">bootasm.S</font>文件:<div align=center>
图2.38

并与<font color="#dd00ee">bootlock.asm</font>文件对比:

<div align=center>
图2.34

从上面的结果可以看到:
<font color="#dd00ee">bootasm.S</font>文件中的代码和<font color="#dd00ee">bootlock.asm</font>是一样的,对于q.log文件,断点之后的代码和<font color="#dd00ee">bootasm.S、bootlock.asm</font>是一样的。

2.4

修改gdbinit文件,在0x7c4a处设置断点 (调用bootmain函数处):

set architecture i8086
target remote :1234
break *0x7c4a

输入<font color="#dd00">make debug</font> 得到结果:

<div align=center>
图2.4

断点设置正常!


练习三 分析bootloader进入保护模式的过程。

  BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。
  1.何开启A20,以及如何开启A20
  2.如何初始化GDT表
  3.如何使能和进入保护模式
关中断和清除段寄存器

.globl start
start:
.code16                                             
    cli              //关中断                          
    cld              //清除方向标志                           
    xorw %ax, %ax    //ax清0                           
    movw %ax, %ds    //ds清0                               
    movw %ax, %es    //es清0                               
    movw %ax, %ss    //ss清0                             

3.1

  初始时<font color="#dd00">A20</font>为0,访问超过1MB的空间时就会从.循环计数,将<font color="#dd00">A20</font>的地址线置为1后才可以访问4G内存。<font color="#dd00">A20</font>地址由8042控制,8042由2个I/O端口:0x60和0x64
打开<font color="#dd00">A20</font>流程:
  1. 等待8042 Input buffer为空
  2. 发送Write 8042 Output Port(P2)命令到Input buffer;
  3. 等待8042 Input buffer为空
  4. .将8042Output Port(P2)得到字节的第2位置1,然后哦写入8042 Input buffer;

seta20.1:            //等待8042键盘控制器不忙
    inb $0x64, %al   //从0x64端口中读入一个字节到al中           
    testb $0x2, %al  //测试al的第2位
    jnz seta20.1     //al的第2位为0,则跳出循环

    movb $0xd1, %al  //将0xd1写入al中                         
    outb %al, $0x64  //将0xd1写入到0x64端口中                          

seta20.2:            //等待8042键盘控制器不忙
    inb $0x64, %al   //从0x64端口中读入一个字节到al中           
    testb $0x2, %al  //测试al的第2位
    jnz seta20.2     //al的第2位为0,则跳出循环

    movb $0xdf, %al  //将0xdf入al中                         
    outb %al, $0x60  //将0xdf入到0x64端口中,打开A20                  

3.2

1. 载入GDT表
 lgdt gdtdesc       //载入GDT表
2:进入保护模式

  通过将<font color="#ff11ff">cr0</font>寄存器PE位置1便开启了保护模式
<font color="#ff00ff">cr0</font>的第0位为1表示处于保护模式。

movl %cr0, %eax       //加载cro到eax
orl $CR0_PE_ON, %eax  //将eax的第0位置为1
movl %eax, %cr0       //将cr0的第0位置为1
3 通过长跳转更新cs的基地址:

  以上已经打开了保护模式,所以这里需要用到逻辑地址。<font color="#dd00dd">$PROT_MODE_CSEG</font>的值为0x80。

ljmp $PROT_MODE_CSEG, $protcseg//长跳转进入保护模式
.code32                          
protcseg:
4: 设置段寄存器 并建立堆栈。
 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  //设置栈指针

####### 5:转到保护模式完成,进入boot主方法。

call bootmain //调用bootmain函数

3.3

  将<font color="#ff11ff">cr0</font>寄存器置1

<div align=center>
Alt text

  首先将<font color="#ff11ff">cr0</font>寄存器里面的内容取出来,然后进行一个或操作,最后将得到的结果再写入<font color="#ff11ff">cr0</font>中,由上文我们知道,在这里需要将<font color="#ff11ff">cr0</font>的最低位设置为1,所以我们的或操作是用来使得<font color="#ff11ff" >cr0</font>的最低位为1的操作,也就是说我们的<font color="#dddd">CR0_PE_ON</font>的值必须为1,这样才可以达成目的,然后通过查询<font color="#dddd">CR0_PE_ON</font>的定义我们发现的确为1,所以顺利开启PE位。


练习四 分析bootloader加载ELF格式的OS的过程。

通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,
  1. bootloader如何读取硬盘扇区的?
  2. bootloader是如何加载ELF格式的OS?

4.1

  分析bootloader读取硬盘扇区的代码:
BootLoader让CPU进入保护模式后,下一步的工作就是从硬盘上加载并运行OS。考虑到实现的简单性,BootLoader的访问硬盘都是LBA模式的PIO(Program IO)方式,即所有的IO操作是通过CPU访问硬盘的IO地址寄存器完成的。
  上一个练习中BootLoader已经成功进入了保护模式,接下来我们要做的是从硬盘读取并运行OS。对于硬盘来说,我们知道是分成许多扇区的,其中每个扇区大小为512字节。读取扇区的流程可从指导书查阅得到:
  1. 等待磁盘准备好

利用waitdisk()函数进行检查

  2. 发出读取扇区的命令

写地址0x1f2~0x1f7,第一条设置读取扇区的数目为1,然后四条是设置LBA的参数,最后一条是发出读取磁盘的命令.
以下是地址查询功能:

<div align=center>
Alt text

  3. 等待磁盘准备好

利用waitdisk()函数进行检查

  4. 把磁盘扇区数据读到指定内存
接下来我们了解一下如何具体的从硬盘读取数据。
因为我们所要读取的操作系统文件是存在0号硬盘上的,所以我们来看一下观念与0号硬盘的I/O端口:

<div align=center>
Alt text
static void
waitdisk(void) { //如果0x1F7的最高2位是01,跳出循环
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* do nothing */;
}
/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {
    // wait for disk to be ready
    waitdisk();

    outb(0x1F2, 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);       // cmd 0x20 - read sectors

    // wait for disk to be ready
    waitdisk();

    // read a sector
    insl(0x1F0, dst, SECTSIZE / 4); //获取数据
}

  一般主板有2个IDE通道,每个通道可以接2个IDE硬盘。访问第一个硬盘的扇区可设置IO地址寄存器0x1f0-0x1f7实现的,具体参数见上表,一般第一个IDE通道通过访问IO地址0x1f0-0x1f7来实现,第二个IDE通道通过访问0x170-0x17f实现。每个每个通道的主从盘的选择通过第6个IO偏移地址寄存器来设置。从outb()可以看出这里是用LBA模式的PIO(Program IO)方式来访问硬盘的。从磁盘IO地址和对应功能表可以看出,该函数一次只读取一个扇区。
  readseg简单包装了readsect,可以从设备读取任意长度的内容。

static void
    readseg(uintptr_t va, uint32_t count, uint32_t offset) {
        uintptr_t end_va = va + count;

        va -= offset % SECTSIZE;

        uint32_t secno = (offset / SECTSIZE) + 1; 
        // 加1因为0扇区被引导占用
        // ELF文件从1扇区开始

        for (; va < end_va; va += SECTSIZE, secno ++) {
            readsect((void *)va, secno);
        }
    }

4.2

  接下来我们需要读取ELF格式的OS,在读取ELF格式的OS之前我们需要了解ELF格式的文件在UCore里面是如何进行存储的,首先我们来观察一下用来读取ELF的结构体elfhdr。
ELF定义:

<div align=center>
Alt text

在这里我们只需要关注其中的几个参数:
e_magic:是用来判断读出来的ELF格式的文件是否为正确的格式;
e_phoff:是program header表的位置偏移;
e_phnum:是program header表中的入口数目;
e_entry:是程序入口所对应的虚拟地址。
  由于我们需要把ELF格式的OS加载到内存中的程序块中,所以我们需要了解下在内存中进程块是如何存储的:
<div align=center>

Alt text

在这里我们需要了解一些参数:
p_va:一个对应当前段的虚拟地址;
p_memsz:当前段的内存大小;
p_offset:段相对于文件头的偏移。
  了解了程序在磁盘和内存中分别的存储方式之后我们就需要开始从内存中读取数据加载到内存中来。由于上问的操作,我们将一些OS的ELF文件读到<font color="#55ff55">ELFHDR</font>里面,所以在加载操作开始之前我们需要对<font color="#55ff55">ELFHDR</font>进行判断,观察是否是一个合法的ELF头。
以下是bootmain函数代码:

    void
    bootmain(void) {
        // 首先读取ELF的头部
        readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

        // 通过储存在头部的幻数判断是否是合法的ELF文件
        if (ELFHDR->e_magic != ELF_MAGIC) {
            goto bad;
        }

        struct proghdr *ph, *eph;

        // ELF头部有描述ELF文件应加载到内存什么位置的描述表,
        // 先将描述表的头地址存在ph
        //ph表示ELF段表首地址,eph表示ELF段表末地址
        ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
        eph = ph + ELFHDR->e_phnum;
        //接下来通过循环读取每个段,并且将每个段读入相应的虚存p_va中。
        // 按照描述表将ELF文件中数据载入内存
        for (; ph < eph; ph ++) {
            readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
        }
        // ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
        // ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000

        // 根据ELF头部储存的入口信息,找到内核的入口,调用头表中的内核入口地址实现内核链接地址转化为加载地址,无返回值。
        ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

    bad:
        outw(0x8A00, 0x8A00);
        outw(0x8A00, 0x8E00);
        while (1);
    }

总结:

  1. 从硬盘读了8个扇区数据到内存<font color="#ff0055">0x10000</font>处,并把这里强制转换成<font color="#440055">elfhdr</font>使用。
  2. 校验<font color="#rr0055">e_magic </font>子段
  3. 根据偏移量分别把程序短的数据读取到内存中


练习五 实现函数调用堆栈跟踪函数

  我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。在如果能够正确实现此函数,可在lab1中执行 “make qemu”后,在qemu模拟器中得到类似如下的输出:


Alt text

请完成实验,看看输出是否与上述显示大致一致,并解释最后一行各个数值的含义。

函数堆栈:

  理解函数堆栈最重要的两点是栈的结构和EBP寄存器的作用。一个函数调用可分解为零到多个push指令(用于参数入栈)和一个CALL指令。CALL指令内部还暗含了一个将返回地址压栈的动作,这是由硬件完成的。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:

pushl %ebp
movl %esp,%ebp

  这两条汇编指令的含义是:首先将ebp寄存器入栈,然后将栈顶指针esp赋值给ebp。<font color="#aaaa">movl %esp,%ebp</font>这条指令表面上看是用esp覆盖ebp原来的值,其实不然。因为给ebp赋值之前,原ebp值已经被压栈(位于栈顶),而新的ebp又恰恰指向栈顶。此时ebp寄存器就已经处于一个非常重要的地位,该寄存器中存储着占中的一个地址(原ebp入栈后的栈顶),从改地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而改地址出又存储着上一层函数调用时的ebp值。

  一般而言,<font color="#bb00bb">ss:[ebp+4]</font>处为返回地址(即调用时的 eip),<font color="#bb00bb">ss:[ebp+8]</font>处为第一个参数值(最后一个入栈的参数值,此处假设其占用4字节内存),<font color="#bb00bb">ss:[ebp-4]</font>处为第一个局部变量,<font color="#bb00bb">ss:[ebp]</font>处为上一层ebp值。由于ebp中的地址处总是“上一层函数调用时的ebp值”,而在每一层函数调用中,都能通过当时的ebp值“向上(栈底方向)”能获取返回地址、参数值,“向下(栈顶方向)”能获取函数局部变量值。如此形成递归,直至到达栈底。这就是函数调用栈。

  打开 <font color="#dd00ee">labcodes/lab1/kern/debug/kdebug.c</font>,找到 <font color="#dd00dd">print_stackframe</font>函数:
<div align=center>[图片上传失败...(image-7e3d15-1571536103241)]

实现:

<div align=center>
Alt text

  通过一个for循环来循环输出栈内的相关参数,首先获取栈传入的参数,根据上面的分析我们可以知道第一个参数存在<font color="#bb00bb">ebp+8</font>的位置,在这里是通过<font color="#bb00bb">ebp+2</font>来实现的,因为在这里2是int型,所以可以得到第一个参数,然后我们需要得到原ebp以及返回地址的值,根据分析我们知道原ebp的值就存在ebp的位置,eip的值存在<font color="#bb00bb">ebp+4</font>的位置,所以在这里通过数组的操作实现具体功能。
执行<font color="#dd00dd">make qemu</font>得到:

<div align=center>
Alt text

最后一行的解释:
  其对应的是第一个使用堆栈的函数,<font color="#aa00aa">bootmain.c</font>中的<font color="#bb00bb">bootmain</font>。(因为此时ebp对应地址的值为0)
<font color="#cc00cc">bootloader</font>设置的堆栈从0x7c00开始,使用<font color="#dd00dd">”call bootmain”</font>转入<font color="#dd00dd">bootmain</font>函数。
call指令压栈,所以<font color="#ff00ff">bootmain</font>中ebp为<font color="#eeaadd">0x7bf8</font>。


练习六 完善中断初始化和处理

请完成编码工作和回答如下问题:
  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”。

【注意】除了系统调用中断(T_SYSCALL)使用陷阱门描述符且权限为用户态权限以外,其它中断均使用特权级(DPL)为0的中断门描述符,权限为内核态权限;而ucore的应用程序处于特权级3,需要采用`int 0x80`指令操作(这种方式称为软中断,软件中断,Tra中断,在lab5会碰到)来发出系统调用请求,并要能实现从特权级3到特权级0的转换,所以系统调用中断(T_SYSCALL)所对应的中断门描述符中的特权级(DPL)需要设置为3。

 要求完成问题2和问题3 提出的相关函数实现,提交改进后的源代码包(可以编译执行),并在实验报告中简要说明实现过程,并写出对问题1的回答。完成这问题2和3要求的部分代码后,运行整个系统,可以看到大约每1秒会输出一次”100 ticks”,而按下的键也会在屏幕上显示。

6.1

一个表项的结构如下:

/*lab1/kern/mm/mmu.h*/
/* 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
};

  中断描述符表一个表项占8字节。其中015位和4863位分别为offset偏移量的低16位和高6位,16~31位为段选择子。通过段选择子获得段基址,加上偏移量即可得到中断处理代码的入口。如下图:
<div align=center>

Alt text

6.2

打开kern/trap/trap.c找到idt_init函数,完成代码:

<div align=center>
Alt text

  第一步,声明<font color="#ff00ff">__vertors[]</font>,其中存放着中断服务程序的入口地址。这个数组生成于vertor.S中。
  第二步,填充中断描述符表IDT。
  第三步,加载中断描述符表。

其中SETGATE在mmu.h中有定义:

 #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:为相应的idt[]数组内容,处理函数的入口地址
  • istrap:系统段设置为1,中断门设置为0
  • sel:段选择子
  • off:为<font color="#ff00ff">__vectors[]</font>数组内容
  • dpl:设置特权级。这里中断都设置为内核级,即第0级
    <div align=center>[图片上传失败...(image-ad46d7-1571536103241)]

6.3

  根据指导书查看函数<font color="#ff00ff">trap_dispatch</font>,发现<font color="#ff00ff">print_ticks()</font>子程序已经被实现了,所以我们直接进行判断输出即可,如下(见注释):

........
........
case IRQ_OFFSET + IRQ_TIMER:
        ticks ++; //每一次时钟信号会使变量ticks加1
        if (ticks==TICK_NUM) {//TICK_NUM已经被预定义成了100,每到100便调用print_ticks()函数打印
            ticks-=TICK_NUM;
            print_ticks();
        }
        break;
.........
.........

根据提示补充:
<div align=center>[图片上传失败...(image-22f305-1571536103241)]

运行结果:

<div align=center>
Alt text

收获:

本次实验花费了大量的时间与精力,但收获也同样不少:学习了如何基本的运行qemu,如何单步调试动态调试,了解到bootloader启动过程,分段机制,ELF文件格式等等相关知识,懂得如何中断,堆栈的利用,学会一些基本的编程知识。


参考链接:
[1].https://blog.csdn.net/Ni9htMar3/article/details/62422984
[2].https://blog.csdn.net/tiu2014/article/details/53998595
[3].https://blog.csdn.net/tangyuanzong/article/details/78595854
[4].https://blog.csdn.net/qq_19876131/article/details/51706973
[5].http://qiaoin.github.io/ucore-ex1-notes.html