美文网首页
ucore lab1

ucore lab1

作者: nlskyfree | 来源:发表于2018-07-13 09:53 被阅读0次

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

    问题1:操作系统镜像文件ucore.img是如何一步一步生成的?

    • 执行make clean再执行make "V=",观察生成ucore.img的过程
    • 提取核心过程如下:
    # 构建bin/kernel
    + cc kern/init/init.c
    + cc kern/libs/readline.c
    + cc kern/libs/stdio.c
    + cc kern/debug/kdebug.c
    + cc kern/debug/kmonitor.c
    + cc kern/debug/panic.c
    + cc kern/driver/clock.c
    + cc kern/driver/console.c
    + cc kern/driver/intr.c
    + cc kern/driver/picirq.c
    + cc kern/trap/trap.c
    + cc kern/trap/trapentry.S
    + cc kern/trap/vectors.S
    + cc kern/mm/pmm.c
    + cc libs/printfmt.c
    + cc libs/string.c
    + ld bin/kernel
    # 构建sign工具与bin/bootblock
    + cc boot/bootasm.S
    + cc boot/bootmain.c
    + 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!
    # 构建ucore.img
    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.0456474 s, 112 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.00281044 s, 182 kB/s
    dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
    138+1 records in
    138+1 records out
    70775 bytes (71 kB) copied, 0.000473867 s, 149 MB/s
    

    由以上过程可知

    • 编译16个内核文件,构建出内核bin/kernel
    • 生成bin/bootblock引导程序
      • 编译bootasm.S,bootmain.c,链接生成obj/bootblock.o
      • 编译sign.c生成sign.o工具
      • 使用sign.o工具规范化bootblock.o,生成bin/bootblock引导扇区
    • 生成ucore.img虚拟磁盘
      • dd初始化ucore.img5120000 bytes,内容为0的文件
      • dd拷贝bin/bootblockucore.img第一个扇区
      • dd拷贝bin/kernelucore.img第二个扇区往后的空间

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

    根据问题1可知通过sign.c文件的操作使得bootblock.o成为一个符合规范的引导扇区,因此查看sign.c的内容

    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    #include <sys/stat.h>
    
    int main(int argc, char *argv[]) {
        struct stat st;
        // 输入状态判断
        if (argc != 3) {
            fprintf(stderr, "Usage: <input filename> <output filename>\n");
            return -1;
        }
        // 读取文件头
        if (stat(argv[1], &st) != 0) {
            fprintf(stderr, "Error opening file '%s': %s\n", argv[1], strerror(errno));
            return -1;
        }
        // 问题1中输出的文件大小
        printf("'%s' size: %lld bytes\n", argv[1], (long long)st.st_size);
        // 文件大小超过510字节报错返回,因为最后2个字节要用作结束标志位
        if (st.st_size > 510) {
            fprintf(stderr, "%lld >> 510!!\n", (long long)st.st_size);
            return -1;
        }
        // 多余位用0填充
        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);
        if (size != 512) {
            fprintf(stderr, "write '%s' error, size is %d.\n", argv[2], size);
            return -1;
        }
        fclose(ofp);
        printf("build 512 bytes boot sector: '%s' success!\n", argv[2]);
        return 0;
    }
    
    

    由以上代码可知,硬盘主引导扇区特征为:

    • 大小为512字节,空余部分用0填充
    • 文件内容不超过510 bytes
    • 最后2 bytes0x55 0xAA

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

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

    修改tools/gdbinit,在lab1下执行make debug

    set architecture i8086
    target remote :1234
    
    image.png
    • 此时CS0xF000PC0xFFF0,内存地址为0xFFFF0
    • 可知,CPU加电后第一条执行位于0xFFFF0,并且第一条指令为长跳转指令
    • 可知,BIOS实例存储在cs:ip0xf000:0xe05b的位置
    • 使用si命令可对BIOS进行单步跟踪

    2 在初始化位置0x7c00设置实地址断点,测试断点正常。
    3 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和 bootblock.asm进行比较。

    修改tools/gdbinit,在lab1下执行make debug

    file obj/bootblock.o
    set architecture i8086
    target remote :1234
    b *0x7c00
    continue
    
    image.png
    • 调试发现0x7C00为主引导程序的入口地址,代码与bootasm.S一致
    • 使用ni可进行单步调试

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

    修改tools/gdbinit,在lab1下执行make debug

    file bin/kernel
    set architecture i8086
    target remote :1234
    b kern_init
    continue
    
    image.png
    • 在内核入口处增加断点,可以看到代码停在kern_init函数
    • 使用ni可进行单步调试

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

    为何开启A20,以及如何开启A20

    i8086时代,CPU的数据总线是16bit,地址总线是20bit,寄存器是16bit,因此CPU只能访问1MB以内的空间。因为数据总线和寄存器只有16bit,如果需要获取20bit的数据, 我们需要做一些额外的操作,比如移位。实际上,CPU是通过对segment(每个segment大小恒定为64K) 进行移位后和offset一起组成了一个20bit的地址,这个地址就是实模式下访问内存的地址:

    address = segment << 4 | offset
    

    理论上,20bit的地址可以访问1MB的内存空间(0x00000 - (2^20 - 1 = 0xFFFFF))。但在实模式下, 这20bit的地址理论上能访问从0x00000 - (0xFFFF0 + 0xFFFF = 0x10FFEF)的内存空间。也就是说,理论上我们可以访问超过1MB的内存空间,但越过0xFFFFF后,地址又会回到0x00000

    上面这个特征在i8086中是没有任何问题的(因为它最多只能访问1MB的内存空间),但到了i80286/i80386后,CPU有了更宽的地址总线,数据总线和寄存器后,这就会出现一个问题: 在实模式下, 我们可以访问超过1MB的空间,但我们只希望访问1MB以内的内存空间。为了解决这个问题, CPU中添加了一个可控制A20地址线的模块,通过这个模块,我们在实模式下将第20bit的地址线限制为0,这样CPU就不能访问超过1MB的空间了。进入保护模式后,我们再通过这个模块解除对A20地址线的限制,这样我们就能访问超过1MB的内存空间了。

    默认情况下,A20地址线是关闭的(20bit以上的地址线限制为0),因此在进入保护模式(需要访问超过1MB的内存空间)前,我们需要开启A20地址线(20bit以上的地址线可为0或者1)。具体代码如下:

    seta20.1:
        inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
        testb $0x2, %al
        jnz seta20.1
    
        movb $0xd1, %al                                 # 0xd1 -> port 0x64
        outb %al, $0x64                                 # 0xd1 means: write data to 8042's P2 port
    
    seta20.2:
        inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
        testb $0x2, %al
        jnz seta20.2
    
        movb $0xdf, %al                                 # 0xdf -> port 0x60
        outb %al, $0x60                                 # 0xdf = 11011111, means set P2's A20 bit(the 1 bit) to 1
    

    如何初始化GDT表

    • 可以看出这里所有GDT表项(除了空段)初始化为全段,此时段偏移量EIP等于物理地址
    ...
    #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)
    ...
    lgdt gdtdesc
    ...
    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
    
    

    段选择子
    在实模式下, 逻辑地址由段选择子和段选择子偏移量组成. 其中, 段选择子16bit, 段选择子偏移量是32bit. 下面是段选择子的示意图:

    image.png
    • 在段选择子中,其中的INDEX[15:3]是GDT的索引。
    • TI[2:2]用于选择表格的类型,1是LDT,0是GDT。
    • RPL[1:0]用于选择请求者的特权级,00最高,11最低。

    GDT的访问
    有了上面这些知识,我们可以来看看到底应该怎样通过GDT来获取需要访问的地址了。我们通过这个示意图来讲解:

    image.png
    • 根据CPU给的逻辑地址分离出段选择子。
    • 利用段选择子查找到对应的段描述符。
    • 将段描述符里的Base Address和EIP相加而得到线性地址。

    如何使能和进入保护模式

    开启A20,初始化gdt后,将控制寄存器CR0PE(bit0)置为1即可

    movl %cr0, %eax
    orl 0x1, %eax
    movl %eax, %cr0
    

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

    执行完bootasm.S后,系统进入保护模式, 进行bootmain.c开始加载OS

    • 定义ELF头指针,指向0x10000
    • 读取8个扇区大小的ELF头到内存地址0x10000
    • 校验ELF header中的魔数,判断是否为0x464C457FU
    • 读取ELF header中的程序段到内存中
    • 跳转到操作系统入口

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

    完成kdebug.c中函数print_stackframe

    要完成实验首先必须了解函数栈的构建过程

    • ebp为基址指针寄存器
    • esp为堆栈指针寄存器(指向栈顶)
    • ebp寄存器处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原ebp入栈后的栈顶),从该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的ebp
      举一个实际的例子查看ebpesp两个寄存器如何构建出完整的函数栈:
      leave等同于movl %ebp, %esppopl %ebp两条指令
    int g(int x) {
        return x + 10;
    }
    
    int f(int x) {
        return g(x);
    } 
    
    int main(void) {
        return f(20) + 8;
    }
    
    376065f7-9c44-4f1d-a2d3-09e644e0dfbc.gif
        int i,j;
        uint32_t ebp = read_ebp(), eip = read_eip();
        for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) {
            cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);
            // ebp向上移动4个字节为eip
            uint32_t *args = (uint32_t *)ebp + 2;
            // 再向上每4个字节都为输入的参数(这里只是假设4个参数,做实验)
            for (j = 0; j < 4; j ++) {
                cprintf("0x%08x ", args[j]);
            }
            cprintf("\n");
            print_debuginfo(eip - 1);
            // ebp指针指向的位置向上一个地址为上一个函数的eip
            eip = ((uint32_t *)ebp)[1];
            // ebp指针指向的位置存储的上一个ebp的地址
            ebp = ((uint32_t *)ebp)[0];
        }
    

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

    为什么有中断?

    操作系统需要对计算机系统中的各种外设进行管理,这就需要CPU和外设能够相互通信才行,CPU速度远快于外设,若采用通常的轮询(polling)机制,则太浪费CPU资源了。所以需要操作系统和CPU能够一起提供某种机制,让外设在需要操作系统处理外设相关事件的时候,能够“主动通知”操作系统,即打断操作系统和应用的正常执行,让操作系统完成外设的相关处理,然后在恢复操作系统和应用的正常执行。这种机制称为中断

    中断的类型

    • CPU外部设备引起的外部事件如I/O中断、时钟中断、控制台中断等是异步产生的(即产生的时刻不确定),与CPU的执行无关,我们称之为异步中断,也称外部中断
    • CPU执行指令期间检测到不正常的或非法的条件(如除零错、地址访问越界)所引起的内部事件称作同步中断,也称内部中断
    • 在程序中使用请求系统服务的系统调用而引发的事件,称作陷入中断,也称软中断,系统调用简称trap

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

    • CPU收到中断时,会查找对应的中断描述符表(IDT),确定对应的中断服务例程
    • IDT是一个8字节的描述符数组,IDT可以位于内存的任意位置,CPU通过IDT寄存器(IDTR)的内容来寻址IDT的起始地址。指令LIDTSIDT用来操作IDTR
    • DT的一个表项如下,4个字节分别存储offset的高位地址、段选择子和offset低位地址
      image.png
    • 中断处理过程如下图


      image.png

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

    查看SETGATE宏定义

    • 由代码看出SETGATE本质是设置生成一个4字节的中断描述表项
    • gate为中断描述符表项对应的数据结构,定义在mmu.hstruct gatedesc
    • istrap标识是中断还是系统调用,唯一区别在于,中断会清空IF标志,不允许被打断
    • seloff分别为中断服务例程的代码段与偏移量,dpl为访问权限
    #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;        \
    }
    

    查看vector.S定义的中断号定义

    • 保护模式下有256个中断号0~31是保留的, 用于处理异常和NMI(不可屏蔽中断); 32~255由用户定义, 可以是设备中断或系统调用.
    • 所有的中断服务例程,最终都是跳到__alltraps进行处理
    • 注意这里的标号对应的地址为代码段偏移量
    .text
    .globl __alltraps
    .globl vector0
    vector0:
      pushl $0
      pushl $0
      jmp __alltraps
    ...
    .globl vector255
    vector255:
      pushl $0
      pushl $255
      jmp __alltraps
    # vector table
    .data
    .globl __vectors
    __vectors:
      .long vector0
      .long vector1
    ...
      .long vector255
    

    由以上可填充idt_init,完成作业

    extern uintptr_t __vectors[];
    int i;
    for(i = 0 ; i < 256 ; i++) {
        SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
    }
    lidt(&idt_pd);
    

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

    通过之前的分析查看__alltraps所在的trappentry.S文件

    • 压栈各种需要传递给中断服务例程的信息,形成trapFrame,调用trap函数
    • 注意进入这个函数前,vector.S中已经压栈了1,2个参数
    .text
    .globl __alltraps
    __alltraps:
        # push registers to build a trap frame
        # therefore make the stack look like a struct trapframe
        pushl %ds
        pushl %es
        pushl %fs
        pushl %gs
        pushal
    
        # load GD_KDATA into %ds and %es to set up data segments for kernel
        movl $GD_KDATA, %eax
        movw %ax, %ds
        movw %ax, %es
    
        # push %esp to pass a pointer to the trapframe as an argument to trap()
        pushl %esp
    
        # call trap(tf), where tf=%esp
        call trap
    
        # pop the pushed stack pointer
        popl %esp
    
    • 最终调用了trap_dispatch根据中断号将中断分发给不同的服务例程
      +IRQ_OFFSET32,与之前32~255由用户定义, 为设备中断或系统调用的描述一致.
    • 填充时钟中断响应代码,完成实验
    void
    trap(struct trapframe *tf) {
        // dispatch based on what type of trap occurred
        trap_dispatch(tf);
    }
    
    static void
    trap_dispatch(struct trapframe *tf) {
        char c;
    
        switch (tf->tf_trapno) {
        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!
             */
            ticks++;
            if(ticks == TICK_NUM) {
                print_ticks();
                ticks = 0;
            }
            break;
        case IRQ_OFFSET + IRQ_COM1:
            c = cons_getc();
            cprintf("serial [%03d] %c\n", c, c);
            break;
        case IRQ_OFFSET + IRQ_KBD:
            c = cons_getc();
            cprintf("kbd [%03d] %c\n", c, c);
            break;
        //LAB1 CHALLENGE 1 : YOUR CODE you should modify below codes.
        case T_SWITCH_TOU:
        case T_SWITCH_TOK:
            panic("T_SWITCH_** ??\n");
            break;
        case IRQ_OFFSET + IRQ_IDE1:
        case IRQ_OFFSET + IRQ_IDE2:
            /* do nothing */
            break;
        default:
            // in kernel, it must be a mistake
            if ((tf->tf_cs & 3) == 0) {
                print_trapframe(tf);
                panic("unexpected trap in kernel.\n");
            }
        }
    }
    

    相关文章

      网友评论

          本文标题:ucore lab1

          本文链接:https://www.haomeiwen.com/subject/jhndpftx.html