MIT6.828 | Lab 1: Booting a PC - Part 2: The Boot Loader

软盘和硬盘用于PC的软盘和硬盘分为512个字节区域,称为扇区。 扇区是磁盘的最小传输粒度:每个读取或写入操作必须是一个或多个扇区,并在扇区边界上对齐。 如果磁盘是可引导的,则第一个扇区称为引导扇区,因为这是引导加载程序代码所在的位置。 当BIOS找到可引导的软盘或硬盘时,它将512字节的引导扇区加载到物理地址0x7c00到0x7dff的内存中,然后使用jmp指令将CS:IP设置为0000:7c00,将控制权传递给引导装载机。 与BIOS加载地址一样,这些地址相当随意 - 但它们是针对PC修复和标准化的。

The ability to boot from a CD-ROM came much later during the evolution of the PC, and as a result the PC architects took the opportunity to rethink the boot process slightly. As a result, the way a modern BIOS boots from a CD-ROM is a bit more complicated (and more powerful). CD-ROMs use a sector size of 2048 bytes instead of 512, and the BIOS can load a much larger boot image from the disk into memory (not just one sector) before transferring control to it. For more information, see the "El Torito" Bootable CD-ROM Format Specification.

MIT 6.828将使用传统的硬盘启动机制,即启动加载程序为512字节。

引导加载程序包含一个汇编语言源文件boot / boot.S和一个C源文件boot / main.c

将处理器由 real mode 切换为 protected mode, 使软件能够访问所有的 1MB 以上的物理地址空间。

Protected mode is described briefly in sections 1.2.7 and 1.2.8 of PC Assembly Language, and in great detail in the Intel architecture manuals.

Bootloader通过x86特殊I / O指令直接访问IDE磁盘设备寄存器,从硬盘读取内核。

what the particular I/O instructions here mean, check out the "IDE hard drive controller" section on the 6.828 reference page.

Question

Be able to answer the following questions:

At what point does the processor start executing 32-bit code? What exactly causes the switch from 16- to 32-bit mode?

in boot / boot.S

  # 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

What is the last instruction of the boot loader executed, and what is the first instruction of the kernel it just loaded?

in boot / main.c

1
2
3
	// call the entry point from the ELF header
	// note: does not return!
	((void (*)(void)) (ELFHDR->e_entry))();

in obj/boot/boot.asm

	((void (*)(void)) (ELFHDR->e_entry))();
    7d6b:	ff 15 18 00 01 00    	call   *0x10018

可以看到跳转至了 0x10018所存储的值,设置断点至*0x7d6b

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(gdb) b *0x7d6b 
Breakpoint 1 at 0x7d6b 
(gdb) c 
Continuing. 
The target architecture is assumed to be i386 
=> 0x7d6b:      call   *0x10018

Breakpoint 1, 0x00007d6b in ?? ()
(gdb) si 
=> 0x10000c:    movw   $0x1234,0x472 
0x0010000c in ?? ()

可以看到接下来运行的地址为 0x001000c, 正好是 kernel 的入口地址。

Where is the first instruction of the kernel?

*0x001000c

How does the boot loader decide how many sectors it must read in order to fetch the entire kernel from disk? Where does it find this information?

in boot / main.c

1
2
3
4
5
6
7
	// 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);

ph记录了初始加载位置,eph记录了加载终点,共同决定了加载的扇区个数

1. Loading the Kernel 加载内核

Review of basis of C programming

Pointers

ELF binary: Executable and Linkable Format ( the ELF specification )

一些相关的 ELF headers (C definition in inc/elf.h):

  • .text: 程序的可执行指令.
  • .rodata: 只读数据,如C编译器生成的ASCII字符串常量。
  • .data: 程序的初始数据,例如全局变量

Examine the full list of the names, sizes, and link addresses of all the sections in the kernel executable by typing: objdump -h obj/kern/kernel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
obj/kern/kernel:     file format elf32-i386 

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         000019e9  f0100000  00100000  00001000  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .rodata       000006c0  f0101a00  00101a00  00002a00  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .stab         00003b95  f01020c0  001020c0  000030c0  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA 
  3 .stabstr      00001948  f0105c55  00105c55  00006c55  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .data         00009300  f0108000  00108000  00009000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  5 .got          00000008  f0111300  00111300  00012300  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  6 .got.plt      0000000c  f0111308  00111308  00012308  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  7 .data.rel.local 00001000  f0112000  00112000  00013000  2**12
                  CONTENTS, ALLOC, LOAD, DATA
  8 .data.rel.ro.local 00000044  f0113000  00113000  00014000  2**2
                  CONTENTS, ALLOC, LOAD, DATA 
  9 .bss          00000648  f0113060  00113060  00014060  2**5
                  CONTENTS, ALLOC, LOAD, DATA
 10 .comment      0000002b  00000000  00000000  000146a8  2**0
                  CONTENTS, READONLY

VMA (link address): 该部分期望执行的内存地址

LMA (load address): 该部分在内存中的加载地址 (for .text)

LMA的定义位置:

We set the link address by passing -Ttext 0x7C00 to the linker in boot/Makefrag, so the linker will produce the correct memory addresses in the generated code.

1
2
3
4
5
6
7
8
# original
The target architecture is assumed to be i8086
[f000:fff0]    0xffff0: ljmp   $0xf000,$0xe05b
0x0000fff0 in ?? ()
+ symbol-file obj/kern/kernel
(gdb) si 
[f000:e05b]    0xfe05b: cmpl   $0x0,%cs:0x6ac8 
0x0000e05b in ?? ()

测试改变其定义位置,改变为 $(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x8C00 -o $@.out $^,

make clean && make, run objdump -h obj/boot/boot.out

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
obj/boot/boot.out:     file format elf32-i386 

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000186  00008c00  00008c00  00000074  2**2
                  CONTENTS, ALLOC, LOAD, CODE
  1 .eh_frame     000000a8  00008d88  00008d88  000001fc  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .stab         0000087c  00000000  00000000  000002a4  2**2
                  CONTENTS, READONLY, DEBUGGING
  3 .stabstr      00000925  00000000  00000000  00000b20  2**0
                  CONTENTS, READONLY, DEBUGGING 
  4 .comment      0000002b  00000000  00000000  00001445  2**0
                  CONTENTS, READONLY

但是使用gdb似乎又正常,不过在退出的时候有一堆报错:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
***
*** Now run 'make gdb'.
***
qemu-system-i386 -nographic -drive file=obj/kern/kernel.img,index=0,media=disk,format=raw -serial mon:stdio -gdb tcp::26000 -D qemu.log  -S
EAX=00000011 EBX=00000000 ECX=00000000 EDX=00000080
ESI=00000000 EDI=00000000 EBP=00000000 ESP=00006f20
EIP=00007c2d EFL=00000006 [-----P-] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0000 00000000 0000ffff 00009300 DPL=0 DS16 [-WA]
CS =0000 00000000 0000ffff 00009b00 DPL=0 CS16 [-RA]
SS =0000 00000000 0000ffff 00009300 DPL=0 DS16 [-WA]
DS =0000 00000000 0000ffff 00009300 DPL=0 DS16 [-WA]
FS =0000 00000000 0000ffff 00009300 DPL=0 DS16 [-WA]
GS =0000 00000000 0000ffff 00009300 DPL=0 DS16 [-WA]
LDT=0000 00000000 0000ffff 00008200 DPL=0 LDT
TR =0000 00000000 0000ffff 00008b00 DPL=0 TSS32-busy
GDT=     00000000 00000000
IDT=     00000000 000003ff
CR0=00000011 CR2=00000000 CR3=00000000 CR4=00000000
DR0=00000000 DR1=00000000 DR2=00000000 DR3=00000000
DR6=ffff0ff0 DR7=00000400
EFER=0000000000000000
Triple fault.  Halting for inspection via QEMU monitor.

而之前是

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
***
*** Now run 'make gdb'.
***
qemu-system-i386 -nographic -drive file=obj/kern/kernel.img,index=0,media=disk,format=raw -serial mon:stdio -gdb tcp::26000 -D qemu.log  -S
6828 decimal is XXX 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
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>

测试内存:

examine memory using GDB's x command. The GDB manual has full details.

the command x/<N>x <ADDR> prints N words of memory at ADDR. (Note that both 'x's in the command are lowercase.) Warning: The size of a word is not a universal standard. In GNU assembly, a word is two bytes (the 'w' in xorw, which stands for word, means 2 bytes).

前面已经知道kernel的入口地址为 0x001000cbootloader 的地址为 0x7c00, 这里做一个简单的测试:

  • bootloader 载入 kernel 前查看 0x001000c 的内存信息,可以发现全是初始状态的0
  • question2可以知道加载完 kernel 后会跳转至 kernel 入口,0x001000c, 此时会发现内存信息已经初始化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
The target architecture is assumed to be i8086
[f000:fff0]    0xffff0: ljmp   $0xf000,$0xe05b 
0x0000fff0 in ?? ()
+ symbol-file obj/kern/kernel
(gdb) b *0x7c00 
Breakpoint 1 at 0x7c00 
(gdb) c 
Continuing. 
[   0:7c00] => 0x7c00:  cli     

Breakpoint 1, 0x00007c00 in ?? ()
(gdb) x /8wx 0x00100000 
0x100000:       0x00000000      0x00000000      0x00000000      0x00000000 
0x100010:       0x00000000      0x00000000      0x00000000      0x00000000
(gdb) b*0x10000c 
Breakpoint 2 at 0x10000c 
(gdb) c 
Continuing. 
The target architecture is assumed to be i386 
=> 0x10000c:    movw   $0x1234,0x472

Breakpoint 2, 0x0010000c in ?? ()
(gdb) x /8wx 0x00100000 
0x100000:       0x1badb002      0x00000000      0xe4524ffe      0x7205c766 
0x100010:       0x34000004      0x2000b812      0x220f0011      0xc0200fd8

同时在 obj/kern/kernel.asm 文件最开始中可以发现:

.globl entry
entry:
	movw	$0x1234,0x472			# warm boot
f0100000:	02 b0 ad 1b 00 00    	add    0x1bad(%eax),%dh
f0100006:	00 00                	add    %al,(%eax)
f0100008:	fe 4f 52             	decb   0x52(%edi)
f010000b:	e4                   	.byte 0xe4

其汇编指令恰好对应 kernel 入口内存中的数据

updatedupdated2023-01-302023-01-30
点击刷新