ucore lab1

ucore lab1 report

这个实验报告暂时还只是草稿,其中还存在格式错误,部分实验的报告还没有写完。

exercise 1: 生成ucore的过程

通过make V=输出的命令研究ucore生成的过程。

下面的命令是make实际执行的命令(23~24行除外)。

  1 gcc -Ikern/init/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/ke
  2 gcc -Ikern/libs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/k
  3 gcc -Ikern/libs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o ob
  4 gcc -Ikern/debug/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/panic.c -o obj
  5 gcc -Ikern/debug/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kdebug.c -o ob
  6 gcc -Ikern/debug/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kmonitor.c -o 
  7 gcc -Ikern/driver/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/clock.c -o o
  8 gcc -Ikern/driver/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/console.c -o
  9 gcc -Ikern/driver/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/picirq.c -o 
 10 gcc -Ikern/driver/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/intr.c -o ob
 11 gcc -Ikern/trap/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trap.c -o obj/ke
 12 gcc -Ikern/trap/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/vectors.S -o obj
 13 gcc -Ikern/trap/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trapentry.S -o o
 14 gcc -Ikern/mm/ -fno-builtin -fno-PIC -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
 15 gcc -Ilibs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/string.c -o obj/libs/string.o
 16 gcc -Ilibs/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/printfmt.c -o obj/libs/printfmt.o
 17 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/
 18 gcc -Iboot/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o
 19 gcc -Iboot/ -fno-builtin -fno-PIC -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o
 20 gcc -Itools/ -g -Wall -O2 -c tools/sign.c -o obj/sign/tools/sign.o
 21 gcc -g -Wall -O2 obj/sign/tools/sign.o -o bin/sign
 22 ld -m    elf_i386 -nostdlib -N -e start -Ttext 0x7C00 obj/boot/bootasm.o obj/boot/bootmain.o -o obj/bootblock.o
 23 'obj/bootblock.out' size: 496 bytes
 24 build 512 bytes boot sector: 'bin/bootblock' success!
 25 dd if=/dev/zero of=bin/ucore.img count=10000
 26 dd if=bin/bootblock of=bin/ucore.img conv=notrunc
 27 dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc

编译过程

编译ucore和编译应用程序的过程相同,但是:

  1. 不使用任何外部的库,比如C标准库;
  2. 不使用动态链接,不生成pic(position-independent code)代码;
  3. 不查找标准库头文件;

具体参数分析:

  1. 链接相关
  • -no-stdlib: 不链接标准库和C初始化函数

由于ucore是操作系统,不应该使用外部的库,而且应该自己指定程序的入口,所以用这个选项。

  • -fno-buitlin: 对于非__buitlin_开头的函数,不使用对应的GCC内置(built in)函数

除了外部的库,GCC内部内嵌的有库libgcc.a,这个库总是自动链接(即使使用了-no-stdlib选项)。大量的标准库函数都有对应的内置版本,比如strcpy,对应有__builtin_strcpy,调用strcpy时GCC实际调用__builtin_strcpy。ucore既需要内置函数实现的C语言特性(比如va_list),又希望自己实现部分标准库函数(比如strcpy),并且在调用时使用自定义版本而非内置版本。

  • -no-stdinc: 不搜索系统默认的头文件目录

ucore不使用外部库,只是用libgcc.a和自定义的标准库。如果不开启这个选项,GCC会搜索到系统的标准库头文件,而非自定义的标准库头文件。

  • -fno-PIC: 不生成位置无关代码(position-independent code)

GCC默认动态链接,生成pic代码。ucore不进行动态链接,不生成pic代码。

  1. 调试相关
  • -ggdb: 生成GDB专用格式的调试信息

ucore使用GDB调试,生成GDB专用的调试信息可以最大限度的增强GDB的调试能力。

  • -gstabs: 生成stabs格式的调试信息

ucore中内置了调试内核的函数,比如联系5完成的print_stackframe()函数,这些函数解析stabs格式的调试信息。

  1. 目标平台
  • -m32: 生成IA-32位代码

ucore是运行在IA-32处理器上的操作系统,所以要用这个选项。

  1. 代码生成规格
  • -fno-stack-protector: 禁用stack-protector

现代GCC编译时会使用stack-protector(在数组末尾添加金丝雀值(哨兵),如果金丝雀值被更改就会终止程序)防止缓冲区溢出。ucore添加这个选项可能是想简化内存布局或者减少运行时的内存消耗。

链接过程

bootloader和kernel都是ucore项目的一部分,但却是两个独立的程序(执行文件),所以分别链接。

ld选项:

  • -m: 指定可执行文件格式与目标平台

ucore使用elf_i386格式。

  • -nostdlib: 禁止链接标准库和C程序初始函数

ucore是操作系统,不依赖与外部函数库。

  • -T: 指定linker script

生成kernel的过程中使用tools/kernel.ld作为linker script,其中包含了设置kernel内部各段的相关信息;生成bootblock的过程中使用tools/boot.ld作为linker scrip,其中包含程序入口点。

  • -Ttext: 设置可执行文件的.text节的绝对地址

在生成bootblock的过程中使用了这个选项,使bootloader的.text节起始地址为绝对地址0x7c00。

  • -e: 设置可执行文件入口点

在生成bootblock的过程中使用了这个选项,指定start标记为bootloader的入口点。

  • -N: 将可执行文件的代码节和数据节设置为读/写、禁止代码节向页大小对齐、禁止动态链接

_

启动扇区的检验和生成

完成了全部的编译链接后,还需要生成启动扇区。使用tools/sign和objdump对目标文件bootblock.o进行修改得到。

在生成了bootblock.o,

  1. 调用objdump提取了bootblock.o中的代码输出到bootblock.out中;
  2. 调用tools/sign检查bootblock.out是否小于510字节,如果大于510字节就报错;
  3. tools/sign将bootblock.out输出到一个bin/bootblock,并将文件最后两个字节设置为0x55,0xAA。
    @$(OBJDUMP) -S $(call objfile,bootblock) > $(call asmfile,bootblock)
    @$(OBJDUMP) -t $(call objfile,bootblock) | $(SED) '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(call symfile,bootblock)
    @$(OBJCOPY) -S -O binary $(call objfile,bootblock) $(call outfile,bootblock)
    @$(call totarget,sign) $(call outfile,bootblock) $(bootblock)

$(call create_target,bootblock)

通过以上步骤得到的bin/bootblock包含了启动扇区中的全部内容。

虚拟硬盘的制作

通过dd命令制作虚拟硬盘。
先将虚拟硬盘第一个扇区区初始化为0,在把bin/bootblock写入其中制成启动扇区,然后再将bin/kernel写入之后的扇区中。

dd if=/dev/zero of=bin/ucore.img count=10000
dd if=bin/bootblock of=bin/ucore.img conv=notrunc
dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc

exercise 3: bootloader进入保护模式的过程

练习一要求分析bootloader进入保护模式的过程。bootloader由三个文件实现,分别为asm.h(包含常量,初始值)、bootasm.S(开启A20,设置GDT,并进入保护模式)、bootmain.c(加载kernel并执行)。这个练习仅涉及asm.h和bootasm.S。

bootasm.S完成了A20的开启、GDT的设置、内核栈的设置后进入保护模式,并call到bootmain执行,进行kernel的加载工作。

 常量与宏

bootasm.S中定义了三个常量:

.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

CRO_PE_ON作为切换到保护模式时修改CRO的掩码。
PROT_MODE_CSEG和PROT_MODE_DSEG分别作为内核代码段、数据段的选择子,指向GDT[1]和GDT[2],RPL(requested privilege level)均设置为ring 0。

asm.h中定义了描述段描述符的宏。

 A20的开启

bootloader最初运行在16位实模式下。

bootloader首先关闭对中断的响应,并将段寄存器ds,es,ss置零,之后开始A20(在8042芯片上)的开启。
开启A20的思路很简单:等到8042芯片空闲时,将A20位设置为1即可。由于8042的设计,向8042写入数据被才分成向0x64端口发送写命令和向0x60端口发送数据两步。

最终操作步骤如下:

  1. 等待8042芯片空闲
  2. 向P2端口发送写命令
  3. 再次等待8042芯片空闲
  4. 像P2端口写入数据

等待8042芯片空闲
等待8042空闲通过循环读取8042的状态寄存器到CPU寄存器al,判断al是否为0x2(芯片初始系统状态)来实现。

seta20.1:
    inb $0x64, %al                                  # read status information into al
    testb $0x2, %al
    jnz seta20.1

像8042写入数据

向端口0x64发送写命令

    movb $0xd1, %al                                 # 0xd1 -> port 0x64
    outb %al, $0x64                                 # 0xd1 means: write data to 8042's P2 port

向端口0x60写入数据

    movb $0xdf, %al                                 # 0xdf -> port 0x60
    outb %al, $0x60                                 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1

经过以上介绍的四步,A20就被开启了,系统可以使用高于1M的线性空间。

 设置GDT

GDT被设置为4字节对齐,仅定义了GDT[0]、GDT[1](内核代码段)、GDT[2](内核数据段)。

# 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(executable and read-only)
    SEG_ASM(STA_W, 0x0, 0xffffffff)                 # data seg for bootloader and kernel(writable)

gdtdesc:
    .word 0x17                                      # sizeof(gdt) - 1
    .long gdt                                       # address gdt

```
其中的的宏定义在asm.h中,asm.h比较短,直接摘抄出来。
```
/* Normal segment */
#define SEG_NULLASM                                             \
    .word 0, 0;                                                 \
    .byte 0, 0, 0, 0

#define SEG_ASM(type,base,lim)                                  \
    .word (((lim) >> 12) & 0xffff), ((base) & 0xffff);          \
    .byte (((base) >> 16) & 0xff), (0x90 | (type)),             \
        (0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)

/* Application segment type bits */
#define STA_X       0x8     // Executable segment
#define STA_E       0x4     // Expand down (non-executable segments)
#define STA_C       0x4     // Conforming code segment (executable only)
#define STA_W       0x2     // Writeable (non-executable segments)
#define STA_R       0x2     // Readable (executable segments)
#define STA_A       0x1     // Accessed

分析以上两段代码可以发现:

  • GDT[0]设置为空,未使用;
  • GDT[1],GDT[2]分别作为内核代码段、数据段;
  • 内核代码段、数据段都被设置为最长4G,且基地址均为0x00;

内核代码段、数据段被设置成这样是有意削弱(避免)X86分段内存模型的影响,在32位的CPU上实现类似64位上的平坦内存模型,方便页机制的实现。

加载GDT

lgdt gdtdesc

切换到保护模式

开启A20、设置并加载GDT后,bootloader已经完成了切换到保护模式的全部准备工作。

开启保护模式仅需要打开控制寄存器CR0中相应的标志位,通过异或之前定义的掩码CRO_PE_ON实现。

    movl %cr0, %eax
    orl $CR0_PE_ON, %eax        
    movl %eax, %cr0

打开模式后,CPU真正进入了32位模式,默认使用分段内存模型,段寄存器中必须存放相应的选择子。
首先设置cs和 eip的值,通过ljmp实现。ljmp仅仅是跳转到了下一条指令(procseg处)

    # Jump to next instruction, but in 32-bit code segment.
    # Switches processor into 32-bit mode.
    ljmp $PROT_MODE_CSEG, $protcseg

.code32
procseg:

设置栈并跳转到bootmain加载kernel

将所有的寄存器设置为PROT_MODE_DSEG(指向内核数据段)。将栈设置为为0x00~0x7c00(bootloader之下都是栈的空间),然后使用call指令执行bootmain,开始加载kernel。
bootmain函数正常情况不会返回,如果返回肯定是bootloader产生错误,进入死循环。

.code32                                             # Assemble for 32-bit mode
protcseg:
    # 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

    # bootmain should not return. if return, loop forever
spin:
    jmp spin

栈和bootloader的位置关系如下:

+--------------------+
+                    +
+                    +
+                    +
+                    +
+     not in use     +
+                    +
+                    +
+                    +
+                    +
+                    +
+--------------------+
+                    +
+                    +
+                    +
+      bootloader    +
+                    +
+                    +
+                    +
+                    +
+--------------------+ <-- 0x7c00 beginning of bootloader
+                 |  +
+                 |  +
+       stack     |  +
+                 |  +
+                 |  +
+                 V  +
+--------------------+ <-- 0x00 

至此,bootlader完成了初始化和各种设置,bootasm.S完成任务,剩下的任务交给bootmain函数完成。

execrise 4: bootloader加载ELF格式的OS的过程

在bootmain函数中,bootloader加载kernel到内存中。
先分析bootmain函数如何加载ELF格式的kernel,再分析具体读取磁盘的机制。

加载ELF格式的kernel

为了理解如何加载ELF格式的kernel,就必须知道ELF格式的结构。在这里只涉及到了ELF32格式中的已链接的可执行文件,所以忽略共享目标文件和可重定位目标文件。

实验手册给的资料不足,参考《深入理解计算机系统》第三版第7章《链接》图7-13“典型的ELF可执行目标文件”

典型的ELF可执行文件

图中ELF头和段头部对应的ucore中的数据类型为struct elfhdr和struct proghdr,均定义在libs/elf.h中。这里只摘抄在这里用到的结构体成员。


#define ELF_MAGIC    0x464C457FU            // "\x7FELF" in little endian

struct elfhdr {
    ...
    uint32_t e_magic;     // must equal ELF_MAGIC
    ...
    uint32_t e_entry;     // entry point of executable
    ...
    uint16_t e_phnum;     // number of entries in program header or 0
    ...
}

struct proghdr {
    ...
    uint32_t p_offset; // file offset of segment
    uint32_t p_va;     // virtual address to map segment
    ...
    uint32_t p_memsz;  // size of segment in memory (bigger if contains bss)
    ...
}

知道这些信息后,加载ELF格式的kernel的机制就很清楚了:

  • 通过判断ELF_MAGIC是否等于ELF头中的e_magic确定kernel是否是合法的ELF32格式
  • 段头部表是struct proghdr的数组,数组元素个数为e_phnum(在ELF头中)
  • 内核各段应该加载到对应段头部中记录的p_va处,大小为p_memsz,位于于kernel文件的p_offset处。通过readseg实现。

bootmain逻辑流:

  1. 读取kernel的ELF头和段头部表加载到ELFHDR(0x10000)处
  2. 判断kerne是否是合法的ELF格式,如果不是则死循环
  3. 如果格式合法就根据kernel中的ELF头和段头部表中的信息将kernel各段加载到适当的位置
  4. 加载完成后,通过函数调用跳转到entry point

具体实现代码如下:

uintptr_t其实就是uint32_t代表32位地址,定义在lib/defs.h中。/**/风格注释是我添加的,//风格注释是源代码中的。

/* bootmain - the entry of bootloader */
void
bootmain(void) {
    // read the 1st page off disk
    /* read SECTSIZE*8 bytes at offset 0 from kernel into virtual address ELFHDR(0x10000) */
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    /* if the executable is not ELF, goto bad(infinite loop) */
    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 is the position after the end of the last program header */
    eph = ph + ELFHDR->e_phnum;
    for (; ph < eph; ph ++) {
        /* load executable code from kernel into corresponding virtual address according to infomation in program header */
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
    }

    // call the entry point from the ELF header
    // note: does not return
    /* use function call to transfer control to kernel */
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

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

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

读取硬盘的机制

ucore为了简化硬盘访问的实现,使用的是可编程IO(programed IO)方式,并且假设硬盘使用IDE。一个IDE通道可以接两个硬盘(主盘/从盘),ucore只读取主盘的数据。

基本思路:

  1. 等待硬盘空闲
  2. 发送要读取的扇区号为硬盘
  3. 等待硬盘空闲
  4. 读取扇区数据到某个内存位置

ucore通过readsg()、readsec()和insl()三个函数实现读取硬盘。readsg()和readsec()定义在bootmain.c中,insl()定义在libs/x86.h头文件中。

/* 从读取扇区号secno的数据到内存dst处 */
static void
readsect(void *dst, uint32_t secno);

/* 从kernel文件偏移offset处读取count字节到内存va处 */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset);

/* 内联汇编。从prot端口读取cnt*4字节数据到内存addr处 */
static inline
void insl(uint32_t port, void *addr, int cnt) __attribute__((always_inline));

函数调用关系: readsg() --> readsec() --> insl()

insl()实现:

*/当%ecx(cnt)不为0使,从端口%edi(port)处读取4字节到(%edi)(内存addr)处*/
static inline void
insl(uint32_t port, void *addr, int cnt) {
    /* edi - addr    
       ecx - cnt
       edi - port
    */
    asm volatile (
            "cld;"
            "repne; insl;"
            : "=D" (addr), "=c" (cnt)
            : "d" (port), "0" (addr), "1" (cnt)
            : "memory", "cc");
}

insl()函数是一个辅助函数,在读取之前还需要像硬盘发送读命令,读取硬盘扇区由readsect()完成。

磁盘IO地址与功能(摘自ucore实验手册)如下:

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); /* 掩码0xF将扇区号28~31位置零,仅保留24~27位。
                                                  异或0xE0避开了IO端口第4位,目的是读取主盘。*/
    outb(0x1F7, 0x20);                      // cmd 0x20 - read sectors

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

    // read a sector
    insl(0x1F0, dst, SECTSIZE / 4);           /*SECTISE被定义为512。insl每次读取4字节,所以SECTISE要除4
}

最终bootmain读取磁盘使用的是readseg()函数。

static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
    /* uintptr_t在/libs/defs.h中定义为uint32_t;
       uintprt_t避免了C语言指针运算时的自动伸展;
    */      
    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()函数提供的功能是从磁盘读取count字节数据,但是读取硬盘却以512字节的扇区为为单位,所以实际读入的字节数很可能大于要求读入的字节数。
readseg()函数实际写入的内存地址是地址参数向下舍入到512字节的边界,所以实际写入的内存地址很可能低于要求写入的内存地址。

完成函数调用跟踪函数

这个练习要求我们kern/debug/kdebug.c中的print_stackframe()函数。在print——stackframe()函数中,注释已经给出了完整的步骤,难度不大,只要理解x86函数调用过程就可以做出来。
栈的设置是在启动阶段(跳转到bootmain函数之前完成的),所以初始时栈结构如下:

+--------------------+ <-- 0x7c00 
+   bootmain frame   +    |g
+--------------------+    |r
+                    +    |o
+                    +    |w
+                    +    V
+                    +
+                    +
+--------------------+ <-- 0x00 

bootmain函数是不会返回的,所有的函数栈帧都在bootmain函数下面。在调用bootmain()之前,%ebp被设置了0,这个值被压入了函数栈中,这个特殊的%ebp是跟踪函数栈帧的结束标志。

x86函数调用的具体过程(同特权级):

  1. 把函数参数压入栈中
  2. 把函数返回地址(%es,%eip)压入栈中
  3. 把旧的%ebp压入栈中,并把%ebp的值改为当前%esp

跟踪函数堆栈就是利用了x86函数调用后堆栈的结构。

/* stack */

+         %cs        +
+        %eip        +
+--------------------+<----------------------------------------------+
+        ...         +  local variables of calling function          +
+--------------------+                                               +
+     parameters     +  parameters passed by calling function        +
+--------------------+  <-----+                                      +
+         %cs        +        +                                      +
+--------------------+        +---- return address                   +
+        %eip        +        +                                      +
+--------------------+  <-----+                                      +
+      old %ebp      +-----------------------------------------------+
+--------------------+<----------------------------------------------+
+        ...         +  local variables of calling function          +
+--------------------+                                               +
+     parameters     +  parameters passed by calling function        +
+--------------------+  <-----+                                      +
+         %cs        +        +                                      +
+--------------------+        +---- return address                   +
+        %eip        +        +                                      +
+--------------------+  <-----+                                      +
+      old %ebp      +-----------------------------------------------+
+--------------------+  <----- current %ebp
+         ...        +
+                    +

在上面的图示是某时刻堆栈的布局。

每次函数调用,%ebp都被压入栈中,并修改为当时栈顶指针%esp的值。栈中保存的%ebp总是指向上一次保存的%ebp处,而且栈中保存的%ebp上面4字节处就是调用函数的返回地址,返回地址之上就是被调用函数的参数。

利用当前的%ebp值和栈中保存的%ebp值就可以实现跟踪堆栈的功能。

print_stackframe(void)函数要求了实现的具体步骤,所以最终补全的代码跟答案结构是完全一样的,这里直接给出了去掉步骤要求的答案代码。

void
print_stackframe(void) {
    uint32_t ebp = read_ebp(), eip = read_eip();

    int i, j;
    for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {
        cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);
        uint32_t *args = (uint32_t *)ebp + 2;
        for (j = 0; j < 4; j ++) {
            cprintf("0x%08x ", args[j]);
        }
        cprintf("\n");
        print_debuginfo(eip - 1);
        eip = ((uint32_t *)ebp)[1];
        ebp = ((uint32_t *)ebp)[0];
    }
}

运行结果

在print_stackframe中对调试信息的解析和显示由print_debuginfo()函数完成。这个函数接受一个代码段中地址,并显示相应的调试信息。
这个函数似乎有bug,只要把print_stackframe()函数的实现稍作改变,%eip的值偏移几个字节,就会无法正确分析出代码在kdebug.c文件中的位置,其他栈帧中函数所在文件似乎不受影响。

完善中断初始化和处理

发生中断后,x86根据终端去IDT中查找相应的描述符,并跳转到描述符指向的中断处理例程执行。
进行中断初始化和处理,就相当于设置IDT中的描述符和描述符指向的中断处理例程。

IDT描述符结构(摘自intel开发手册)如下:
IDT描述符的结构

IDT表项占8个字节,其中0~31位、47~63位代表中断处理例程的入口。

猜你喜欢

转载自www.cnblogs.com/kongj/p/12507214.html