ucore lab1

练习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");
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342