初学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

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

 

上一篇:关于c#:如何在不同的命名空间中处理相同的类名?


下一篇:配置文件中的数据库连接串加密了,你以为我就挖不出来吗?