Loading... # [Lab 1: Booting a PC](https://pdos.csail.mit.edu/6.828/2018/labs/lab1/) ## 基础概念 ### 处理器架构 * `i386 i686 x86`是同一套指令集,86指这一系列`CPU`,`x`则表示`i3/i6`是不同的处理器型号,x86即泛指`i386 i686`等,都是`32`位处理器 * `x86_64`则为`x86`的衍生,为`x86`的`64`位版本,另外`amd64`也是一样的东西 * `aarch32`指`armv7`处理器架构下的指令集 * `aarch64`指`armv8`处理器架构下的指令集 ### `AT&T`格式汇编指令介绍 * 基本格式`command source, destination`,`AT&T`将源操作数放在第一位,目标操作数放在第二位,这与`Intel`格式的汇编语法是相反的 * 在寄存器操作数前用`%`标记,立即操作数前用`$`标记 * `testb/testw/testl s d` * `test`表示对两个操作数进行`AND`操作,其结果如果为`0`则将`CPU`中的`ZF`标志寄存器设置为`1`否则清除`ZF`寄存器的值,即设置为`0` * `b/w/l`为助记符,`b`用于`8`位字节值,`w`用于`16`位字值,`l`则用于`32`位长字值 * `je/jz` 检查`ZF`标志是否为`1`, 为`1`则跳转到目标地址 * `jne/jnz` 检查`ZF`标志是否为`0`, 为`0`则跳转到目标地址 ### `ZF`标志寄存器([FLAGS register](https://en.wikipedia.org/wiki/FLAGS_register)) * 此寄存器存储上一条指令的计算结果,结果为`0`则此寄存器值为`1`,否则为`0` ## Parts ### 制作内核 根据给出的 `make`命令构建出 `kernel.img`文件,这步基本没什么问题,该安装的环境装上即可,`GNU`的那套东西 ### 使用QEMU加载内核 有了内核文件当然要加载,由于以后的开发不可都是真机去调试,所以介绍了使用 `QEMU`模拟加载内核,与上个步骤一样,该安装的装上,我是在`Ubuntu 20.04 LTS`环境下直接安装的`QEMU`,没有按照`Lab`步骤自己编译。使用 `qemu-system-i386 kernel.img`(`qemu-system-i686`或 `qemu-system-x86`)便能将内核跑起来,展示出一个简单的 `Shell`环境,可以运行两个简单的命令,`help和kerninfo` ### 常规PC内存地址布局 ``` +------------------+ <- 0xFFFFFFFF (4GB) | 32-bit | | memory mapped | | devices | | | /\/\/\/\/\/\/\/\/\/\ /\/\/\/\/\/\/\/\/\/\ | | | Unused | | | +------------------+ <- depends on amount of RAM | | | | | Extended Memory | | | | | +------------------+ <- 0x00100000 (1MB) | BIOS ROM | +------------------+ <- 0x000F0000 (960KB) | 16-bit devices, | | expansion ROMs | +------------------+ <- 0x000C0000 (768KB) | VGA Display | +------------------+ <- 0x000A0000 (640KB) | | | Low Memory | | | +------------------+ <- 0x00000000 ``` 由此图可看出,前`1MB`的内存空间是保留的,给各种硬件设备使用,例如从`0x000F0000->0x000FFFFF`为`BIOS`的地址空间(想法:用来存储`BIOS`程序的地址空间?),从`0x000A0000->0x000BFFFF`为`VGA`显示的内存空间,而前`1MB`以外的所有内存叫做拓展内存,前`640KB`则叫做`低内存`区域。 * 早期的`CPU`仅仅支持`1MB`的寻址空间,所以设计师将最顶部的区域留给了硬件,最底部的`640KB`留给软件使用。而当`CPU`终于支持了超过`1MB`寻址空间达到`4GB`后,设计师为了兼容性依然保留了`1MB`情况下的内存设计。 * 当`CPU`支持`4GB`内存寻址(32位可寻址空间)能力后,一般会将顶部(最高位)的一些内存地址留给32位`PCI`设备 * 当`CPU`支持大于`4GB`内存寻址能力后,也会保留32位可寻址空间顶部的一些地址空间给需要映射32位地址的设备使用 * `BIOS`程序会做一些硬件自检与检测,所以会读取上述保留的硬件内存地址 ### 使用GDB调试内核 新版的`QEMU`使用`-s -S`参数来侦听`GDB`的传入连接([GDB usage](https://www.qemu.org/docs/master/system/gdb.html)),同时开启另一个终端,使用`make gdb`命令启动GDB调试,`JOS`源码中已经自带`GDB`的相关配置(`.gdbinit`),跑起来后我们能看到`GDB`吐出的调试信息: ```text gdb -n -x .gdbinit GNU gdb (Ubuntu 9.1-0ubuntu1) 9.1 Copyright (C) 2020 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word". + target remote localhost:1234 warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. The target architecture is assumed to be i8086 [f000:fff0] 0xffff0: ljmp $0x3630,$0xf000e05b 0x0000fff0 in ?? () + symbol-file obj/kern/kernel warning: A handler for the OS ABI "GNU/Linux" is not built into this configuration of GDB. Attempting to continue with the default i8086 settings. (gdb) ``` 能看到几个关键信息:`[f000:fff0] 0xffff0: ljmp $0x3630,$0xf000e05b` * `[f000:fff0]` 表示`CPU`起始的`CS`和`IP`寄存器指向的地址(这两寄存器的地址加在一起组成一个完整的地址,也就是`PC`的概念)`0xffff0`,此地址指向为`BIOS ROM`保留的`64KB`区域的最顶部 * `0xffff0`地址存储了第一个`BIOS`程序指令:`ljmp $ 0xf000,$ 0xe05b`,一个`jmp`指令,指示跳转到`CS=0x3630,IP=0xf000e05b` * 物理地址=16*分段地址+偏移量 ``` 16 * 0xf000(CS) + 0xfff0(IP) # 十六进制*16只需要在后面加上0即可 = 0xf0000 + 0xfff0 = 0xffff0 ``` 由上得知,`CPU`通电后`CS:IP`中存储的第一个地址就是`BIOS ROM`所在内存区域的顶部地址,这是在`CPU`的`CS:IP`中写死的地址,是`Intel 8086`处理器开始的方式。另外需要注意的是,此处`BIOS ROM`所对应的实际物理位置,是在主板上的,并不是在内存条中,包括上述的一些为硬件保留的地址,也应当都是在主板上的,而非内存条提供的内存。 ### `Boot.S`文件分析 整段代码内容就是将CPU从一开始的实模式切换到保护模式,实模式下只能操作16位内存地址空间(1MB),保护模式可以启用32位地址空间(4GB)。最后设置栈指针为`0x7c00`,这是一个固定大小的栈,栈地址向低位地址递减。 ```assmbly #include <inc/mmu.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. # # BIOS将硬盘第一个扇区数据读取到物理内存地址0x7c00(cs=0 ip=7c00)上并使用实模式执行 # 分段地址就是为了在CPU只有16位寄存器时通过两个寄存器(CS<<4+IP)存一个物理地址的骚操作,因为当年的CPU地址线具有20根,也就是20位寄存器才能存储下, # 定义几个常量后续会用到 .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方法给外部调用,链接器链接时需要用到 .globl start # 开始标记 start: # 以16位模式运行 .code16 # Assemble for 16-bit mode # 禁用中断 cli # Disable interrupts # 清除顺序(设置字符串操作为递增) cld # String operations increment # Set up the important data segment registers (DS, ES, SS). # 其实就是将这几个寄存器清零 # xor %ax,%ax,一个异或操作,自己异或自己=0 xorw %ax,%ax # Segment number zero # 将寄存器ax的值移动到ds寄存器,以下同理 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. # 遥想当年的CPU寄存器只有16位,而CPU的地址线则有20根, # 16位寄存器无法表示完整的20根地址线的寻址空间(1MB),于是有了分段地址这种操作。 # 将一个寄存器的地址作为分段地址,另一个寄存器作为OFFSET, # 合并到一起用来表示20根地址线的寻址空间(1MB),表示方式如下: # 分段地址左移4位+OFFSET,换算成二进制能得到一个20位的地址,超过20位,则CPU地址线不够, # 所以会将第21个bit(bit位是从0开始数的,第20位其实是第21个bit位)始终设置为0 # 因此,这里需要启用A20 # 怎么激活呢,由于历史原因A20地址位由键盘控制器芯片8042管理。所以要给8042发命令激活A20 # 8042有两个IO端口:0x60和0x64 # 激活流程位: 发送0xd1命令到0x64端口 --> 发送0xdf到0x60 # 发送命令给端口前先判断端口缓冲区是否为空 seta20.1: # in操作是将目标地址0x64端口数据读取到al寄存器 inb $0x64,%al # Wait for not busy # 判断al寄存器值第二位是否为1 test指令用来将 $0x2 & $al,并将结果设置给ZF标志寄存器 testb $0x2,%al # 判断ZF的值,如果缓冲区被占用跳转到seta20.1重新检测 jnz seta20.1 # 将0xd1值移动到寄存器al,由于in与out操作都只能从寄存器进行操作,所以这里先移动数据到寄存器 movb $0xd1,%al # 0xd1 -> port 0x64 # 发送寄存器中的数据到端口 outb %al,$0x64 # 同上seta20.1 seta20.2: inb $0x64,%al # Wait for not busy testb $0x2,%al jnz seta20.2 movb $0xdf,%al # 0xdf -> port 0x60 outb %al,$0x60 # Switch from real to protected mode, using a bootstrap GDT # and segment translation that makes virtual addresses # identical to their physical addresses, so that the # effective memory map does not change during the switch. lgdt gdtdesc # cr0寄存器中的第0位用来设置是否为保护模式(Protedted Enable),设置为1表示启动保护模式 # 先移动cr0寄存器中的数据到eax寄存器 movl %cr0, %eax # 做一个或操作,CR0_PE_ON为我们一开始定义的常量,值就是1,这个操作就是将寄存器eax中的数据第0位设置为1 orl $CR0_PE_ON, %eax # 将eax寄存器数据移动到cr0寄存器,至此已经开启保护模式 movl %eax, %cr0 # Jump to next instruction, but in 32-bit code segment. # Switches processor into 32-bit mode. ljmp $PROT_MODE_CSEG, $protcseg # 使用32位模式 .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. # 设置栈指针为0x7c00 movl $start, %esp # 调用内核 call bootmain # If bootmain returns (it shouldn't), loop. # 理论上call kernel代码后永远不会退出,但是如果真的退出,这里进行了一个循环 spin: jmp spin # Bootstrap GDT .p2align 2 # force 4 byte alignment gdt: SEG_NULL # null seg SEG(STA_X|STA_R, 0x0, 0xffffffff) # code seg SEG(STA_W, 0x0, 0xffffffff) # data seg gdtdesc: .word 0x17 # sizeof(gdt) - 1 .long gdt # address gdt ``` 至此我们已经设置`CPU`为保护模式并且能够寻址`32`位地址空间,接下来跳转到`bootmain`方法。 ### `main.c`文件分析 此文件主要用来引导内核程序,它将扫描硬盘第一个扇区,然后将`ELF`格式的`kernel`程序加载到内存中,然后跳转到内核入口处。 ```c #include <inc/x86.h> #include <inc/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(boot.S and main.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 boot.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. **********************************************************************/ //定义扇区大小 #define SECTSIZE 512 //定义ELF头文件在内存中的位置 #define ELFHDR ((struct Elf *) 0x10000) // scratch space void readsect(void*, uint32_t); void readseg(uint32_t, uint32_t, uint32_t); void bootmain(void) { //定义program header 结构指针,这两个指针将指向内存中segment开始和结尾的地址 struct Proghdr *ph, *eph; //将第一页加载到ELFHDR内存空间处 readseg((uint32_t) ELFHDR, SECTSIZE*8, 0); //检测刚加载到ELFHDR处的数据是否是ELFHDR if (ELFHDR->e_magic != ELF_MAGIC) goto bad; //指定内存中的program segment起始地址为ELFHDR+ELFHDR->e_phoff就是从ELFHDR结束开始e_phoff为ELFHD结束位置的偏移量 ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff); //指定内存中最后一个program segment地址 eph = ph + ELFHDR->e_phnum; //循环将扇区的program segment加载到内存中 for (; ph < eph; ph++) //指针++则表示指针指向的地址+指针数据结构大小*2,这里可以看成指针指向下一个program segment //ph->p_pa为需要加载到内存中地址的起始位置,ph->p_memsz为需要的内存大小,ph->p_offset为program segment //数据所在硬盘上的基于ELFHDR起始地址的偏移量 readseg(ph->p_pa, ph->p_memsz, ph->p_offset); //跳转到内核入口处(示例中ELF入口指定为0x10000c处,内核代码被加载到由ELF指定的LMA:0x100000处) ((void (*)(void)) (ELFHDR->e_entry))(); bad: outw(0x8A00, 0x8A00); outw(0x8A00, 0x8E00); while (1) /* do nothing */; } // Read 'count' bytes at 'offset' from kernel into physical address 'pa'. // Might copy more than asked void readseg(uint32_t pa, uint32_t count, uint32_t offset) { //program segment在内存中的结束地址 uint32_t end_pa; //由起始地址+内存大小 end_pa = pa + count; pa &= ~(SECTSIZE - 1); //将偏移量转换为扇区 offset = (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. //循环读取 while (pa < end_pa) { // 将一个扇区读取到指定的内存位置 readsect((uint8_t*) pa, offset); pa += SECTSIZE; offset++; } } void waitdisk(void) { // wait for disk reaady while ((inb(0x1F7) & 0xC0) != 0x40) /* do nothing */; } void readsect(void *dst, uint32_t offset) { // 等待硬盘准备好 waitdisk(); // 设置读取扇区的数目为1 outb(0x1F2, 1); // count = 1 outb(0x1F3, offset); outb(0x1F4, offset >> 8); outb(0x1F5, offset >> 16); outb(0x1F6, (offset >> 24) | 0xE0); outb(0x1F7, 0x20); // 0x20命令,读取扇区 //上面四条指令联合制定了扇区号 //在这4个字节线联合构成的32位参数中 //29-31位强制设为1 //28位(=0)表示访问"Disk 0" //0-27位是28位的偏移量 waitdisk(); // 读取数据到目标内存位置,每次读取128 bit读取4次 insl(0x1F0, dst, SECTSIZE/4); } ``` ### 内核加载顺序 BIOS 程序硬件检测完成后会将硬盘引导扇区加载到0x7c00至0x7dff的内存地址中,然后设置PC为`0x7c00`执行`boot.S` `boot.S`将CPU从`real mode`转换为`protected mode`,这将启用`32`位寻址空间,然后设置栈指针为0x7c00,栈空间增加时,栈指针地址递减,所以这将是一个固定大小的栈空间。设置完栈指针后,调用`main.c`。 `main.c`扫描硬盘的第一页,加载`ELF`格式的文件头,根据`ELF`的文件定义将内核加载到指定内存地址(`0x00100000`),然后跳转到内核程序入口(`0x0010000c`)开始执行`entry.S`。 `entry.S`之前使用的都为物理地址,`entry.S`则将`entrypgdir.c`中定义的页目录加载到`cr3`寄存器,这会将物理地址前`4MB`(`0x000000`-`0x3ff000`)映射到虚拟地址`KERNBASE+4MB`的位置。然后设置`cr0`寄存器以开启分页,最后设置`%esp`指定栈指针位置后跳转到`i386_init.c` ### Booting Output ```txt Booting from Hard Disk.. 6828 decimal is 15254 octal! entering test_backtrace 5 entering test_backtrace 4 entering test_backtrace 3 entering test_backtrace 2 entering test_backtrace 1 entering test_backtrace 0 Stack backtrace: ebp f0110f18 eip f01000a5 args 00000000 00000000 00000000 f010004e f0112308 kern/init.c:18: test_backtrace+101 ebp f0110f38 eip f010007a args 00000000 00000001 f0110f78 f010004e f0112308 kern/init.c:16: test_backtrace+58 ebp f0110f58 eip f010007a args 00000001 00000002 f0110f98 f010004e f0112308 kern/init.c:16: test_backtrace+58 ebp f0110f78 eip f010007a args 00000002 00000003 f0110fb8 f010004e f0112308 kern/init.c:16: test_backtrace+58 ebp f0110f98 eip f010007a args 00000003 00000004 00000000 f010004e f0112308 kern/init.c:16: test_backtrace+58 ebp f0110fb8 eip f010007a args 00000004 00000005 00000000 f010004e f0112308 kern/init.c:16: test_backtrace+58 ebp f0110fd8 eip f0100110 args 00000005 00001aac 00000640 00000000 00000000 kern/init.c:41: i386_init+102 ebp f0110ff8 eip f010003e args 00000003 00001003 00002003 00003003 00004003 debuginfo_eip error leaving test_backtrace 0 leaving test_backtrace 1 leaving test_backtrace 2 leaving test_backtrace 3 leaving test_backtrace 4 leaving test_backtrace 5 Welcome to the JOS kernel monitor! Type 'help' for a list of commands. K> ``` ### Make Grade Output ```txt $ make grade make clean make[1]: 进入目录“/home/fattiger/work/MIT-CS6.828” rm -rf obj .gdbinit jos.in qemu.log make[1]: 离开目录“/home/fattiger/work/MIT-CS6.828” ./grade-lab1 make[1]: 进入目录“/home/fattiger/work/MIT-CS6.828” + as kern/entry.S + cc kern/entrypgdir.c + cc kern/init.c + cc kern/console.c + cc kern/monitor.c + cc kern/printf.c + cc kern/kdebug.c + cc lib/printfmt.c + cc lib/readline.c + cc lib/string.c + ld obj/kern/kernel ld: warning: section `.bss' type changed to PROGBITS + as boot/boot.S + cc -Os boot/main.c + ld boot/boot boot block is 412 bytes (max 510) + mk obj/kern/kernel.img make[1]: 离开目录“/home/fattiger/work/MIT-CS6.828” running JOS: (1.1s) printf: OK backtrace count: OK backtrace arguments: OK backtrace symbols: OK backtrace lines: OK Score: 50/50 ``` 最后修改:2020 年 09 月 20 日 © 允许规范转载 赞 0 如果觉得我的文章对你有用,请随意赞赏