实验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)加载GDTGDTR寄存器,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

     kernelkernel.img的第2个扇区,第一个扇区为mbr

     根据如上分析过程,令用biosio指令读取硬盘的方法加载内核到地址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.内核初识

因为我们此次实验关心的是从开机到内核运行的流程,所以这次实现的内核只是一个简单的命令终端,而不具有任何实际内核的功能。而它可以作为ubootgrub的功能实现,因为它们就实现这样的终端功能。

当内核被启动时,依然是以汇编语言(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的主要内容,如果未能解决你的问题,请参考以下文章

实验3正篇——用户进程

实验3正篇——用户进程

实验2正篇——内存管理

实验2正篇——内存管理

Lab1: Booting a PC

《MIT 6.828 Lab1: Booting a PC》实验报告