实验1正篇——引导PC
Posted yiye_01
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实验1正篇——引导PC相关的知识,希望对你有一定的参考价值。
讲了这么久,终于进入主题了。为了让读者更深入的理解整个系统,做一些必要的铺垫是必需的;这样不仅能做到有理有据,更能让人知其然,而且知其所以然。题目所谓“正”篇,就是要从正面去描述该实验的内容,当然是对其进行意译,同时根据其内容,结合源码,提出自己的理解,相当于为实验1的内容进行翻译的基础上,进行注解。实验1的主要内容是“引导PC”,主要讲述的是从PC一开机到运行内核的一个过程。实验内容大致分为如下几部分:1.PC引导(Bootstrap),2.内核引导程序(Boot loader),3.内核初识(kernel).当然这是以开机流程的时序为主线来分割内容。同时这也需要我们去理解Bootstrap,boot loader,boot等概念,这些都有引导的意思,但是它们是工作在不同的时段,而且任务也不一样。Bootstrap为从硬件开机引导整个硬件系统,存在于硬件可以执行代码的地方,它们主要有bios,uboot等,boot loader,存在于二级存储器上,用于加载内核的功能,比如:grub;boot,为内核的启动过程,存在于内核的代码中。
参考网址如下:https://pdos.csail.mit.edu/6.828/2012/labs/lab1/
0.实验前的准备
a)创建实验环境:
(1)编译交叉编译工具——i486-yiye-linux-gcc
(2)编译虚拟机——qemu-system-i386
(3)设置i486-yiye-linux-gcc与qemu-system-i386的路径到环境变量PATH中
b)下载实验1的源代码:
因为课程源码是用git来管理,所以需要用git的相关命令来下载源码:
(1)克隆代码到本地:
git clone http://pdos.csail.mit.edu/6.828/2012/jos.git lab1.demo
(2)浏览代码所在的目录详情
├── boot #实现mbr的代码目录
│ ├── boot.S #mbr进入点的代码
│ ├── main.c #被boot.S调用,加载内核的代码
│ ├── Makefrag#编译boot下的makefile的部分
│ └── sign.pl #修改编译生成的扇区,最后两个字节为0xaa55,生成mbr。
├── CODING
├── conf #编译的配置目录
│ ├── env.mk #编译环境的配置——交叉编译工具与虚拟机
│ └── lab.mk #实现课程的配置
├── GNUmakefile #编译整个系统的makefile,主要生成boot.img
├── grade-lab1
├── gradelib.py
├── handin-prep
├── inc #主要头文件,用于应用引用或者内核引用
│ ├── assert.h#c语言中的assert的实现
│ ├── COPYRIGHT
│ ├── elf.h #elf32的文件格式定义
│ ├── error.h #错误的处理
│ ├── kbdreg.h#按键的寄存器配置
│ ├── memlayout.h#内核管理内存映像的配置
│ ├── mmu.h #内存mmu的管理
│ ├── stab.h #elf文件的slab调试信息
│ ├── stdarg.h#支持c语言变参数的定义
│ ├── stdio.h #标准io的头文件实现
│ ├── string.h#字符串的头文件实现
│ ├── types.h #标准的类型定义
│ └── x86.h #x86的相关内敛函数的实现
├── kern #内核的相关代码实现,其中的头文件用于内核的引用
│ ├── console.c#终端的输入输出实现
│ ├── console.h#终端的头文件
│ ├── COPYRIGHT
│ ├── entrypgdir.c#保护模式下的页表进入点
│ ├── entry.S #内核的进入点代码
│ ├── init.c #内核的初始化代码
│ ├── kdebug.c#内核调试代码实现
│ ├── kdebug.h#调试的头文件
│ ├── kernel.ld#内核链接脚本
│ ├── Makefrag#内核编译的makefile
│ ├── monitor.c#内核模拟终端实现
│ ├── monitor.h#相应头文件
│ └── printf.c#终端标准输出实现
├── lib #内核引用库的代码实现
│ ├── printfmt.c#格式化标准输出
│ ├── readline.c#从终端读取行数据
│ └── string.c#字符串的处理实现
└── mergedep.pl
(3)修改编译环境-env.mk:
GCCPREFIX='i486-yiye-linux-'
QEMU=qemu-system-i386
(4)编译——make
(5)运行——make qemu或者手动运行之
1.PC引导(PC Bootstrap)
这部分的主要内容是x86的汇编语言,pc引导流程,以及用qemu模拟pc与qemu配合gdb调试整个流程。这个部分的学习,需要能够去理解相关的内容。
a)熟悉x86平台上的汇编实现——这里只需要提醒linux下使用的汇编为AT&T而Intel汇编可以由工具NASM来进行编译。详细的部分可以参考我之前的博客相关内容。如下为原文的参考文献:
介绍pc的汇编:https://pdos.csail.mit.edu/6.828/2012/readings/pcasm-book.pdf
c语言内嵌汇编的介绍:http://www.delorie.com/djgpp/doc/brennan/brennan_att_inline_djgpp.html
intel的开发手册:http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html
b)仿真x86——我们使用现成的工具qemu,熟悉它请参考之前的博客。
c)PC开机的映像:
关于低1M的内存空间详细使用可以参考《BIOS编程空间》的BIOS的数据资料。
因为早期的PC是在8088处理器的基础上构架的,所以目前系统为了兼容之前的PC系统,开始时处于8088的状态,访问内存空间只有1M。在内存范围0x00000-0xA0000标记为“低内存”(640KByte),这部分内存只能被编程作为RAM来使用。内存范围0xA0000-0xFFFFF的384KByte部分为显示区域与BIOS的程序部分,这部分空间被放在非易失性存储器。最重要的部分为BIOS代码部分——0xF0000-0xFFFFF。BIOS的基本功能是执行系统的初始化,比如:激活视频卡,查询系统存在的内存等。当系统初始化完了,BIOS将会加载二级存储空间(软盘,CD-ROM,硬盘,网络等)到内存中,从而引导操作系统。最后由操作系统去初始化所有的内存空间,管理所有的硬件。
d)BOIS的调试(The ROM BIOS)
PC一上电,处理器POST之后,它就会去0xffff0拿指令执行(刚好该地址在BIOS的地址空间中),然后执行BIOS的代码。为此我们用 make qemu-gdb进行调试。有如下输出:
qemu-system-i386 -hda obj/kern/kernel.img -serial mon:stdio -gdb tcp::26000 -D qemu.log -S
将编译出来的kernel.img作为硬盘1,添加串口stdio,调试端口为tcp::26000,停止处理器-S。更详细的内容查看qemu的使用。
当程序停止后,我们需要重新启动一个终端去启动gdb,然后远程去调试我们刚运行的模拟器。执行如下命令:
gdb -q -iex 'add-auto-load-safe-path .' .gdbinit
然后得到如下打印:
如上的内容被.gdbinit所指定。
通过查看当前寄存器的状态:
可以发现当前指令为[0xf000:0xfff0]内存0xffff0来实现,从而发现了BIOS的进入点。
通过反汇编 x/i $cs*16+$eip发现当前的指令是 ljmp $0xf000,$0xe05b,即调转到0xfe05b执行代码。然后调试步骤,需要耐心与其他硬件的知识了。通过si或者ni单条指令执行来调试;因为我们的研究重点是操作系统,而不是BIOS,所以只需要了解它的基本工作原理,当然也可以去详细的反汇编它们且一步步调试它。另外需要了解的目前的内存访问方式为physical address = 16 * segment + offset.。
比如:
(gdb) si
[f000:e05b] 0xfe05b:cmpl $0x0,%cs:0x6bb8
0x0000e05b in ?? ()
更详细的内容可以参考查阅Intel公司的x86资料<<64-ia-32-architectures-software-developer-manual-325462.pdf>>第三卷-第九章
根据表9-1.
初始化的状态:控制寄存器CR0=0x6000,0010(0110,0000,0000,0000,0000,0000,0001,0000)2.根据第三卷-2.5节知道系统初始状态处于
CR0.PE(bit0)=0,保护模式没有开启,所以系统处于8086的模式
CR0.PG(bit31)=0,分页机制被禁用,系统访问内存的方式为平坦模式,通过地址直接得到内存值。
CR0.CD(bit30)=1,处理器内部的缓存被禁用
CR0.NW(bit29)=1,处理器的回写功能被禁用
CR0.AM(bit18)=0,自动对齐检测关闭
CR0.WP(bit16)=0,写保护,被操作系统使用
CR0.NE(bit5)=1,检测协处理器X87的算术错误
CR0.AT(bit4)=0,扩展类型
CR0.EM/MP/TS(bit1,2,3)=0,跟协处理器的任务调度相关
2.内核引导程序(Boot Loader)
当BIOS执行完成之后,它会将硬盘的第一扇区MBR加载内存0x7C000中执行,而MBR的代码是由目录下boot实现的,boot下主要有boot.S与main.c。它们的主要功能如下:
a.boot.S将系统从16位模式切换到32位模式,然后调用bootmain(main.c的入口)
.globl start start: .code16 # Assemble for 16-bit mode 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: inb $0x64,%al # Wait for not busy testb $0x2,%al jnz seta20.1 movb $0xd1,%al # 0xd1 -> port 0x64 outb %al,$0x64 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 movl %cr0, %eax 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 .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. movl $start, %esp call bootmain # If bootmain returns (it shouldn't), loop. 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</span></em>
b.main.c用于加载内核代码到内存中,然后执行它。
<blockquote style="margin-right: 0px;" dir="ltr"><pre class="plain" name="code">void
bootmain(void)
struct Proghdr *ph, *eph;
// read 1st page off disk
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0);
// is this a valid ELF?
if (ELFHDR->e_magic != ELF_MAGIC)
goto bad;
// load each program segment (ignores ph flags)
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph++)
// p_pa is the load address of this segment (as well
// as the physical address)
readseg(ph->p_pa, ph->p_memsz, ph->p_offset);
// call the entry point from the ELF header
// note: does not return!
((void (*)(void)) (ELFHDR->e_entry))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
while (1)
/* do nothing */;
为了将如上的步骤完整的演示,可以通过直接反汇编出来boot.asm(objdump -d -S 即可)。
而要通过gdb去追踪该过程只需要在地址0x7c00设置断点——b *0x7C00,执行如下:
(gdb) b *0x7c00
Breakpoint 1 at 0x7c00
(gdb) c
Continuing.
[ 0:7c00] => 0x7c00:cli
当了解执行流程之后,我们还需要更具体的知道代码的实现,首先boot.S检查了A20,关于A20的详细介绍,请参考:http://www.win.tue.nl/~aeb/linux/kbd/A20.html
为了访问更多的内存,将A20使能。然后切换模式到32位保护模式,用于访问更多的内存空间。
根据Intel的官方文档第9.9.1节进入保护模式的步骤如下:
0)关闭中断
1)在程序中设置GDT(全局段描述符)
2)加载GDT到GDTR寄存器,LGDT
3)设置控制寄存器CR0.PE=1
4)ljmp到保护模式的地址执行——一定要执行它,用于清空之后的代码执行缓存。
关于段地址描述符:5.2节第3卷。
然后boot.S 调用bootmain.为了调用它,因为它是c语言实现的所以需要先创建c语言运行环境——设置段寄存器与设置堆栈。
当调用了bootmain之后,我们注意力就集中到了加载内核的步骤了。因为我们的内核是一个elf32格式的执行文件,而且目前系统没有任何加载器所以我们需要自己实现一个elf32的加载器,然后调转到内核进入点执行它。流程如下:
1)加载内核到内存中
为了加载内核到内存中,首先知道内核在哪儿,根据kern/Makefrag文件可以发现kernel镜像的创建方式如下:
$(OBJDIR)/kern/kernel.img: $(OBJDIR)/kern/kernel $(OBJDIR)/boot/boot
@echo + mk $@
$(V)dd if=/dev/zero of=$(OBJDIR)/kern/kernel.img~ count=10000 2>/dev/null
$(V)dd if=$(OBJDIR)/boot/boot of=$(OBJDIR)/kern/kernel.img~ conv=notrunc 2>/dev/null
$(V)dd if=$(OBJDIR)/kern/kernel of=$(OBJDIR)/kern/kernel.img~ seek=1 conv=notrunc 2>/dev/null
$(V)mv $(OBJDIR)/kern/kernel.img~ $(OBJDIR)/kern/kernel.img
kernel在kernel.img的第2个扇区,第一个扇区为mbr。
根据如上分析过程,令用bios的io指令读取硬盘的方法加载内核到地址0x10000。
2)解析内核elf32头——详细的结构被inc/elf.h所定义
当加载内核到内存中了,之后需要知道内核是什么。通过分析编译流程,发现内核其实生成在obj/kernel/kernel中,然后读取其头信息,知道内核为elf32文件(readelf -h obj/kern/kernel)。
根据elf32的数据结构定义,解析加载的kernel得到需要加载的段到对应的物理地址,可以通过readelf -l obj/kern/kernel读取如下:
如上的分析结果是通过读取elf文件所得的,而只需要将其以c语言代码的方式实现就可以了。
3)跳转到内核进入点执行
当加载完全之后,需要跳转到内核的入口地址来进入内核执行。通过如上的分析可以看出内核代码进入点为 物理地址0x10000c。
对于内核的分析,还可以通过编译时所用的链接脚本来分析与理解:
<em><span style="font-size:12px;">/* Simple linker script for the JOS kernel. See the GNU ld 'info' manual ("info ld") to learn the syntax. */ OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")——输出为elf32-i386的格式 OUTPUT_ARCH(i386)——构架为i386 ENTRY(_start)——进入点为_start,所以可通过nm obj/kernel/kernel|grep _start查看内核进入点 SECTIONS /* Link the kernel at this address: "." means the current address */ . = 0xF0100000;——内核地址段开始的虚拟地址 /* AT(...) gives the load address of this section, which tells the boot loader where to load the kernel in physical memory */ .text : AT(0x100000) ——将.text映射到0x100000的地方 *(.text .stub .text.* .gnu.linkonce.t.*)——.text段的内容 PROVIDE(etext = .); /* Define the 'etext' symbol to this value */——设置链接变量,被程序读取得到代码段的长度 .rodata : *(.rodata .rodata.* .gnu.linkonce.r.*) /* Include debugging information in kernel memory */ .stab : PROVIDE(__STAB_BEGIN__ = .); *(.stab); PROVIDE(__STAB_END__ = .); BYTE(0) /* Force the linker to allocate space for this section */ .stabstr : PROVIDE(__STABSTR_BEGIN__ = .); *(.stabstr); PROVIDE(__STABSTR_END__ = .); BYTE(0) /* Force the linker to allocate space for this section */ /* Adjust the address for the data segment to the next page */ . = ALIGN(0x1000); /* The data segment */ .data : ——数据段的定义 *(.data) PROVIDE(edata = .); .bss : ——数据段未初始化的数据段 *(.bss) PROVIDE(end = .); /DISCARD/ : *(.eh_frame .note.GNU-stack) </span></em>
关于elf的详细介绍可以参考如下:
https://pdos.csail.mit.edu/6.828/2012/readings/elf.pdf
3.内核初识
因为我们此次实验关心的是从开机到内核运行的流程,所以这次实现的内核只是一个简单的命令终端,而不具有任何实际内核的功能。而它可以作为uboot与grub的功能实现,因为它们就实现这样的终端功能。
当内核被启动时,依然是以汇编语言(kern/entry.S)开始,因为目前还不具备运行c语言的环境,所以需要汇编语言去创建之。
<em><span style="font-size:12px;">.globl _start _start = RELOC(entry) .globl entry entry: movw $0x1234,0x472 # warm boot # We haven't set up virtual memory yet, so we're running from # the physical address the boot loader loaded the kernel at: 1MB # (plus a few bytes). However, the C code is linked to run at # KERNBASE+1MB. Hence, we set up a trivial page directory that # translates virtual addresses [KERNBASE, KERNBASE+4MB) to # physical addresses [0, 4MB). This 4MB region will be # sufficient until we set up our real page table in mem_init # in lab 2. # Load the physical address of entry_pgdir into cr3. entry_pgdir # is defined in entrypgdir.c. movl $(RELOC(entry_pgdir)), %eax movl %eax, %cr3 # Turn on paging. movl %cr0, %eax orl $(CR0_PE|CR0_PG|CR0_WP), %eax movl %eax, %cr0 # Now paging is enabled, but we're still running at a low EIP # (why is this okay?).
以上是关于实验1正篇——引导PC的主要内容,如果未能解决你的问题,请参考以下文章