代码阅读报告——bootstrap 一、源代码中重要函数或语句及关键技术的代码分析和注释 bootasm.S: .code16 //在实模式下运行的代码 .globl start //设定为全局函数,可以从外部进行调用 start: //首先执行的函数 cli //使硬件不能进行中断 xorw %ax,%ax //将ax寄存器初始化为零 movw %ax,%ds //初始化数据 seta20.1: inb $0x64,%al //等待硬件空闲 movb $0xd1, %al //将0xd1输出到第0x64号I\O端口 seta20.2: //以上代码的作用是打开A20地址线.在默认的情况下,第20根地址线一直为0,这样做的目的是为了向下兼容早期的PC.由早期的PC仅仅只是在实模式下进行寻址,这样所可能理论上可以寻到的最大地址应该是0xFFF0+0xFFFF.这看上去超过了1MB的地址空间,然而因为早期的PC 只有20根地址线,于是相当于最高位的进位时被忽略了,地址最终还是在1MB以内.所以当PC 有了32根地址线并且能够在保护模式下寻址4G的地址空间后,为了向下兼容,在默认情况下将第20根地址线一直置零,这样就可以让仅在实模式下运行的程序不会出现最高为的进位,相当于还是只有20根地址线在起作用. lgdt gdtdesc //将GDT 表的首地址加载到GDTR movl %cr0, %eax //将cr0寄存器的最低位置置1, orl $CR0_PE, %eax //标志进入保护模式 movl %eax, %cr0 // ljmp $(SEG_KCODE3), $start32 //跳转到32位模式,并执行32位模式的代码 .code32 //32位模式的代码 start32: //32位函数 call bootmain //调用main.c中的bootmain spin: //无限循环,但是理论上不会返回. jmp spin .p2align 2 //GDT表4字节对齐 gdt: //定义GDT表 bootmain.c: #define SECTSIZE 512 //每个磁盘片为512字节 void readseg(uchar*, uint, uint); //函数声明 void bootmain(void) { struct elfhdr *elf; //指向elf的指针 elf = (struct elfhdr*)0x10000; // 指向地址0x10000 readseg((uchar*)elf, 4096, 0); //首先读入4096字节,确保全部都读进去 if(elf‐magic != ELF_MAGIC) return; // 通过查看magic来确定elf是否正确组织. ph = (struct proghdr*)((uchar*)elf + elf‐phoff); //指针指向程序头表的首地址 eph = ph + elf‐phnum; //明确文件段的个数 for(; ph eph; ph++) { va = (uchar*)(ph‐va & 0xFFFFFF); //由虚拟地址映射为物理地址 readseg(va, ph‐filesz, ph‐offset); //将文件的每一段读入内存中相应的位置 if(ph‐memsz ph‐filesz) stosb(va + ph‐filesz, 0, ph‐memsz – ph‐filesz); //如果memsz } //的大小大于fiesz,则将多余地址均赋为0 entry = (void(*)(void))(elf‐entry & 0xFFFFFF); //将内核加载到内存中后转移 //到内核入口转移到内核入口地址处执行,并且不会再返回. entry(); } void waitdisk(void) { while((inb(0x1F7) & 0xC0) != 0x40) //等待硬盘空闲,否则循环. ; } void readsect(void *dst, uint offset) //读入一个磁盘分区 { outb(0x1F2, 1); // count = 1 //把1输出到端口0x1F2 outb(0x1F3, offset); //将offset输出到0x1F3 outb(0x1F4, offset 8); outb(0x1F5, offset 16); outb(0x1F6, (offset 24) | 0xE0); outb(0x1F7, 0x20); // 以上为将参数输出到端口 waitdisk(); } void readseg(uchar* va, uint count, uint offset) //从硬盘读文件 { uchar* eva; eva = va + count; //找到内存中加载地址的最末端 va ‐= offset % SECTSIZE; //将链接地址转换成加载地址 offset = (offset / SECTSIZE) + 1; //将在硬盘中的偏移字节由字节数换成扇区数, //由于内核可执行程序是从磁盘的第二扇区开始存储的,所以要加1 for(; va eva; va += SECTSIZE, offset++) readsect(va, offset); //一扇区一扇区的读取文件 二、操作系统启动引导的流程分析 1.PC启动时,首先进入的是实模式,并且开始执行位于地址0xFFFF0处得代码,也就是BIOS的起始位置的代码。 2.BIOS先进行一系列的系统自检,然后初始化位于地址0的中断向量表。最后BIOS将启动盘的第一个扇区装入到0x7C00,并开始执行此处的代码。 3.先初始化寄存器值,然后打开A20地址线。转换为保护模式,跳转执行32位代码。 4.启动保护模式数据段寄存器,初始值化寄存器。,调用bootmain函数。 5.建立elf指针,指向0x10000,读入指向地址后的4096个字节。并验证elf是否组织良好。 6.将elf中的内核文件从磁盘中读入到内存。 7.将控制权交给内核。 三、 简答题目的简要回答 (1)仔细阅读Makefile,分析xv6.img 是如何一步一步生成的。1.先生成bio.o console.o exec.o file.o fs.o ide.o ioapic.o kalloc.o kbd.o lapic.o main.o mp.o picirq.o pipe.o proc.o spinlock.o string.o swtch.o syscall.o sysfile.o sysproc.o timer.o trapasm.o trap.o uart.o vectors.o vm.o 2.定义TOOLPREFIX,并判断是否存在,如果存在侧在正确的端口中输出,不存在在错误的端口中输出。 3.定义QEMU,并判断是否存在,如果存在侧在正确的端口中输出,不存在在错误的端口中输出。 4.编译bootmain.c生成bootmain.o,编译bootasm.S生成bootmain.o,链接bootmain.o ,bootasm.S生成bootblock.o,并将程序入口函数定位start,令start的内存地址为0x7C00。将bootblock.o中的内容复制到bootblock中,并且声明不复制重分配和符号信息。 5.编译bootother.S生成bootother.o,链接bootother.o生成bootother.out,并将程序入口函数定位start,令start的内存地址为0x7C00。将bootother.out中的内容复制到bootother中,并且声明不复制重分配和符号信息。 6.编译initcode.S生成initcode.o,链接initcode.o生成initcode.out,并将程序入口函数定位start,令start的内存地址为0。将initcode.out中的内容复制到initcode中,并且声明不复制重分配和符号信息。 7.生成tags,vectors.S,通过规则生成ulib.o usys.o printf.o umalloc.o,通过规则生成_forktest。 8.通过mkfs.c fs.h生成mkfs。 9.将/dev/zero中的1000字节输出到xv6.img,将bootblock不间断的输出到xv6.img,将kernel从磁盘1不间断的输出到xv6.img,生成xv6.img。 (2)xv6如何做准备(建立GDT表等进入保护模式的) 1. 初始化寄存器数据 2. 建立GDT表。 3. 将cr0寄存器的最低位置置1,标识进入保护模式 4. 跳转执行32位代码,进入保护模式。 (3)引导程序如何读取硬盘扇区的?又是如何加载ELF 格式的OS的?1.确定读的文件数,文件首地址和偏置后,通过 readseg(),readsect()函数读取硬盘扇区。具体实现,见第一部分的readseg()和readsect()函数的分析。 2. 通过定义elf型的指针,指向0x10000.加载前4096个字节,并判断magic是否组织良好,这样来加载elf的OS。 四、对本部分原理和技术的阅后心得 本部分旨在将控制权交给内核之前的引导工作。读后,对操作系统内核是怎样被加载到内存中的有了一定了解。看见了软件与硬件结合的部分,对代码的运行机制和软件与硬件的联系有了一定的了解。感觉,想要彻底的了解操作系统,还需要部分的硬件知识,感觉以后应该多修修硬件课,这样可能会对以后的软件编写有大大的益处。