操作系统Ucore实验一:系统软件启动过程

LAB1:系统软件启动过程

[练习1]

理解通过make生成执行文件的过程:
1. 操作系统镜像文件ucore.img是如何一步一步生成的?(需要比较详细地解释Makefile中每一条相关命令和命令参数的含义,以及说明命令导致的结果)
2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么?

[解答]

Q1

  1. 运行命令:

    cd /moocos/ucore_lab/labcodes_answer/lab1_result	//进入文件夹
    mooc->make clean			//将生成的文件删除		
    mooc->make V= > mpro.txt	//将makefile的信息重定位到mpro.txt
    

    操作系统Ucore实验一:系统软件启动过程_第1张图片

  2. 打开mpro.txt进行分析

    可以从复杂的生成过程中,提取出几条关键的命令:

    命令1:

    在132,3541条命令中,其大致格式为:

    + cc xx.c
    gcc ... -o xxx.o
    

    其含义为将源代码.c文件编译成目标文件.o文件,例如:

    + cc kern/init/init.c
    gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o
    

    其含义为将源代码init.c转换成目标文件init.o

    命令2:

    在42~45行中,其大致格式为:

    + ld 
    ld xxx.o
    

    其含义为将目标文件.o文件转换成执行程序,例如:

    + 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: 488 bytes
    build 512 bytes boot sector: 'bin/bootblock' success!
    

    其含义为生成bootloader程序

    命令3

    在46~48行,其大致格式为:

    dd xxx
    

    其含义为将执行程序放入虚拟硬盘,例如:

    dd if=/dev/zero of=bin/ucore.img count=10000
    

    其含义为将bootloader放入虚拟硬盘(此处为ucore.img)中,qemu未来会基于虚拟硬盘中的数据执行相应代码。

    接下来详细解释每一条命令和相关参数:

    #将源代码kern/init/init.c编译为中间文件
    + cc kern/init/init.c
    #-Wall 生成所有警告信息 -ggdb 尽可能的生成gdb可以使用的调试信息 -m32 -gstabs以stabs格式声称调试信
    #息,但是不包括gdb调试信息 -nostdinc 使编译器不再系统默认的头文件目录中找头文件 -fno-stack-#protector 停用栈溢出保护
    gcc -Ikern/init/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/init/init.c -o obj/kern/init/init.o
    #将源代码readline.c编译为中间文件
    + cc kern/libs/readline.c
    gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/readline.c -o obj/kern/libs/readline.o
    #将源代码stdio.c编译为中间文件
    + cc kern/libs/stdio.c
    gcc -Ikern/libs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/libs/stdio.c -o obj/kern/libs/stdio.o
    #将kdebug.c编译为中间文件
    + cc kern/debug/kdebug.c
    gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kdebug.c -o obj/kern/debug/kdebug.o
    #将kmonitor.c编译为中间文件
    + cc kern/debug/kmonitor.c
    gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/kmonitor.c -o obj/kern/debug/kmonitor.o
    #将panic.c编译为中间文件
    + cc kern/debug/panic.c
    gcc -Ikern/debug/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/debug/panic.c -o obj/kern/debug/panic.o
    #将clock.c编译为中间文件
    + cc kern/driver/clock.c
    gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/clock.c -o obj/kern/driver/clock.o
    #将console.c编译为中间文件
    + cc kern/driver/console.c
    gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/console.c -o obj/kern/driver/console.o
    #将intr.c编译为中间文件
    + cc kern/driver/intr.c
    gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/intr.c -o obj/kern/driver/intr.o
    #将picirq.c编译为中间文件
    + cc kern/driver/picirq.c
    gcc -Ikern/driver/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/driver/picirq.c -o obj/kern/driver/picirq.o
    #将trap.c编译为中间文件
    + cc kern/trap/trap.c
    gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trap.c -o obj/kern/trap/trap.o
    #将trapentry.S汇编为中间文件
    + cc kern/trap/trapentry.S
    gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/trapentry.S -o obj/kern/trap/trapentry.o
    #将vector.S汇编为中间文件
    + cc kern/trap/vectors.S
    gcc -Ikern/trap/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Ikern/debug/ -Ikern/driver/ -Ikern/trap/ -Ikern/mm/ -c kern/trap/vectors.S -o obj/kern/trap/vectors.o
    #将pmm.c编译为中间文件
    + cc kern/mm/pmm.c
    gcc -Ikern/mm/ -fno-builtin -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/pmm.o
    #将printfmt.c编译为中间文件
    + cc libs/printfmt.c
    gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/printfmt.c -o obj/libs/printfmt.o
    #将string.c编译为中间文件
    + cc libs/string.c
    gcc -Ilibs/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/  -c libs/string.c -o obj/libs/string.o
    #链接生成可执行文件Kernel
    + ld bin/kernel
    #参数介绍:-m 模拟指定的连接器 -nostdlib 不用标准库
    ld -m    elf_i386 -nostdlib -T tools/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
    #将bootasm.S汇编成中间文件
    + cc boot/bootasm.S
    gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootasm.S -o obj/boot/bootasm.o
    #将bootmain.c编译成中间文件
    + cc boot/bootmain.c
    gcc -Iboot/ -fno-builtin -Wall -ggdb -m32 -gstabs -nostdinc  -fno-stack-protector -Ilibs/ -Os -nostdinc -c boot/bootmain.c -o obj/boot/bootmain.o
    #将sign.c编译成中间文件,再生成可执行文件sign
    + 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
    #链接生成可执行文件bootblock
    + ld bin/bootblock
    #参数介绍 -N 指定读取/写入文本段
    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: 488 bytes
    build 512 bytes boot sector: 'bin/bootblock' success!
    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
    

Q2

符合规范的硬盘主引导扇区即大小为512bytes,且最后两个字节为0x55AA的一个硬盘扇区。

打开tools/sign.c文件,可得到以下内容:

    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;

由代码可得,可执行文件的大小不小于510bytes,在511和512字节分别放0x55和0xAA。

补充资料:

主引导记录(MBR),也被称为主引导扇区,是计算机开机以后访问硬盘时所必须要读取的第一个扇区。在深入讨论主引导扇区内部结构的时候,有时也将其开头的446字节内容特指为”主引导 记录“(MBR),其后是4个16字节的”磁盘分区表“(DPT)以及2字节的结束标志(55AA)。

在执行MBR的引导程序时,会验证MBR扇区最后两个字节是否为“55AA”,如果是“55AA”,那么系统才会继续执行下面的程序;如果不是“55AA”,则程序认为这是一个非法的MBR,那么程序将停止执行,同时会在屏幕上列出错误信息。

[练习2]

使用qemu执行并调试lab1中的软件:
1. 从CPU加电后执行的第一条指令开始,单步跟踪BIOS的执行。
2. 在初始化位置0x7c00设置实地址断点,测试断点正常。
3. 从0x7c00开始跟踪代码运行,将单步跟踪反汇编得到的代码与bootasm.S和bootblock.asm进行比较。
4. 自己找一个bootloader或内核中的代码位置,设置断点并进行测试。
  1. 根据提示,打开/tools/gdbinit文件,得到以下内容:
file obj/bootblock.o
target remote :1234
break bootmain
continue

通过查阅手册可以推测,以上的指令与执行的操作相关。

为方便调试的展开(如实现单步跟踪、设置断点),将gdbinit修改为如下内容:

target remote :1234
set architecture i8086

通过以上配置可以在每次gdb命令行前强制反汇编当前的指令。

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

    由于是单步跟踪,故需要将continue删除。因为continue是连续执行的意思。

    在lab1的目录下执行make debug

在这里插入图片描述

可以看到gdb停在第一条指令处,如果查看当前执行的指令的具体内容,需要执行以下操作:

获取当前的$cseip

$cs = 0xf000
$eip = 0xfff0

转换为实地址模式下的地址为:

$cs << 4 | $eip = 0xffff0

如此转换的原因是,在实模式下,内存寻址方式和8086相同,由16位寄存器的内容乘以16(10H)当做段基地址,加上16位偏移地址形成20位物理地址。代码存放寄存器CS(Code Segment)存放正在运行的程序代码所在段的段基址,表示当前使用的指令代码可以从该寄存器指定的存储器段中取得,相应的偏移量则由IP提供。

执行命令x/i 0xffff0,可得到

=> 0xffff0:     ljmp   $0xf000,$0xe05b

这就是CPU加电后执行的第一条指令。

执行si即可单步调试。

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

    执行命令(对于绝对地址,需要添加*将其作为地址):

    b *0x7c00
    

    image-20211105091045889

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

    单步跟踪反汇编后得到的代码为:

操作系统Ucore实验一:系统软件启动过程_第2张图片

查看bootasm.S和bootblock.asm文件,可以得到:

操作系统Ucore实验一:系统软件启动过程_第3张图片
操作系统Ucore实验一:系统软件启动过程_第4张图片

可以看出,三个文件的结果是一致的。

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

    bootloader的起始位置是0x7c00,故设置断点为0x7c02来进行测试。

    b *0x7c02
    continue
    x/2i $pc
    

    操作系统Ucore实验一:系统软件启动过程_第5张图片

[练习3]

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

根据提示,我们首先查看lab1/boot/bootasm.S源码,并补充注释:

#include 
#asm.h文件中包含一些宏定义,用于定义gdt,gdt是保护模式使用的全局段描述符表,其中存储段描述符。
# Start the CPU: switch to 32-bit protected mode, jump into C.
# The BIOS loads this code from the first sector of the hard disk into
# memory at physical address 0x7c00 and starts executing in real mode
# with %cs=0 %ip=7c00.
#启动CPU:切换到32位保护模式,跳转到C函数
#BIOS将代码从硬盘的第一扇区加载到物理内存0x7c00处并在实模式下开始执行
#cs的内容为0 ip的内容为0x7c00

.set PROT_MODE_CSEG,        0x8                     # kernel code segment selector
#提供gdt中代码段的索引
.set PROT_MODE_DSEG,        0x10                    # kernel data segment selector
#提供gdt中数据段的索引
.set CR0_PE_ON,             0x1                     # protected mode enable flag
#保护模式使能标志,为1是开启保护模式
# start address should be 0:7c00, in real mode, the beginning address of the running bootloader
#开始地址为0x7c00 在实模式中,这个地址为bootloader开始的地址
.globl start
start:
#相当于C语言中的main函数,BIOS调用程序时从这里开始执行
.code16                                             # Assemble for 16-bit mode
#以下代码在实模式下执行,告诉编译器使用16位模式编译
    cli                                             # Disable interrupts
    #关闭中断
    cld                                             # String operations increment
	#设置字符串操作是递增方向
    # Set up the important data segment registers (DS, ES, SS).
    #设置重要的数据段寄存器
    xorw %ax, %ax                                   # Segment number zero
    #清零ax 相当于movw $0,%ax
    #xorw比mov reg,0的优势是:
    	1. 比mov reg,0有更小的代码大小
    	2. 避免对以后的代码进行部分注册惩罚
    	3. 不使用执行单元,节省电源并释放执行资源
    	4. 较小的uop(无即时数据)会在UOP缓存线中留下空间,以便在需要时获取附近的指令
    	5. 不会用尽物理寄存器文件中的条目
    movw %ax, %ds                                   # -> Data Segment
    #设置ds为0
    movw %ax, %es                                   # -> Extra Segment
    #设置es为0
    movw %ax, %ss                                   # -> Stack Segment
	#设置ss为0
    # Enable A20:
    #  For backwards compatibility with the earliest PCs, physical
    #  address line 20 is tied low, so that addresses higher than
    #  1MB wrap around to zero by default. This code undoes this.
    #使能A20,为向下兼容早期PC,物理地址第20位调低,高于1MB的地址回到0
    #A20地址由键盘控制器芯片042管理,所以给8042发命令激活A20
    #8042有两个IO端口:0x60和0x64
    #激活流程:发送0xd1命令到0x64端口 -->  发送0xdf到0x60
seta20.1:
    inb $0x64, %al                                  # Wait for not busy(8042 input buffer empty).
    testb $0x2, %al
    jnz seta20.1
	#发送命令前等待输入缓冲区为空,缓冲区是否为空,通过8042的状态寄存器的第2位观察(testb $0x2,%al)
	#如果状态寄存器的第2位为1,则跳转到seta20.1符号循环执行
	#如果状态寄存器的第2位为0,则代表缓冲区为空
    movb $0xd1, %al                                 # 0xd1 -> port 0x64
    outb %al, $0x64                                 # 0xd1 means: write data to 8042's P2 port
    #发送0xd1到0x64端口

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
	#此处操作与上述一致,发送0xdf到0x60端口
    # Switch from real to protected mode, using a bootstrap GDT
    # and segment translation that makes virtual addresses
    # identical to physical addresses, so that the
    # effective memory map does not change during the switch.
    #从实模式切换到保护模式,在切换过程中使用bootstrap GDT和段翻译将虚拟地址映射到物理地址,以使内存映射不会改变。
    lgdt gdtdesc
    #载入gdt
    movl %cr0, %eax
    orl $CR0_PE_ON, %eax
    movl %eax, %cr0
	#打开保护模式标志位,相当于按下保护模式的开关。
    # Jump to next instruction, but in 32-bit code segment.
    # Switches processor into 32-bit mode.
    #进入保护模式
    ljmp $PROT_MODE_CSEG, $protcseg

.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

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

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

下面,我们回答以下子问题来加深对问题的理解:

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

    8088/8086只有20位地址线,PC的寻址结构是segment:offset,segment和offset都是16位寄存器,最大值为0x0ffff,换算成物理地址是segment左移4位,再加上offset,所以segment:offset能表达的寻址空间最大为0ffefh,即1088KB,超过20位地址线的寻址能力,早期由于内存较小,即使超过影响也不大,如寻址100000h时可以访问00000h,但是之后出现大的内存,如80286的地址线达到24位,此时如果超过,则多出来的接近64k的存储器成为可能,为了保持向下兼容,于是出现A20 Gate。
    PC机再设计上在第21条地址线(A20)做了一个开关,开关打开时,这条地址线和其他地址线一样可以使用,关闭时A20始终为0,则系统只能访问x.?M的内存,x一定是一个偶数,但是在实模式下如果要访问高端内存区或者在保护模式下32位地址线一定要都有效,这两种情况下A20都要打开。
    那么如何开启A20呢?工程师使用8042键盘控制器来控制A20 Gate,理论上说,只要操作8042芯片的输出端口(64h)的bit1就可以控制A20,但是当我准备向8042的输入缓冲区写入数据时,可能还有其他数据没有处理,所以首先禁止键盘操作,等数据缓冲区中没有数据后,再去操作8042打开或者关闭A20。
    打开A20的步骤如下:
    	1.关闭中断;
        2.等待8042 Input buffer为空;
        3.发送禁止键盘操作命令;
        4.等待8042 Input buffer为空;
        5.发送读取8042 Output Port命令;
        6.等待8042 Output buffer有数据;
        7.读取8042 Output buffer,并保存得到的字节;
        8.等待8042 Input buffer为空;
        9.发送Write 8042 Output Port命令到8042 Input buffer;
        10.等待8042 Input buffer为空;
        11.将从8042 Output Port得到的字节的第2位置1(或清0),然后写入8042 Input buffer;
        12.等待,直到8042 Input buffer为空为止;
        13.发送允许键盘操作命令到8042 Input buffer;
        14. 打开中断。
    
  2. 如何初始化GDT表

    全局描述符表(GDT)是一个用于界定不同内存区域的特征的数据结构,存在于内存,全局描述符的条目描述及规定不同段的各种特征,包括基地址、大小和访问等特权。

    全局描述符表用于内存地址的转换,访问GDT需要segment selector(索引用于查找GDT的条目)和segment offset。

    GDT提供内存保护,在80286之前的处理器中只有实模式,所有程序都可访问任意内存,GDT是保护模式下限制非法内存访问的一种方式。

    查看lab1/boot/bootasm.S,底部有初始化GDT的代码

    # 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
        SEG_ASM(STA_W, 0x0, 0xffffffff)                 # data seg for bootloader and kernel
    
    gdtdesc:
        .word 0x17                                      # sizeof(gdt) - 1
        .long gdt                                       # address gdt
    

​ 如代码所示,初始化GDT需要初始化内核代码段描述符和内核数据段描述符。

  1. 如何使能和进入保护模式

    进入“保护模式”我们需要打开开关(控制寄存器),X86的控制寄存器一共有4个:CR0、CR1、CR2、CR3,进入“保护模式”的开关在CR0上。CR0包含6个预定义标志,0位是保护允许位PE,用于启动保护模式,如果PE位为1,则保护模式启动,如果PE=0,则在实模式下运行。

    对应代码为:

        movl %cr0, %eax #用通用寄存器保存当前CR0寄存器的值
        orl $CR0_PE_ON, %eax #CR0_PE_ON为宏定义值为0x00000001,或操作后将eax中的第0位设置为1
        movl %eax, %cr0 #将eax中的值保存到cr0寄存器
    

    通过长跳转,更新CS寄存器的基地址,代码如下:

    ljmp $PROT_MODE_CSEG, $protcseg
    

    设置段寄存器,并建立堆栈,代码如下:

    .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
    

    下面回答本题的问题:分析bootloader进入保护模式的过程。

    步骤1,BIOS加载硬盘第一扇区到物理内存0x7c00处,并且开始执行bootloader。

    步骤2,CLI屏蔽中断,中断屏蔽是通过写CPU提供的中断屏蔽寄存器来完成的;CLD使DF复位,DF为方向标志(DF=0,地址向高地址增加;DF=1,地址向低地址减小)。

    步骤3,设置寄存器ax,ds,es,ss为0。

    步骤4,激活A20,激活步骤见1中打开A20的步骤

    步骤5,从实模式转换到保护模式,为保护内存不被非法访问,初始化全局描述符表使得虚拟地址和物理地址匹配可以相互转换。

    步骤6,将CR0的第0号设置为1,进入保护模式;指令跳转由代码段跳到protcseg位置;设置保护模式下的数据寄存器;设置堆栈寄存器并调用bootmain函数。

[练习4]

分析bootloader加载ELF格式的OS的过程。

查看bootmain.c文件,得到的结果如下(已添加注释):

#include 
#include 
#include 

/* *********************************************************************
 * This a dirt simple boot loader, whose sole job is to boot
 * an ELF kernel image from the first IDE hard disk.
 *
 * DISK LAYOUT
 *  * This program(bootasm.S and bootmain.c) is the bootloader.
 *    It should be stored in the first sector of the disk.
 *
 *  * The 2nd sector onward holds the kernel image.
 *
 *  * The kernel image must be in ELF format.
 *
 * BOOT UP STEPS
 *  * when the CPU boots it loads the BIOS into memory and executes it
 *
 *  * the BIOS intializes devices, sets of the interrupt routines, and
 *    reads the first sector of the boot device(e.g., hard-drive)
 *    into memory and jumps to it.
 *
 *  * Assuming this boot loader is stored in the first sector of the
 *    hard-drive, this code takes over...
 *
 *  * control starts in bootasm.S -- which sets up protected mode,
 *    and a stack so C code then run, then calls bootmain()
 *
 *  * bootmain() in this file takes over, reads in the kernel and jumps to it.
 * */
 /**
 * 这是一个简单的引导加载程序,唯一的工作就是引导来自第一个IDE硬盘的ELF内核映像
 * 磁盘布局
 * 这个程序(bootasm.S和bootmain.c)是引导加载程序应该存储在磁盘的第一扇区。
 * 第二个扇区包含内核镜像,内核镜像应为ELF格式
 * 开机步骤:
 * 当CPU启动,它将BIOS从硬盘加载到内存并执行BIOS
 * BIOS初始化设备,设置中断并读取启动设备的第一扇区,进入内存并跳转到它(启动bootloader)。
 * 控制启动bootasm.S并设置保护模式和一个堆栈、C代码然后运行、调用bootmain()
 **/
unsigned int    SECTSIZE  =      512 ;
struct elfhdr * ELFHDR    =      ((struct elfhdr *)0x10000) ;     // scratch space

/* waitdisk - wait for disk ready */
static void
waitdisk(void) {
    while ((inb(0x1F7) & 0xC0) != 0x40)
        /* do nothing */;
}

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

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

/* bootmain - the entry of bootloader */
void
bootmain(void) {
    // read the 1st page off disk
    // 读取ELF的头部
    readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);

    // is this a valid ELF?
    //通过检查ELFHDR中的幻数e_magic来判断是否是ELF文件
    if (ELFHDR->e_magic != ELF_MAGIC) {
        goto bad;
    }

    struct proghdr *ph, *eph;

    // load each program segment (ignores ph flags)
    // 按照描述符将ELF文件中数据载入内存
    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
    // 根据ELF头部存储的入口信息,找到内核的入口
    ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();

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

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

bootmain将内核从硬盘加载到内存中,并把控制权交给内核。

  1. bootloader如何读取硬盘扇区?

    对应的代码如下:

    /* 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 设置读取扇区的数目为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); //读扇区
    }
    

    其步骤解释如下:

    1. 等待硬盘空闲:调用函数waitdisk,其实现为while ((inb(0x1F7) & 0xC0) != 0x40),意思是不断查询读0x1f7寄存器的最高两位,直到最高位为0、次高位为1(磁盘空闲)才返回。
    2. 发出读取扇区的命令。outb(0x1F7, 0x20);为读取命令,读取的扇区编号放在0x1F3~0x1F6寄存器中。
    3. 发出命令后,再次等待硬盘空闲。
    4. 硬盘再次空闲后,开始从0x1F0寄存器读数据。insl的作用是"This function will read cnt dwords from the input port specified by port into the supplied output array addr.",单位为dword,因此SECTSIZE需要除以4。
  2. bootloader如何加载ELF格式的OS?

    对应代码如下:

    /* bootmain - the entry of bootloader */
    void
    bootmain(void) {
        // read the 1st page off disk
        // 读取ELF的头部
        readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
    
        // is this a valid ELF?
        //通过检查ELFHDR中的幻数e_magic来判断是否是ELF文件
        if (ELFHDR->e_magic != ELF_MAGIC) {
            goto bad;
        }
    
        struct proghdr *ph, *eph;
    
        // load each program segment (ignores ph flags)
        // 按照描述符将ELF文件中数据载入内存
        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
        // 根据ELF头部存储的入口信息,找到内核的入口
        ((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
    
    bad:
        outw(0x8A00, 0x8A00);
        outw(0x8A00, 0x8E00);
    
        /* do nothing */
        while (1);
    }
    

    其步骤解释如下:

    1. 从硬盘读取8个扇区数据到内存0x10000,并在这里强制转换成elfhdr
    2. 通过检查ELFHDR中的幻数e_magic来判断是否是ELF文件
    3. 按照描述符将ELF文件中数据载入内存
    4. 根据ELF头部存储的入口信息,找到内核的入口

[练习5]

实现函数调用堆栈跟踪函数:
我们需要在lab1中完成kdebug.c中函数print_stackframe的实现,可以通过函数print_stackframe来跟踪函数调用堆栈中记录的返回地址。
  1. 打开lab1/kern/debug/kdebug.c,定位到print_stackframe函数,得到以下内容:

    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]
          */
    }
    

    上述内容大致含义为:

    1. 调用read_ebp函数获取ebp的值
    2. 调用read_eip函数获取eip的值
    3. 
    	3.1 打印ebp和eip的值
    	3.2 打印参数
    	3.3 打印换行符
    	3.4 调用print_debuginfo,参数为eip-1
    	3.5 将stackframe弹出栈
    	注意:返回地址eip = ebp+4
    		 ebp = ss:[ebp]
    

    根据翻译补充函数,得到:

    void
    print_stackframe(void) {
        uint32_t ebp = read_ebp();
        uint32_t eip = read_eip();
        int i,j;
        for(i; i<STACKFRAME_DEPTH; i++)
        {
            cprintf("ebp:%#010x eip:%#010x args:",ebp,eip);
            uint32_t *args = (uint32_t *)(ebp+8);
            for (j=0;j<4;j++) {
                cprintf("0x%08x ",args[j]);
            }
            cprintf("\n");
        	print_debuginfo(eip-1);
        	eip = *(uint32_t *)(ebp+4);
        	ebp = *(uint32_t *)(ebp);
        }
    }
    

    验证:

    1. lab1/kern/debug/kdebug.c中补充代码,在终端中运行make qemu,得到结果如下

      操作系统Ucore实验一:系统软件启动过程_第6张图片

    2. labanswer中查看结果

操作系统Ucore实验一:系统软件启动过程_第7张图片

两者相似

补充资料:

  1. 如何打印带0x的8位数据?

    int a = 7;
    printf("%#010x\n",a);	//givens 0x00000007
    printf("0x%08x\n",a);	//givens 0x00000007
    printf("%#08x\n",a);	//givens 0x000007
    

    #代表的是在字符串前面加上0x,010表示输出10个bit,x是输出16进制。也可以写成0x%08x

    两者的区别在于%#010x的是包含0x输出10个bit,0x%08x是不连0x输出8个bit

  2. 指针和整数相加?

    指针*p和整数n相加的结果:p+n*sizeof(p的类型),如此题中获取args的地址,可以表示为(uint32_t *)(ebp+8)或者(uint32_t *)ebp+2

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

[练习6]

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

    中断向量表在./kern/mm/mmu.h中定义,打开该文件,得到:

操作系统Ucore实验一:系统软件启动过程_第8张图片

由上图可以看出中断描述符表中一个表项占(16+16+5+3+4+1+2+1+16)\8=8个字节,其中段选择子和偏移地址用来代表中断处理程序的入口地址,具体表现为:通过段选择子查找相应段描述符得到代码段的基址,再用基址加上偏移地址得到中断处理程序入口地址。段选择子segment selector在1631位,偏移地址低16位在描述符015位(即首16位),偏移地址高16位在49~64位(即末尾16位)。

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

    打开kern/trap/trap.c文件,查看idt_init函数

    /* idt_init - initialize IDT to each of the entry points in kern/trap/vectors.S */
    void
    idt_init(void) {
         /* LAB1 YOUR CODE : STEP 2 */
         /* (1) Where are the entry addrs of each Interrupt Service Routine (ISR)?
          *     All ISR's entry addrs are stored in __vectors. where is uintptr_t __vectors[] ?
          *     __vectors[] is in kern/trap/vector.S which is produced by tools/vector.c
          *     (try "make" command in lab1, then you will find vector.S in kern/trap DIR)
          *     You can use  "extern uintptr_t __vectors[];" to define this extern variable which will be used later.
          * (2) Now you should setup the entries of ISR in Interrupt Description Table (IDT).
          *     Can you see idt[256] in this file? Yes, it's IDT! you can use SETGATE macro to setup each item of IDT
          * (3) After setup the contents of IDT, you will let CPU know where is the IDT by using 'lidt' instruction.
          *     You don't know the meaning of this instruction? just google it! and check the libs/x86.h to know more.
          *     Notice: the argument of lidt is idt_pd. try to find it!
          */
    }
    

    其翻译如下:

    /* 对kern/trap/vector.S中的每个入口进行初始化 */
    void
    idt_init(void) {
        /**
        (1)每个中断服务的地址在哪?所有ISR的地址存储在__vectors中,__vectors在kern/trap/vector.S中,该文件由			            tools/vector.c生成
    	(2)现在建立在IDT中的ISR。idt[256]就是我们要建立的IDT,我们可以使用SETGATE macro来设置IDT的每一项
    	 (3) 设置IDT后,我需要使用lidt指令让CPU知道我在哪里使用IDT。
    	**/
    }
    

    查看kern/mm文件,得到:

    /* *
     * Set up a normal interrupt/trap gate descriptor
     *   - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate
     *   - sel: Code segment selector for interrupt/trap handler
     *   - off: Offset in code segment for interrupt/trap handler
     *   - dpl: Descriptor Privilege Level - the privilege level required
     *          for software to invoke this interrupt/trap gate explicitly
     *          using an int instruction.
     * */
    #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;        \
    }
    翻译如下:
        istrap:1--陷阱门 0--中断门
        sel: 代码段选择子
        off: 代码段偏移值
        dpl: 设置特权等级
    在《中断与异常》中可知,在保护模式下,最多会存在256个Interrupt/Exception Vectors。范围[0,31]内的32个向量被异常Exception和NMI使用,范围[32,255]内的向量被保留给用户定义的Interrupts
    

    实现idt_init代码如下:

    void
    idt_init(void) {
    	extern uintptr_t __vectors[];
    	int sum = sizeof(idt) / sizeof(struct gatedesc);
    	int i;
    	for(i = 0; i < sum; i++)
    	{
    		if(i <= 31) {
    			/*在/kern/mm/memlayout.h中*/
    			SETGATE(idt[i],1,GD_KTEXT,__vectors[i],DPL_KERNEL);
    		}else {
    			SETGATE(idt[i],0,GD_KTEXT,__vectors[i],DPL_KERNEL);
    		}
    	}
    	lidt(&idt_pd);
    }
    

    补充资料:

    1. extern和头文件的区别

      头文件里存放函数、变量、类的声明而不是定义,否则多次引用这个头文件就会多次定义。extern添加在变量前,表示声明一个全局变量;extern不添加则表示定义一个全局变量,如果有赋值,则都表示定义变量。对于函数,定义的时候用extern说明函数可以被外部引用,声明的时候用extern说明这是一个声明。

    2. uintptr_t是什么?

      在C中,不能对指针执行按位操作,因此先将指针转换为uintptr类型,然后执行按位操作。

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

    打开/kern/trap文件,查看trap_dispatch函数,即需要补充的函数:

        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!
             */
            break;
    处理时间中断
        1. 在一个时间中断后,你应该记录这个使用一个个全局变量记录这个事件,比如说kern/driver/clock.c中的ticks
        2. 每个TICK_NUM循环,你应该调用print_ticks()打印一些信息
    

    补充代码如下:

        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++;//在kern/driver/clock.c中可以看到定义
    		if(ticks == TICK_NUM)
            {
                ticks = 0;
                print_ticks();
            }
            break;
    

    结果如下:

操作系统Ucore实验一:系统软件启动过程_第9张图片

扩展练习1

扩展proj4,增加`syscall`功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务。

题目的含义为实现内核态和用户态之间的切换。题目中建议通过中断处理的方式实现,并会调用下面的test函数。

    static void
    switch_test(void) {
        print_cur_status();          // print 当前 cs/ss/ds 等寄存器状态
        cprintf("+++ switch to  user  mode +++\n");
        switch_to_user();            // switch to user mode
        print_cur_status();
        cprintf("+++ switch to kernel mode +++\n");
        switch_to_kernel();         // switch to kernel mode
        print_cur_status();
    }

从内核态切换到用户态(switch_to_user)

实现代码如下:

void switch_to_user(void) {
    asm volatile {
        //为trapframe预留用户栈ss、esp的空间
        "sub $0x8,%%esp \n"
        //完成从内核态进入中断向用户态转换
        "int %0 \n"
        //转换到用户态后将函数处于刚进入函数执行的状态
        "movl %%ebp,%%esp"
        :
        :"i"(T_SWITCH_TOU)
    };
}

case T_SWITCH_TOU:
	if(tf->tf_cs != USER_CS) {
        //将CS设置为用户代码段
        tf->tf_cs = USER_CS;
        //将DS、ES、SS设置为用户数据段
        tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS
        //设置eflags 确保ucore可以在用户模式下使用IO
        tf->tf_eflags |= FL_IOPL_MASK;
    }
	break;

补充资料:

  1. int和iret指令的使用

    int指令的格式为:int n,n为中断类型码,功能是引发中断过程。CPU执行int n指令相当于引发一个n号中断的中断过程。中断例程可以直接返回dos,也可以像子程序一样返回到中断产生的地方,这时需要使用iret——中断返回指令。iret与int相对,它会恢复原始的CS和IP值,并恢复标志寄存器的值。

  2. 寄存器的作用

    ss:栈段

从用户态到内核态(switch to kernel)

指令在用户态执行时,也会进行中断处理。但是中断处理程序都是在内核态,在执行的过程中需要将用户栈转换为内核栈,具体步骤如下:

1. 从当前进程的描述符中提取内核栈的ss0和esp0的信息。
2. 将SS设置为ss0,esp设置为esp0,同时压入用户栈的ss和esp,待中断返回时,从内核栈转为用户栈。
  1. 设置一个用户态可以使用的中断

    当用户态代码使用int指令调用中断处理程序时,要进行权限检查,只有该中断描述符的DPL>=CPL时,才可顺利进行中断调用。因此在IDT初始化时,将T_SWITCH_TOK对应的中断描述符DPL设置为用户态。

    SETGATE(idt[T_SWITCH_TOK],0,GD_KTEXT,__vectors[T_SWITICH_TOK],DPL_USER)
    
  2. 用户态触发中断

    void switch_to_kernel(){
    	asm volatile(
    		"int %0\n"
            "movl %%ebp,%%esp\n"
            :
            :"i"(T_SWITCH_TOK)
    	);
    }
    
    case T_SWITCH_TOK:
    	if(tf->tf_cs != KERNEL_CS) {
    	tf->tf_cs = KERNEL_CS;
    	tf->tf_ds = tf->tf_es = tf->tf_ss = KERNEL_DS;
        tf->tf_eflags &= ~FL_IOPL_MASK;   
        }
    

做扩展实验之前:

操作系统Ucore实验一:系统软件启动过程_第10张图片

做扩展实验之后:

操作系统Ucore实验一:系统软件启动过程_第11张图片

扩展练习2

用键盘实现用户模式内核模式切换。具体目标是:“键盘输入3时切换到用户模式,键盘输入0时切换到内核模式”。
  1. ./kern/init/init.c中的switch_to_userswitch_to_kernel两个函数添加到trap.c文件
  2. trap_dispatch中的case IRQ_OFFSET+IRQ_KBD中调用这两个函数,即键盘按下0时切换到内核,打印出switch to kernel 000,键盘按下3时切换到用户模式,打印出switch to user

实现函数如下:

    case IRQ_OFFSET + IRQ_KBD:
        c = cons_getc();
	if ( c == '3' ) {
	     cprintf("switch to user\n");
	     lab1_switch_to_user();
	     print_trapframe(tf);
	     lab1_switch_to_kernel();
	}else if ( c == '0' ) {
		cprintf("switch to kernel 000\n");
		lab1_switch_to_kernel();
		print_trapframe(tf);
	}
        cprintf("kbd [%03d] %c\n", c, c);
        break;

操作系统Ucore实验一:系统软件启动过程_第12张图片

与参考答案的区别

使用图形化界面Meld Diff Viewer,可以查看出以下不同:

kern/kdebug.c

操作系统Ucore实验一:系统软件启动过程_第13张图片

主要是地址、格式的表现形式不同,两种方式都可以,具体内容在实验代码部分已经说明。

kern/trap.c

操作系统Ucore实验一:系统软件启动过程_第14张图片

左侧(我的)代码将中断向量表分为两个部分,右侧没有区分。

操作系统Ucore实验一:系统软件启动过程_第15张图片

右侧定义一个trapframe(switchk2u)作为中间变量

相关知识点

  • 与OS原理中相应的知识点

    • CPU的编制与寻址:基于分段机制的内存管理

      如果我们将整个地址空间放入物理内存,那么栈和堆之间的空间没有被进程使用,却依然占用实际的物理内存。因此,分段机制应运而生,分段机制使得操作系统能够将不同的段放到不同的物理内存区域,从而避免了虚拟地址空间中的未使用部分占用物理内存。

    • CPU的中断机制

      CPU利用时间中断重新获得控制权,在实验中每隔100个时钟周期,就会打印100ticks。

    • 函数调用关系:在汇编级了解函数调用栈的结构和处理过程

    • 受限直接执行

      直接执行的明显优势是快速。但是在CPU上运行会带来一个问题——如果进程希望执行某种受限操作(如向磁盘发出I/O请求或获得更多系统资源)时,就不能简单允许,如果这样做,一个进程就可以读取或写入整个磁盘,这样所有的保护就会失效。因此,采用的方法是引入一种新的处理模式——用户模式。

    • 外设:串口/并口/CGA,时钟,硬盘

    • 编译运行bootloader的过程

    • 调试bootloader的方法

    • PC启动bootloader的方法

    • ELF执行文件的格式和加载

    • 外设访问:读硬盘,在CGA上显示字符串

    • 编译运行ucore OS的过程

    • ucore OS的启动过程

    • 调试ucore OS的方法

    • 中断管理:与软件相关的中断处理

    • 外设管理:时钟

  • 未对应的知识点

    • 进程调度
    • 基于分页机制的内存管理
    • 文件系统

你可能感兴趣的:(操作系统)