------------恢复内容开始------------
初学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的中断工作机制大致可以分为以下几个部分:
1. IDT中断描述符表的建立
2. 中断栈帧的生成
3. 接收到中断栈帧,通过对应的中断服务例程进行处理
4. 中断服务例程处理完毕,中断返回
/* * * 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
初次学习,在这个博客中也学到了不少的内容,补充了一些课上并没有的东西。