操作系统实验一

操作系统实验一

在这里插入图片描述

lab0

基础操作及准备

1.lab0要求我们准备好实验所需的工具,但实际上大部分工具我们在大二时就已经安装完毕,比如Ubuntu和VMware。而且我们在逐渐的使用中也已经熟悉了自己喜欢的文本编辑器和操作方式,同时一些命令行的基本操作也已经烂熟于心,例如ls查找文件,clear清除信息,history查询历史命令行记录,mkdir新建文件夹,gedit或者vi或者vim则都可以新建文件,使用的是不同的文本编辑器,rm删除文件等等。

2.当然这些操作都较为基本,最主要的操作还是makefile或者make操作以及gcc编译器编译并执行c程序,chxomd编译py程序。

新的工具QEMU以及我的安装模式

在Linux运行环境中,QEMU用于模拟一台x86计算机,让ucore能够运行在QEMU上。为了能够正确的编译和安装 qemu,尽量使用最新版本的qemu,或者os ftp服务器上提供的qemu源码。在 Ubuntu 系统中,版本可以通过 gcc -v 或者 gcc --version 进行查看。
安装方式1:可直接使用ubuntu中提供的qemu,只需执行如下命令即可。

sudo apt-get install qemu-system

安装方式2:也可采用下面描述的方法对qemu进行源码级安装。需要到网站上下载源码并进行一系列神奇的命令行操作。这里不再赘述。

我的安装方式:
尝试过两种安装方式后第一个报错另一个则安装方式稍显繁琐。于是我仔细在第一种方式中改进。如果直接运行指导书中的命令会报错,看英文的意思大概是很多东西没有安装。所以想到不加参数而直接使用命令:

sudo apt-get install qemu

发现报错中出现友好提示,recommend:util操作可能解决这个问题。所以我尝试运行:

sudo apt-get install qemu-util

这个命令实际上将包括模拟x86所用的所有包都下载了,所以自然之后再运行指导书中的命令后就不会报错,至此实验所需工具安装完毕!

lab1

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

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

总结起来,总的步骤可以分为以下五步完成:
(1)通过GCC编译器将Kernel目录下的.c文件编译成OBJ目录下的.o文件。
(2)ld命令根据链接脚本文件kernel.ld将生成的*.o文件,链接成BIN目录下的kernel文件 。
(3)通过GCC编译器将boot目录下的.c,.S文件以及tools目录下的sign.c文件编译成OBJ目录下的*.o文件。
(4)ld命令将生成的*.o文件,链接成BIN目录下的bootblock文件。
(5)dd命令将dev/zero, bin/bootblock,bin/kernel 写入到bin/ucore.img

①编译命令
命令展示详细信息后都是用的GCC,将.c文件编译成为.o文件。
-l: 对于其中命令-I的含义如下:-I /home/hello/include表示将/home/hello/include目录作为第一个寻找头文件的目录。
-o: 经查询,-o OUTPUT’–output=OUTPUT’使用OUTPUT作为’ld’产生的程序的名字;如果这个选项没有指定,缺省的输出文件名是’a.out’.脚本命令’OUTPUT’也可以被用来指定输出文件的文件名。
-t:’–trace’打印’ld’处理的所有输入文件的名字。
-T: SCRIPTFILE’–script=SCRIPTFILE’把SCRIPTFILE作为连接脚本使用. 这个脚本会替代’ld’的缺省连接脚本(而不是增加它的内容),所以命令文件必须指定所有需要的东西以精确描述输出文件. 如果SCRIPTFILE在当前目录下不存在,‘ld’会在’-L’选项指定的所有目录下去寻找.多个’-T’选项会使内容累积.
-M:’–print-map’
打印一个连接位图到标准输出.一个连接位图提供的关于连接的信息有如下一些:目标文件和符号被映射到内存的哪些地方普通符号如何被分配空间.所有被连接进来的档案文件,还有导致档案文件被包含进来的那个符号。
举个例子

  • cc boot/bootasm.S //编译bootasm.c
    gcc -c boot/bootasm.S -o obj/boot/bootasm.o

②链接命令:
-o即是把所有后面的.o文件链接起来。比如kernel的形成:

  • ld bin/kernel //链接成kernel
    ld -o bin/kernel
    obj/kern/init/init.o obj/kern/libs/readline.o
    obj/kern/libs/stdio.o obj/kern/debug/kdebug.o
    obj/kern/debug/kmonitor.o obj/kern/debug/panic.o
    obj/kern/driver/clock.o obj/kern/driver/console.o
    obj/kern/driver/intr.o obj/kern/driver/picirq.o
    obj/kern/trap/trap.o obj/kern/trap/trapentry.o
    obj/kern/trap/vectors.o obj/kern/mm/pmm.o
    obj/libs/printfmt.o obj/libs/string.o

③填写boot block命令:
生成并填写bookblock的操作在这里可以看出来:
dd if=/dev/zero of=bin/ucore.img count=10000
//创建大小为10000个块的ucore.img,初始化为0,每个块为512字节

dd if=bin/bootblock of=bin/ucore.img conv=notrunc
//把bootblock中的内容写到第一个块

dd if=bin/kernel of=bin/ucore.img seek=1 conv=notrunc
//从第二个块开始写kernel中的内容

2.一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?
如图在tools文件夹之中找到sign.c文件进入可发现如下代码:在这里插入图片描述根据这个代码可以发现,符合规范的硬盘主引导扇区的特征应该包括,
①大小为512个字节,
②没用到的其他位置设置为0,
③第511个字节0x55,第512个字节是0xAA,也就是说,最后一个和倒数第二个字节的内容是确定的,
④如果读出的字节数不是512,需要报错。即应该可以读出512.

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

1.从 CPU 加电后执行的第一条指令开始,单步跟踪 BIOS 的执行。
对于gdbinit,也就是gdb的初始设置,开始时发现是如下:

file bin/kernel
set architecture i8086
target remote :1234
break kern_init
continue

①在其中加入语句:set architecture i8086,再执行make debug,有如下结果
此时进入gdb调试,而且显示architecture被假设到i8086。
在这里插入图片描述
②之后进行单步跟踪。
在这里插入图片描述发现每执行一次si,白框内的指针就向下移动到下一个执行代码处,用指令x/2i $pc意思是展示出当前pc中指令,如此实现gdb模式下的单步跟踪。

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

如图,gdbinit文件里面的内容编写成这样,gdb调试的过程就被钉死,即建筑体假设完成后设置断点0x7c00,并且展示出来。

file bin/kernel
set architecture i8086
target remote :1234
break *0x7c00
continue

结果如下,断点功能正常。
在这里插入图片描述

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

debug: $(UCOREIMG)
	$(V)$(QEMU) -S -s -parallel stdio -hda $< -serial null &
	$(V)sleep 2
	$(V)$(TERMINAL) -e "gdb -q -tui -x tools/gdbinit"
	

改写成
在这里插入图片描述
向其中加入-d in_asm,说明要与.asm也想比较。

运行后,Gedit bootasm.S得到,他在boot文件夹里。

# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
.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文件,他其中也有相似内容,代表含义相同。

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

从这里可以发现断点后运行,这两个代码应该是相同。

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

我i自己设置的断点是0x7c13,对应于相加操作,用gdb后结果如下:
在这里插入图片描述
正确

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

1.为何开启A20,以及如何开启A20?
①为何: A20打开是为了可以找到更远地址位置的数据,在寻址超过1M时,A20便会打开,让数据寻找更大的地方,也就是4G内存或更大的几个G的内存,这里的1M相当于一个高速缓存的地方。
具体来说,这是一个历史性问题。在intel处理器8086中,“段:偏移”最大能表示的内存地址是FFFF:FFFF,即10FFEFh,但是8086仅仅有20位寻址地址总线,仅仅能寻址到1MB,假设试图訪问1MB以上的内存地址,并不会错误发生,而是回卷。即又回到0000:0000地址,又从零開始寻址。但是到了80286时,真的能够訪问到1MB以上的内存了。假设遇到相同的情况,系统不会再回卷寻址,这就造成了向上不兼容,为了保证100%兼容,IBM想出了一个办法。使用8042键盘控制器来控制第20个地址位。这就是A20地址线。
②如何: 开启A20的步骤:
等待8042为空;
发送Write命令到8042;
等待8042为空;
将8042 得到字节的第2位置1,然后写入8042;

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

2.如何初始化GDT表?

首先载入GDT表 ,再通过将cr0寄存器PE位置1,cro的第0位为1,再通过长跳转更新cs的基地址,跳转到code32。由于是32,$PROT_MODE_CSEG的值为0x30。之后设置段寄存器,并建立堆栈,最后转到保护模式完成,进入boot主方法。这个实际上就是在代码里给出的过程:

.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

    # If bootmain returns (it shouldn't), loop.
spin:
    jmp spin

3.如何使能和进入保护模式?
通过将cr0寄存器PE位置1,cro的第0位为1

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

1.bootloader如何读取硬盘扇区的?
读取扇区的流程我们通过查询指导书可以看到:
1、等待磁盘准备好;
2、发出读取扇区的命令;
3、等待磁盘准备好;
4、把磁盘扇区数据读到指定内存。
同时有对应的0号硬盘的IO端口号如下:
在这里插入图片描述
结合Bootmain.c的代码可总结:代码的含义就是,在等待磁盘准备的阶段,如果0x1F7的最高两位是1,就跳出循环,也就是如果不是出于忙的状态就可以读取0X1F0的数据,就可以从准备状态跳出.在发出扇区的命令阶段,设置读取扇区数为1,规定好扇区信息的保存,分别用来保存扇区的编号,低八位,高两位信息,还有其磁盘号以及磁头号。然后又是等待,再是从0X1F0在disk不忙时读取数据。这样就完成了硬盘扇区的信息读取。
拓展
具体来说LBA模式中,使用LBA模式的PIO(Program IO)方式来访问硬盘的情况与CHS不同之处对比如下:
CHS方式:
写0x1f1: 0,写0x1f2: 要读的扇区数,写0x1f3: 扇区号,写0x1f4: 柱面的低8位,写0x1f5: 柱面的高8位,写0x1f6: 75位,101,第4位0表示主盘,1表示从盘,30位,磁头号,写0x1f7: 0x20为读, 0x30为写,读0x1f7: 第4位为0表示读写完成,否则要一直循环等待,读0x1f0: 每次读取1个word,反复循环,直到读完所有数据
24-bit LBA方式:
写0x1f1: 0,写0x1f2: 要读的扇区数,写0x1f3: LBA参数的0~7位,写0x1f4: LBA参数的8~15位,写0x1f5: LBA参数的16~23位,写0x1f6: 75位,111,第4位0表示主盘,1表示从盘,30位,LBA参数的24~27位,写0x1f7: 0x20为读, 0x30为写,读0x1f7: 第4位为0表示读写完成,否则要一直循环等待,读0x1f0: 每次读取1个word,反复循环,直到读完所有数据。

2.bootloader是如何加载ELF格式的OS?
阅读Makefile中的设置可知:e_magic,是用来判断读出来的ELF格式的文件是否为正确的格式;e_phoff,是program header表的位置偏移;e_phnum,是program header表中的入口数目;e_entry,是程序入口所对应的虚拟地址。

再阅读bootmain.c代码:

void
bootmain(void) {
    // read the 1st page off disk
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // is this a valid 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);
    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);
}

总结出加载方式:
首先我们从硬盘将扇区数据存到内存0x10000处,主要的是将其转换为ELF格式。
之后我们需要通过存储于头部的幻数来判断ELF的格式是否正确,再根据偏移量分别把程序段的数据读取到内存中,最后根据入口信息,找到内核的入口。

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

大部分函数的书写方式都已经在注释中加以提示,我们可以由此总结出函数设计步骤:
①首先,可以通过read_ebp()和read_eip()函数来获取当前ebp寄存器和eip 寄存器的值。
②可以发现ebp+12,ebp+16,ebp+20,ebp+24这四个地址正好是保存4个参数的值,我们可以通过它输出参数信息,之后我们调用函数信息输出函数。此时一个函数已经输出完毕。
③要转到下一函数,每次更新ebp的值为ebp[0],更新eip的值为ebp[1]。这样就可以转到下一函数。
④如果有一次进行ebp赋值时发现对应地址的值为0,表示当前函数为bootmain,这时他前面的函数已经输出完了,可以退出。
实现如下:

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]
      */
uint32_t ebp = read_ebp(), eip = read_eip();  
    int i, j;
    //#define STACKFRAME_DEPTH 20
    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]; 
    }
}

执行结果如下:
在这里插入图片描述

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

1.中断向量表中一个表项占多少字节?其中哪几位代表中断处理代码的入口?
Mmu.h中找到中断向量表结构:

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

这个表是GDT中的段描述表,提供基地址。

struct segdesc {
    unsigned sd_lim_15_0 : 16;        // low bits of segment limit
    unsigned sd_base_15_0 : 16;        // low bits of segment base address
    unsigned sd_base_23_16 : 8;        // middle bits of segment base address
    unsigned sd_type : 4;            // segment type (see STS_ constants)
    unsigned sd_s : 1;                // 0 = system, 1 = application
    unsigned sd_dpl : 2;            // descriptor Privilege Level
    unsigned sd_p : 1;                // present
    unsigned sd_lim_19_16 : 4;        // high bits of segment limit
    unsigned sd_avl : 1;            // unused (available for software use)
    unsigned sd_rsv1 : 1;            // reserved
    unsigned sd_db : 1;                // 0 = 16-bit segment, 1 = 32-bit segment
    unsigned sd_g : 1;                // granularity: limit scaled by 4K when set
    unsigned sd_base_31_24 : 8;        // high bits of segment base address
};

中断向量表一个表项占用8字节,(16+16)/4=8.
在这里插入图片描述
入口说明: 其中2-3字节是用于找到段,通过段选择子去GDT中找到对应的基地址,之后是偏移量,我认为是前半段的最后两个字节是偏移量,之后发现0-1字节也是,然后基地址加上偏移量就是中断处理程序的地址。

2.请编程完善kern/trap/trap.c中对中断向量表进行初始化的函数idt_init。
要用到的setgate宏定义如下:

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

这些门实际上是为后面做索引使用,观察上述setgate宏的参数,分别表示陷阱表程序所需信息,宏之内的正是各种参数,包括选择子,是否陷阱的标志,特权优先级,偏移量等。

因此算法思想设计如下:
①首先保存vectors.S中的中断处理例程的入口地址数组。
②填写陷阱表各个表项,IDT表项的个数是idt除以宏数量,对中断描述符表中的每一个表项进行设置,即填写各种陷阱程序的信息。在中断描述符表中通过建立中断描述符,其中存储了中断处理例程的代码段和偏移量,特权级。这样通过查询idt[i]就可定位到中断服务例程的起始地址。
③建立好中断门描述表后,通过指令lidt把中断门描述符表的起始地址装入IDTR寄存器中,从而完成中段描述符表的初始化工作。

dt_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函数。
函数设计思想:这个函数比较简单,每次到时钟中断处理程序,都将tickets变量加1,每次积满100时就输出一次tickets。这样可以验证时钟中断的功能。

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 == 0) {
            print_ticks();
        }
        break;

验证结果如下:
在这里插入图片描述

扩展练习

CHALLENGE1
switchto* 函数建议通过 中断处理的方式实现。主要要完成的代码是在 trap 里面处理 T_SWITCH_TO* 中断,并设置好返回的状态。函数思想是首先在函数外先定义好switchk2u,作用是保存tf,解决的情况是tf_cs不等于用户的cs,switchk2u保存tf,但是他的cs设置为用户的cs,他的ds,es,ss设置为用户的DS,标志设置和FL的标志相或,能够包含此标志。书写代码如下:

case T_SWITCH_TOU:
        if (tf->tf_cs != USER_CS) {
            switchk2u = *tf;
            switchk2u.tf_cs = USER_CS;
            switchk2u.tf_ds = switchk2u.tf_es = switchk2u.tf_ss = USER_DS;
            switchk2u.tf_esp = (uint32_t)tf + sizeof(struct trapframe) - 8;
            switchk2u.tf_eflags |= FL_IOPL_MASK;
            *((uint32_t *)tf - 1) = (uint32_t)&switchk2u;
        }
        break;

输入make grade之后结果,得分为40,满分,发现构造函数正确。
在这里插入图片描述
challenge2
用键盘实现用户模式内核模式切换。具体目标是:“键盘输入3时切换到用户模式,键盘输入0时切换到内核模式”。 基本思路是借鉴软中断(syscall功能)的代码,并且把trap.c中软中断处理的设置语句拿过来。

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值