初学ucore之lab1

2021/9/18 6:05:47

本文主要是介绍初学ucore之lab1,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

------------恢复内容开始------------

初学ucore。

ucore的lab1并不难,每个练习的思路也很清晰。lab1学完,并看了他人的笔记巩固。写下自己的理解。

80386型CPU开机的流程:先执行在bios中的程序,但由于bios容量很小,不能完成所有的工作,也不具备更高的拓展性,所以他读取磁盘中第一个扇区(引导扇区)中的内容,将其加载至内存地址空间0x7c00处。之后将cs:ip指向0x7c00处,并执行引导程序的第一条指令。

练习一 :

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

     调用gcc把.c源码编译成了.o目标文件。然后通过ld 让这些目标文件转换为.out可执行文件

       Bootasm.s  bootmain.c->Bootasm.o  bootmain.o ->bootblock.out  ->(sign 处理)bootblock

       Kernel.ld init.o readline.o stdio.o kdebug.o ->kernel

       Bootblock + kernel ->ucore.img

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

   在sign.c文件中: 一个主引导扇区必须是512字节,且第510字节是0x55 第511为0xAA

当bios工作完成后,cpu的控制权移交给了ucore的引导程序bootloader,并由bootloader完成一些初始的操作:将cpu从实模式进入到保护模式,初始化GDT表

练习3:

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

在早期的8086CPU中,内存总线是20位的,由高16位的段基址和低16位的段内偏移共同构成一个20位的内存地址,而为了进入32位的保护模式,我们需要开启A20(第二十一位内存访问总线)

如何初始化GDT表?

    GDT表及其描述符已经在引导区中,载入即可。

如何进入保护模式?

    Cr0寄存器PE位置1就开启了保护模式

 
bootasm.S

#include <asm.h>

# 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.

.set PROT_MODE_CSEG,        0x8                     # kernel code segment selector
.set PROT_MODE_DSEG,        0x10                    # kernel data segment selector
.set CR0_PE_ON,             0x1                     # protected mode enable flag

# 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   清理环境,将一些寄存器置为0
    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

    # 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.
seta20.1:                                         #开启A20总线
    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。A20置为了1,打开了A20

    # 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.
    lgdt gdtdesc                                #一个简单的GDT表和其描述符已经静态储存在引导区中,载入即可
    movl %cr0, %eax                             #将cr0寄存器PE位置1便开启了保护模式
    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            #通过长跳转更新cs的基地址,进入下一个代码执行

.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      #跳转到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

 bootloader引导程序是位于设备的第一个扇区,即引导扇区的,而ucore的内核程序则是从第二个磁盘扇区开始往后存放的。bootmain.c的任务就是将kernel内核部分从磁盘中读出并载入内存,并将程序的控制流转移至指定的内核入口处。

ucore的内核文件在生成磁盘映像时是以ELF格式保存的

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

 

bootmain.c

#include <defs.h>
#include <x86.h>
#include <elf.h>

/* *********************************************************************
 * 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.
 * */
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) {    readsect从设备的第secno扇区读取数据到dst位置
    // 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) {   readseg简单包装了readsect,可以从设备读取任意长度的内容。
    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?   检查是否一个合格的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);
}

在引导程序bootloader将ucore的kernel加载至内存后,将cs:ip跳转至内核入口,接下来就开始进行内核的初始化操作,即/kern/init.c中的kern_init函数。

kern_init函数是内核的总控函数,内核中的各个组成部分都在kern_init函数中完成初始化。

(取经他人的补充)

#include <defs.h>
#include <stdio.h>
#include <string.h>
#include <console.h>
#include <kdebug.h>
#include <picirq.h>
#include <trap.h>
#include <clock.h>
#include <intr.h>
#include <pmm.h>
#include <kmonitor.h>
void kern_init(void) __attribute__((noreturn));
void grade_backtrace(void);
static void lab1_switch_test(void);

/**
 * 内核入口 总控函数
 * */
void
kern_init(void){
    extern char edata[], end[];
    memset(edata, 0, end - edata);

    // 初始化控制台(控制显卡交互),只有设置好了对显卡的控制后,std_out输出的信息(例如cprintf)才能显示在控制台中
    cons_init();                // init the console

    const char *message = "(THU.CST) os is loading ...";
    cprintf("%s\n\n", message);

    print_kerninfo();

    grade_backtrace();

    // 初始化物理内存管理器
    pmm_init();                 // init physical memory management

    // 初始化中断控制器
    pic_init();                 // init interrupt controller
    // 初始化中断描述符表
    idt_init();                 // init interrupt descriptor table

    // 初始化定时芯片
    clock_init();               // init clock interrupt
    // 开中断
    intr_enable();              // enable irq interrupt

    //LAB1: CAHLLENGE 1 If you try to do it, uncomment lab1_switch_test()
    // user/kernel mode switch test
    lab1_switch_test();

    /* do nothing */
    // 陷入死循环,避免内核程序退出。通过监听中断事件进行服务
    while (1);
}

 从kern_init函数的代码中可以看出,其依次完成了如下的几个主要工作:

  1. cons_init  初始化控制台(控制显卡交互)

  2. pmm_init  初始化物理内存管理器(lab1中里面暂时只是完成了GDT的重新设置,比较简单。而在lab2的物理内存管理的实现中,pmm_init才成为主角)

  3. pic_init 初始化中断控制器(内部通过与8259A中断控制器芯片进行交互,令ucore能够接收到来自硬件的各种中断请求)

  4. idt_init 初始化中断描述符表(在下面的中断机制一节中详细介绍)

  5. clock_init 初始化定时器(进行8253定时器的相关设置,将其设置为10ms发起一次时钟中断)

  6. intr_enable 完成了内核结构的初始化后,开启中断,至此ucore内核正式开始运行

 

在练习5与练习6中,主要了解的就是ucore的中断机制。用户态写下的程序需要调用系统函数,就需要用到中断,来暂时的从用户态变为内核态,调用完函数后重新返回至用户态

练习5: 补充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]
      */
    uint32_t ebp = read_ebp(), eip = read_eip();#读取ebp,eip的值,具体函数在这个程序中已经为我们封装好了

    int i, j; 
    for (i = 0; ebp != 0 && i < STACKFRAME_DEPTH; i ++) { #开始循环,程序中已经为我们算出了栈帧的深度
        cprintf("ebp:0x%08x eip:0x%08x args:", ebp, eip);#打印出当前ebp,和eip的值
        uint32_t *args = (uint32_t *)ebp + 2;            #args代表的是栈帧中ebp下方的参数 被保存的一些原函数的值
        for (j = 0; j < 4; j ++) {
            cprintf("0x%08x ", args[j]);                  #打印ebp之后的四个参数
        }
        cprintf("\n");
        print_debuginfo(eip - 1);           
        eip = ((uint32_t *)ebp)[1];              #ebp[1] ebp的下一位,指向的是返回地址,即eip的迭代
        ebp = ((uint32_t *)ebp)[0];             # ebp[0] 即ebp的所指向的旧的ebp的值
    } 
} 

 

 (取经)

ucore中断功能的组成部分

  ucore的中断工作机制大致可以分为以下几个部分:

  1. IDT中断描述符表的建立

  2. 中断栈帧的生成

  3. 接收到中断栈帧,通过对应的中断服务例程进行处理

  4. 中断服务例程处理完毕,中断返回

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

  

/* *
 * Interrupt descriptor table:
 *
 * Must be built at run time because shifted function addresses can't
 * be represented in relocation records.
 * */
static struct gatedesc idt[256] = {{0}};

static struct pseudodesc idt_pd = {
    sizeof(idt) - 1, (uintptr_t)idt
};

/* 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!
      */
    extern uintptr_t __vectors[];
    int i;
    // 首先通过tools/vector.c通过程序生成/kern/trap/verctor.S,并在加载内核时对之前已经声明的全局变量__vectors进行整体的赋值
    // __vectors数组中的每一项对应于中断描述符的中断服务例程的入口地址,在SETGATE宏的使用中可以体现出来
    // 将__vectors数组中每一项关于中断描述符的描述设置到下标相同的idt中,通过宏SETGATE构造出最终的中断描述符结构
    for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
        // 遍历idt数组,将其中的内容(中断描述符)设置进IDT中断描述符表中(默认的DPL特权级都是内核态DPL_KERNEL=0)
        SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
    }
    // set for switch from user to kernel
    // 用户态与内核态的互相转化是通过中断实现的,单独为其一个中断描述符
    // 由于需要允许用户态的程序访问使用该中断,DPL特权级为用户态DPL_USER=3
    SETGATE(idt[T_SWITCH_TOK], 0, GD_KTEXT, __vectors[T_SWITCH_TOK], DPL_USER);
    // load the IDT 令IDTR中断描述符表寄存器指向idt_pd,加载IDT
    // idt_pd结构体中的前16位为描述符表的界限,pd_base指向之前完成了赋值操作的idt数组的起始位置
    lidt(&idt_pd);
}

 

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

   中断向量表一个表项占用8字节,其中2-3字节是段选择子,0-1字节和6-7字节拼成位移,两者联合便是中断处理程序的入口地址。

 

部分内容借鉴https://www.cnblogs.com/xiaoxiongcanguan/p/13714587.html

初次学习,在这个博客中也学到了不少的内容,补充了一些课上并没有的东西。

 



这篇关于初学ucore之lab1的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程