实验题目: Lab1

附录1:

附录2:


实验目的:

操作系统是一个软件,也需要通过某种机制加载并运行它。

在这里我们将通过另外一个更加简单的软件-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的方法
    • 函数调用关系:在汇编级了解函数调用栈的结构和处理过程
    • 中断管理:与软件相关的中断处理
    • 外设管理:时钟

实验环境

操作系统: Ubuntu 20.04 LTS
虚拟机: Hyper-V
依赖: build-essential git qemu-system-x86 gdb make diffutils lib32z1

实验内容及操作步骤

lab1中包含一个bootloader和一个OS。这个bootloader可以切换到X86保护模式,能够读磁盘并加载ELF执行文件格式,并显示字符。

而这lab1中的OS只是一个可以处理时钟中断和显示字符的幼儿园级别OS。


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

1. 操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)

先了解关于 Make 的用法:

target ... : prerequisites ...
    command
    ...
    ...

target即一个目标文件(object file、执行文件、label)。

prerequisites就是,要生成那个target所需要的文件或是目标。

command也就是make需要执行的命令(任意的shell命令)。

这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。如果prerequisites中有一个以上的文件比target文件要新,那么command所定义的命令就会被执行。

首先找到相对应的文件 Makefile 发现注释里面的一段创建 ucore.img 的代码:

# create ucore.img
UCOREIMG	:= $(call totarget,ucore.img)

$(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)

注意到第四行**$(UCOREIMG): $(kernel) $(bootblock)**,可知要创建 ucore.img ,先得创建 kernelbootblock

这里是结合 make_v 分析得到的。

先找到生成 kernel 的代码:

# create kernel target
kernel = $(call totarget,kernel)

$(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)

$(call create_target,kernel)

根据 make_v 中对于 kernel 部分的分析可知:该代码是编译 libskern 目录下所有的 .c.S 文件,生成 .o 文件,并链接得到 bin/kernel 文件。

然后是生成 bootblock 的代码:

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

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)

$(call create_target,bootblock)

发现里面有提及到 sign ,找到相关代码:

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

根据 make_v 中对于 bootblock 部分的分析可知:该代码是编译 boot 目录下所有的 .c.S 文件,生成 .o 文件,并链接得到 bin/bootblock.out 文件;

利用 bin/sign 工具将 bin/bootblock.out 文件转化为 512 字节的 bin/bootblock 文件,并将 bin/bootblock 的最后两个字节设置为 **0x55AA ** 。

字节设置在 tools/sign.c 代码中:

// line 31
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;
}

然后就是生成 ucore.img,包括:

  • 创建大小为 10000 个块的 ucore.img,初始化为 0,每个块为 512 字节
  • bootblock 的内容写到第一个块
  • 从第二个块开始写 kernel 的内容

2.一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

注意到 bin.bootlock 是根据 sign 规范生成的,而这边的规范则来源于 tools/sign.c 代码中:

// line 31
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;
}

有两个要点:

  • 最后两个字节为 0x55AA
  • 大小为 512 字节

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

1. 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。

修改 tools/gdbinit

set architecture i8086
target remote :1234

然后执行命令:

make debug
# 下面为 gdb 中执行
si
x /2i $pc

得到如下结果:

image-20220417113045579

2. 在初始化位置0x7c00设置实地址断点,测试断点正常。

修改 tools/gdbinit

set architecture i8086
target remote :1234
b* 0x7c00
c

然后执行命令:

make debug
# 下面为 gdb 中执行
x/2i $pc

得到如下结果:

image-20220417113708342

3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。

在上面的基础上(改写tools/gdbinit),执行命令:

make debug
# 下面为 gdb 中执行
x /10i $pc

得到结果:

image-20220417115701909

查看boot/bootasm.S文件:

// Line 13:
.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                                   # Segment number zero
    movw %ax, %ds                                   # -> Data Segment
    movw %ax, %es                                   # -> Extra Segment
    movw %ax, %ss                                   # -> Stack Segment

查看obj/bootblock.asm文件:

# Line 10:
.globl start
start:
.code16                                             # Assemble for 16-bit mode
    cli                                             # Disable interrupts
    7c00:	fa                   	cli    
    cld                                             # String operations increment
    7c01:	fc                   	cld    

    # Set up the important data segment registers (DS, ES, SS).
    xorw %ax, %ax                                   # Segment number zero
    7c02:	31 c0                	xor    %eax,%eax
    movw %ax, %ds                                   # -> Data Segment
    7c04:	8e d8                	mov    %eax,%ds
    movw %ax, %es                                   # -> Extra Segment
    7c06:	8e c0                	mov    %eax,%es
    movw %ax, %ss                                   # -> Stack Segment
    7c08:	8e d0                	mov    %eax,%ss

从上面结果可以看出bootasm.S文件代码和bootblock.asm是一样的,单步跟踪代码也是跟前两者一样的。

4. 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。

修改 tools/gdbinit

set architecture i8086
target remote :1234

然后执行命令:

make debug
# 下面为 gdb 中执行
b *0x7c12
x /2i $pc

得到如下结果:

image-20220417120913528

说明断点正常。

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

首先查看 lab1/boot/bootasm.S 代码,根据分析可知该过程有如下几个阶段:

  • 清理环境
  • 开启A20
  • 进入保护模式
    • 通过将cr0寄存器PE位(第0位)置1开启了保护模式
    • 通过长跳转更新cs的基地址
    • 设置段寄存器,并建立堆栈
    • 转到保护模式完成,进入 bootmain 方法

练习4:分析bootloader加载ELF格式的OS的过程

bootloader 是如何读取硬盘扇区的?

读取扇区的流程,根据指导书[1]的内容可知有以下步骤:

  • 等待磁盘准备好

  • 发出读取扇区的命令

  • 等待磁盘准备好

  • 把磁盘扇区数据读到指定内存

通过查看 boot/bootmain.c ,对比上面步骤来进行分析,发现其中的函数:

  • waitdisk
  • readsect
  • readseg

应该对应的就是等待磁盘准备、读取扇区、读取内存。

读取扇区
// Line 43:
/* 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);                         // 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);
}

可以看到等待磁盘准备部分用的是**waitdisk()函数,而发出命令则对应指导书[1:1]所给的 LBA 模式,读取到指定内存则用的是insl()**函数。

先分析waitdisk()

磁盘IO地址和对应功能

第6位:为1=LBA模式;0 = CHS模式 第7位和第5位必须为1

IO地址功能
0x1f0读数据,当0x1f7不为忙状态时,可以读。
0x1f2要读写的扇区数,每次读写前,你需要表明你要读写几个扇区。最小是1个扇区
0x1f3如果是LBA模式,就是LBA参数的0-7位
0x1f4如果是LBA模式,就是LBA参数的8-15位
0x1f5如果是LBA模式,就是LBA参数的16-23位
0x1f6第0~3位:如果是LBA模式就是24-27位 第4位:为0主盘;为1从盘
0x1f7状态和命令寄存器。操作时先给命令,再读取,如果不是忙状态就从0x1f0端口读数据
等待磁盘准备
// Line 36:
/* waitdisk - wait for disk ready */
static void
waitdisk(void) {
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* do nothing */;
}

里面的0x1F7根据指导书[1:2]可知对应的是状态和命令寄存器。

读取到内存
// Line 63:
/* *
 * readseg - read @count bytes at @offset from kernel into virtual address @va,
 * might copy more than asked.
 * */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
    uintptr_t end_va = va + count;

    // round down to sector boundary
    va -= offset % SECTSIZE;

    // translate from bytes to sectors; kernel starts at sector 1
    uint32_t secno = (offset / SECTSIZE) + 1;

    // If this is too slow, we could read lots of sectors at a time.
    // We'd write more to memory than asked, but it doesn't matter --
    // we load in increasing order.
    for (; va < end_va; va += SECTSIZE, secno ++) {
        readsect((void *)va, secno);
    }
}

可以看到 readseg() 简单包装了 readsect() ,可以从设备读取任意长度的内容。

bootloader 是如何加载ELF格式的OS的?

ELF格式,根据指导书[2]的内容可知其定义:

struct elfhdr {
  uint magic;  // must equal ELF_MAGIC
  uchar elf[12];
  ushort type;
  ushort machine;
  uint version;
  uint entry;  // 程序入口的虚拟地址
  uint phoff;  // program header 表的位置偏移
  uint shoff;
  uint flags;
  ushort ehsize;
  ushort phentsize;
  ushort phnum; //program header表中的入口数目
  ushort shentsize;
  ushort shnum;
  ushort shstrndx;
};

通过查看 boot/bootmain.c ,对比上面步骤来进行分析,发现其中的函数**bootmain()**所做的便是对于 ELF 格式的读取跟操作:

// Line 85:
/* bootmain - the entry of bootloader */
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 描述表的头地址存在 ph
    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))();

bad:
    outw(0x8A00, 0x8A00);
    outw(0x8A00, 0x8E00);

    /* do nothing */
    while (1);
}

简言之:

  • 从硬盘读了8个扇区数据到内存0x10000处,并把这里强制转换成elfhdr使用;
  • 校验e_magic字段;
  • 根据偏移量分别把程序段的数据读取到内存中

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

先看看labcodes/lab1/kern/debug/kdebug.c里面需要添加的代码:

// Line 291:
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 (uint32_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]
      */
}

根据注释我们可以知道要做的任务:

  • 调用read_ebp()来拿到ebp的值,类型是uint32_t

  • 调用read_eip()来拿到eip的值,类型是uint32_t

  • 从0到栈帧深度

    • 打印ebp,eip
    • 把在(uint32_t)ebp +2 [0…4]中的内容,赋给调用参数的[0…4]
    • 直接cprintf(“\n”);
    • 调用print_debuginfo(eip-1),打印C调用的函数名,行号等
    • 弹出调用的栈帧

    注意:调用函数返回的addr的eip = ss:[ebp+4],ebp = ss:[ebp]

    目标输出:

    ebp:0x00007b28 eip:0x00100992 args:0x00010094 0x00010094 0x00007b58 0x00100096
        kern/debug/kdebug.c:305: print_stackframe+22
    ebp:0x00007b38 eip:0x00100c79 args:0x00000000 0x00000000 0x00000000 0x00007ba8
        kern/debug/kmonitor.c:125: mon_backtrace+10
    ebp:0x00007b58 eip:0x00100096 args:0x00000000 0x00007b80 0xffff0000 0x00007b84
        kern/init/init.c:48: grade_backtrace2+33
    ebp:0x00007b78 eip:0x001000bf args:0x00000000 0xffff0000 0x00007ba4 0x00000029
        kern/init/init.c:53: grade_backtrace1+38
    ebp:0x00007b98 eip:0x001000dd args:0x00000000 0x00100000 0xffff0000 0x0000001d
        kern/init/init.c:58: grade_backtrace0+23
    ebp:0x00007bb8 eip:0x00100102 args:0x0010353c 0x00103520 0x00001308 0x00000000
        kern/init/init.c:63: grade_backtrace+34
    ebp:0x00007be8 eip:0x00100059 args:0x00000000 0x00000000 0x00000000 0x00007c53
        kern/init/init.c:28: kern_init+88
    ebp:0x00007bf8 eip:0x00007d73 args:0xc031fcfa 0xc08ed88e 0x64e4d08e 0xfa7502a8
    <unknow>: -- 0x00007d72 –

    根据注释参考指导书[3]感觉可以直接开写了:

    // Line 291:
    void
    print_stackframe(void) {
    	// read_ebp() && read_eip 直接看函数调用就行
        uint32_t ebp = read_ebp();
        uint32_t eip = read_eip();
        
        // loop
        int i,j;
        for(i = 0,i < STACKFRAME_DEPTH && ebp != 0;i++){
            // format 仿照 print_kerninfo() 写的
            cprintf("ebp:0x%08x",ebp);
            cprintf("eip:0x%08x",eip);
            cprintf("args:");
          	// entry args
            uint32_t *args= (uint32_t*)ebp +2;
            // inner loop
            for(j = 0; j < 4; j++){
                cprintf("0x%08x",args[j]);
            }
        	cprintf("\n");
            print_debuginfo(eip-1);
          	// pop 更新数据
            eip = ((uint32_t *)ebp)[1];
            ebp = ((uint32_t *)ebp)[0];
        }
    }

    写完之后跑一下看看:

    image-20220417154454724

    可以看到是符合题意的。

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

1. 中断描述符表(也可简称为保护模式下的中断向量表)中一个表项占多少字节?其中哪几位代表中断处理代码的入口?

根据指导书[4]中关于中断描述符表的描述,发生中断后,x86根据终端去IDT中查找相应的描述符,并跳转到描述符指向的中断处理例程执行。

进行中断初始化和处理,就相当于设置IDT中的描述符和描述符指向的中断处理例程。

下面的图是 IDT 的描述结构:

IDT描述符的结构

labcodes/lab1/kern/mm/mmu.h的代码中,我们可以找到段的定义。

// Line 49:
/* 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
};

总的加起来是64位也就是8字节,即中断向量表的一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成位移,两者联合便是中断处理程序的入口地址。

2. 请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。在idt_init函数中,依次对所有中断入口进行初始化。使用mmu.h中的SETGATE宏,填充idt数组内容。每个中断的入口由tools/vectors.c生成,使用trap.c中声明的vectors数组即可。

先看看要求填的代码:

/* 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!
      */
}

不难发现注释里面的:

  • You can use "extern uintptr_t __vectors[];" to define this extern variable which will be used later.
  • you can use SETGATE macro to setup each item of IDT
  • the argument of lidt is idt_pd.

翻了翻代码,找到了SETGATE的实现:

labcodes/lab1/kern/mm/mmu.h : Line 71

#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;        \
}

以及相关宏定义:

/* global segment number */
#define SEG_KTEXT    1
#define SEG_KDATA    2
#define SEG_UTEXT    3
#define SEG_UDATA    4
#define SEG_TSS        5

/* global descriptor numbers */
#define GD_KTEXT    ((SEG_KTEXT) << 3)        // kernel text
#define GD_KDATA    ((SEG_KDATA) << 3)        // kernel data
#define GD_UTEXT    ((SEG_UTEXT) << 3)        // user text
#define GD_UDATA    ((SEG_UDATA) << 3)        // user data
#define GD_TSS        ((SEG_TSS) << 3)        // task segment selector

#define DPL_KERNEL    (0)
#define DPL_USER    (3)

#define KERNEL_CS    ((GD_KTEXT) | DPL_KERNEL)
#define KERNEL_DS    ((GD_KDATA) | DPL_KERNEL)
#define USER_CS        ((GD_UTEXT) | DPL_USER)
#define USER_DS        ((GD_UDATA) | DPL_USER)

idt_init函数的功能是初始化IDT表。

IDT表中每个元素均为门描述符,记录一个中断向量的属性,包括中断向量对应的中断处理函数的段选择子/偏移量、门类型(是中断门还是陷阱门)、DPL等。

因此,初始化IDT表实际上是初始化每个中断向量的这些属性。

有了以上的铺垫,代码便能比较轻松的写出来了。

/* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
void
idt_init(void) {
	extern uintptr_t __vectors[];
    int i;
    for(i=0; i<sizeof(idt)/sizeof(struct gatedesc); 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);
}

3. 请编程完善trap.c中的中断处理函数trap,在对时钟中断进行处理的部分填写trap函数中处理时钟中断的部分,使操作系统每遇到100次时钟中断后,调用print_ticks子程序,向屏幕上打印一行文字”100 ticks”。

看看labcodes/lab1/kern/trap/trap.c中的相关代码:

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!
         */
break;

注释翻译如下:

  • 计时器中断后,您应该使用全局变量记录该事件(增加),例如,kern / driver / clock.c中的滴答声
  • 在每个TICK_NUM周期中,您都可以使用诸如print_ticks()之类的函数来打印一些信息。
  • 太简单了?我想也是!

嘛,根据注释写还是不难的。

case IRQ_OFFSET + IRQ_TIMER:
	ticks++;
	if(ticks%TICK_NUM==0){
        print_ticks();
    }
break;

写完跑了看看:

image-20220417163905447

完工!

实验结果及分析

经过阅读文档以及网上查阅资料,能够完成前面六个练习,但对于第七个练习还是感觉有心无力,实验结果已经在上面的实验过程分析中有所包含,便不再提及。

收获与体会

经过这次实验,对于操作系统 ucore 的启动有了更加深入的认识,了解到了诸如 ELF 等之前没有了解的或者对于之前了解过的磁盘读取更为深入,感觉对于整个操作系统有了更为清晰客观的认识。


附录


  1. 硬盘访问概述 | uCore Lab Documents (gitbooks.io) ↩︎ ↩︎ ↩︎

  2. ELF文件格式概述 | uCore Lab Documents (gitbooks.io) ↩︎

  3. 函数堆栈 | uCore Lab Documents (gitbooks.io) ↩︎

  4. 中断与异常 | uCore Lab Documents (gitbooks.io) ↩︎