清华大学-操作系统实验-Lab1

练习1

列出本实验各练习中对应的OS原理的知识点,并说明本实验中的实现部分如何对应和体现了原理中的基本概念和关键知识点。

在此练习中,大家需要通过静态分析代码来了解:

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

    • 编译 kmonitor.c panic.c clock.c console.c intr.c picirq.c trap.c

    • 编译 trapentry.s vectors.s pmm.c printfmt.c string.c

    • ld -m elf_i386 模拟指定的连接器 把一大堆.o文件(大多数是上面的步骤生成的)

    • 编译 boot 和tool里面的文件 并连接 (这个是bootloader?)

    • dd的用法https://www.cnblogs.com/ginvip/p/6370836.html 这里的主要做法是

      • 从/dev/zero 拷贝10000个块到ucore.img 一个块512字节
      • 从bootblock 拷贝1个块512字节到ucore.img,作为bootloader
      • 从第二个块开始把kernel赋值到ucore.img,138个块
  2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

    倒数第二个字节是0x55,倒数第一个字节是0xaa

补充材料:

如何调试Makefile

当执行make时,一般只会显示输出,不会显示make到底执行了哪些命令。

如想了解make执行了哪些命令,可以执行:

$ make "V="

要获取更多有关make的信息,可上网查询,并请执行

$ man make

练习2

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

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

提示:参考附录“启动后第一条执行的指令”,可了解更详细的解释,以及如何单步调试和查看BIOS代码。

提示:查看 labcodes_answer/lab1_result/tools/lab1init 文件,用如下命令试试如何调试bootloader第一条指令:

$ cd labcodes_answer/lab1_result/
$ make lab1-mon

留一个问题,为什么我自己编译的bootloader貌似就有问题??

问题在于ucore_lab1里面的启动有问题,所以在ucore_lab1_result里面去实验

  1. 修改makefile
    在这里插入图片描述

  2. 修改moninit

在这里插入图片描述

  1. cpu上电后初始化各个寄存器的值
    在这里插入图片描述

第一条指令的值就位于cs:eip 其中eip即为pc指针

在这里插入图片描述

跳转到bios代码

由于此时cs里面的基地址还是没变,所以bios的代码实际上在0xfe05b里面

练习3

练习3:分析bootloader进入保护模式的过程。(要求在报告中写出分析)

BIOS将通过读取硬盘主引导扇区到内存,并转跳到对应内存中的位置执行bootloader。请分析bootloader是如何完成从实模式进入保护模式的。

提示:需要阅读**小节“保护模式和分段机制”**和lab1/boot/bootasm.S源码,了解如何从实模式切换到保护模式,需要了解:

  • 为何开启A20,以及如何开启A20
  • 如何初始化GDT表
  • 如何使能和进入保护模式

开启A20是为了将键盘控制器8040的A20线置于高位,全部32条地址线可用。

下一代的基于Intel 80286 CPU的PC AT计算机系统提供了24根地址线,这样CPU的寻址范围变为 2^24=16M,同时也提供了保护模式,可以访问到1MB以上的内存了,此时如果遇到“寻址超过1MB”的情况,系统不会再“回卷”了,这就造成了向下不兼容。为了保持完全的向下兼容性,IBM决定在PC AT计算机系统上加个硬件逻辑,来模仿以上的回绕特征,于是出现了A20 Gate。他们的方法就是把A20地址线控制和键盘控制器的一个输出进行AND操作,这样来控制A20地址线的打开(使能)和关闭(屏蔽\禁止)。一开始时A20地址线控制是被屏蔽的(总为0),直到系统软件通过一定的IO操作去打开它(参看bootasm.S)。很显然,在实模式下要访问高端内存区,这个开关必须打开,在保护模式下,由于使用32位地址线,如果A20恒等于0,那么系统只能访问奇数兆的内存,即只能访问0–1M、2-3M、4-5M…,这样无法有效访问所有可用内存。所以在保护模式下,这个开关也必须打开。

在保护模式下,为了使能所有地址位的寻址能力,需要打开A20地址线控制,即需要通过向键盘控制器8042发送一个命令来完成。键盘控制器8042将会将它的的某个输出引脚的输出置高电平,作为 A20 地址线控制的输入。一旦设置成功之后,内存将不会再被绕回(memory wrapping),这样我们就可以寻址整个 286 的 16M 内存,或者是寻址 80386级别机器的所有 4G 内存了。

开启A20就是控制8042将P2输出的第2个bit置为1,具体方法为

  1. 等待8042 Input buffer为空;
  2. 发送Write 8042 Output Port (P2)命令到8042 Input buffer;
  3. 等待8042 Input buffer为空;
  4. 将8042 Output Port(P2)得到字节的第2位置1,然后写入8042 Input buffer;

代码

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 1101 0001
    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表

分段机涉及4个关键内容:逻辑地址、段描述符(描述段的属性)、段描述符表(包含多个段描述符的“数组”)、段选择子(段寄存器,用于定位段描述符表中表项的索引)。

*段描述符*

在分段存储管理机制的保护模式下,每个段由如下三个参数进行定义:段基地址(Base Address)、段界限(Limit)和段属性(Attributes)。在ucore中的kern/mm/mmu.h中的struct segdesc 数据结构中有具体的定义。

*全局描述符表* 全局描述符表的是一个保存多个段描述符的“数组”,其起始地址保存在全局描述符表寄存器GDTR中。GDTR长48位,其中高32位为基地址,低16位为段界限。由于GDT 不能有GDT本身之内的描述符进行描述定义,所以处理器采用GDTR为GDT这一特殊的系统段。

*选择子*

线性地址部分的选择子是用来选择哪个描述符表和在该表中索引一个描述符的。选择子可以做为指针变量的一部分,从而对应用程序员是可见的,但是一般是由连接加载器来设置的。选择子的格式如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Q9kkwRu-1597225518523)(E:\SJTUONE\OneDrive - sjtu.edu.cn\Note\操作系统\清华版本\第2章-基础操作.assets\image-20200801102503075.png)]

图3 段选择子结构

  • 索引(Index):在描述符表中从8192个描述符中选择一个描述符。处理器自动将这个索引值乘以8(描述符的长度),再加上描述符表的基址来索引描述符表,从而选出一个合适的描述符。
  • 表指示位(Table Indicator,TI):选择应该访问哪一个描述符表。0代表应该访问全局描述符表(GDT),1代表应该访问局部描述符表(LDT)。
  • 请求特权级(Requested Privilege Level,RPL):保护机制,在后续试验中会进一步讲解。

逻辑地址-物理地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WACMZHt0-1597225518524)(https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1_figs/image002.png)]

[1] 分段地址转换:CPU把逻辑地址(由段选择子selector和段偏移offset组成)中的段选择子的内容作为段描述符表的索引,找到表中对应的段描述符,然后把段描述符中保存的段基址加上段偏移值,形成线性地址(Linear Address)。如果不启动分页存储管理机制,则线性地址等于物理地址

加载GDT很简单 只需要直接加载就行

​ lgdt gdtdesc

使能保护模式也很简单,将cr0寄存器PE位置置1即可

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

但是转到保护模式还需要

更新cs

  ljmp $PROT_MODE_CSEG, $protcseg
  .set PROT_MODE_CSEG,        0x8
.set PROT_MODE_DSEG,        0x10  

设置段寄存器并建立堆栈

 # 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

栈指针esp 基址指针 ebp

练习4

练习4:分析bootloader加载ELF格式的OS的过程。(要求在报告中写出分析)

通过阅读bootmain.c,了解bootloader如何加载ELF文件。通过分析源代码和通过qemu来运行并调试bootloader&OS,

  • bootloader如何读取硬盘扇区的?
  • bootloader是如何加载ELF格式的OS?

提示:可阅读“硬盘访问概述”,“ELF执行文件格式概述”这两小节。

bootloader让CPU进入保护模式后,下一步的工作就是从硬盘上加载并运行OS。考虑到实现的简单性,bootloader的访问硬盘都是LBA模式的PIO(Program IO)方式,即所有的IO操作是通过CPU访问硬盘的IO地址寄存器完成。

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端口读数据

首先把os的代码读进内存

/* 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);
}

调用这个函数就可以从sceno扇区读取该扇区(512字节)的内容到dst位置

/* *
 * 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);
    }
}

就比如说我要从 第513 字节处load 512 字节到va处(扇区2 的第二个字节到 扇区3的第一个字节)

  • endva 是 va + 512

  • va先退一点 退多少呢? 退513 % 512 = 1 (这一步很关键,因为读扇区必须整块读,只有把起始扇区中的字节放在起始内存的前面去 程序从起始内存读到的数据才是正确的)

  • secno 是 513/512 + 1= 2

  • 在for循环中会读2 3扇区全部内容

以上就是 第一个问题 bootrloader如何读取扇区

第二个问题,如何加载elf文件

void
bootmain(void) {
    // read the 1st page off disk
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// ELFHDR 是0x10000, 把开头的八个扇区读到这个位置
    // is this a valid ELF? 通过储存在头部的幻数字判断是否是合法的elf,
    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);//uint32_t e_phoff;     // file position of program header or 0
    eph = ph + ELFHDR->e_phnum; //尾巴
    for (; ph < eph; ph ++) {
        readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
    }

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

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

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

((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))(); 这一句没怎么看懂,但是它就找到了内核代码的入口开始执行os内核代码了.

练习5

练习5:实现函数调用堆栈跟踪函数 (需要编程)

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

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 –
……

esp 是栈指针 指向栈顶(低地址)

ebp是基指针寄存器, 可以认为是一个函数的入口,通过它可以访问到函数调用栈当中的很多东西.

eip是指令指针寄存器 对应于函数返回的地址

一些内联函数

read_ebp();//inline
read_eip();//non-inline
print_debuginfo();//print_debuginfo函数完成查找对应函数名并打印至屏幕的功能
    

一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作(由硬件完成)。

这个就是某一层函数调用时的栈的情形.由于已经开启了段机制,这里的ebp eip都需要有段寄存器作为基址. 去访问 ebp时 ss堆栈段寄存器

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 (unit32_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]
      */
      uint32_t ebp = read_ebp();
      uint32_t eip = read_eip();
      int i = 0;
      for (; i < STACKFRAME_DEPTH; ++i)
      {
                cprintf("ebp:%x eip:%x\t", ebp,eip);
                uint32_t* arg;
                arg = (uint32_t *)ebp + 2;
                cprintf("args %x %x %x %x", arg[0], arg[1], arg[2],arg[3]);
                cprintf("\n");
                print_debuginfo(eip - 1);
                ebp = ((uint32_t *)ebp)[0];
                //eip = read_eip();
                eip = ((uint32_t *)ebp)[1];
      }
      
}

几个关键点

  1. 这里的地址是直接开辟新的uint32_t去保存的, 要想访问地址中的内容 需要强制类型转换为指针
  2. 函数读到的地址是线性地址(在当前环境下为物理地址)

练习6

练习6:完善中断初始化和处理 (需要编程)

请完成编码工作和回答如下问题:

  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。(问题在于怎么区分哪些是系统调用而哪些是普通中断?系统调用中断只有1个,就是121)

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

提示:可阅读小节“中断与异常”。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gBUFUK1F-1597225518525)(https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1_figs/image007.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AfX0qvDl-1597225518526)(https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1_figs/image009.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RIohWoWt-1597225518526)(https://chyyuu.gitbooks.io/ucore_os_docs/content/lab1_figs/image010.png)]

中断描述符表

中断描述符表把每个中断或异常编号和一个指向中断服务例程的描述符联系起来。同GDT一样,IDT是一个8字节的描述符数组,但IDT的第一项可以包含一个描述符。CPU把中断(异常)号乘以8做为IDT的索引。IDT可以位于内存的任意位置,CPU通过IDT寄存器(IDTR)的内容来寻址IDT的起始地址。指令LIDT和SIDT用来操作IDTR。两条指令都有一个显示的操作数:一个6字节表示的内存地址。

  1. 所以问题1的答案就是一个表项有8个字节组成

    X86的各种门的格式

其中中断用到的中断门描述符而调用使用的陷阱门描述符

其中selector去GDT找段描述符取得段基址,然后加上offset即可得到handler的入口

https://www.cnblogs.com/FrankChen831X/p/10772472.html

GDT定义在 pmm.c里面

         ++ticks;
         if (ticks % 100 == 0){
                print_ticks();
                ticks = 0;
        }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值