【实现】bootloader加载并运行ucore
了解完proj2/3的组成与编译,并大致理解上述两个背景知识后,我们就可以分析bootloader加载并运行ucore操作系统的工作流程。
硬盘数据是储存到硬盘扇区中,一个扇区大小为512字节。读一个扇区的流程可参看bootmain.c中的readsect函数实现。大致如下:
- 读I/O地址0x1f7,等待磁盘准备好;
- 写I/O地址0x1f2~0x1f5,0x1f7,发出读取第offseet个扇区处的磁盘数据的命令;
- 读I/O地址0x1f7,等待磁盘准备好;
- 连续读I/O地址0x1f0,把磁盘扇区数据读到指定内存。
这个函数是被bootloader用于读取硬盘上的ucore操作系统。bootloader为了读取硬盘上的ucore操作系统,将调用bootmain函数首先读取了位于主引导扇区的后的连续8个扇区(可参见bootmain函数中的第一条语句),并把数据放到0x10000处(可回顾一下2.7.1中描述链接bin/kernel的过程),并按照数据结构elfhdr来解析这块4KB大小的数据;如果其e_magic数据域不等于ELF_MAGIC(即0x464C457F),则表示这个不是标准的ELF格式的文件;如果等于ELF_MAGIC,则继续解析,并根据其e_phnum数据域的值来读取多个program header,并根据program header的信息,了解到ucore中各个segment的起始位置和大小,然后把放在硬盘上的相关segment读入到内存中。
【实验】分析kernel并在bootloader中显示kernel的segment信息
- 在proj3目录下执行命令make,则会在bin目录下生成kernel,即ELF执行格式文件的操作系统ucore;
在proj3目录下执行命令 readelf -h bin/kernel,可得到有关elf header的如下信息
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x100000
Start of program headers: 52 (bytes into file)
Start of section headers: 19872 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 3
Size of section headers: 40 (bytes)
Number of section headers: 17
Section header string table index: 14
从中,我们可以看到kernel的入口点在0x100000,program header相对文件的偏移位置在52,elf header的大小为52字节,program header的大小为32字节。
在proj3目录下执行命令 readelf -l bin/kernel,可得到有关program header的如下信息
Elf file type is EXEC (Executable file)
Entry point 0x100000
There are 3 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x001000 0x00100000 0x00100000 0x01038 0x01038 R E 0x1000
LOAD 0x002038 0x00102038 0x00102038 0x00004 0x00004 RW 0x1000
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Section to Segment mapping:
Segment Sections...
00 .text .rodata
01 .data
02
从中,我们可以看到kernel的入口点在0x100000,代码段位于0x100000,大小为0x1038;数据段位于0x102038,大小为0x04。
【实验】用gdb调试bootloader,并在gdb中显示kernel的segment信息
我们还可通过用gdb调试bootloader进行验证,具体步骤如下:
- 开两个窗口;在一个窗口中,在proj3目录下执行命令make;
- 在proj3目录下执行 “qemu -hda bin/ucore.img -S –s”,这时会启动一个qemu窗口界面,处于暂停状态,等待gdb链接;
- 在另外一个窗口中,在proj3目录下执行命令 gdb obj/bootblock.o;
在gdb的提示符下执行如下命令,会有一定的输出:
(gdb) target remote :1234 #与qemu建立远程链接
(gdb) break bootmain.c:100 #在bootmain.c的第100行设置一个断点
(gdb) continue #让qemu继续执行
这时qemu会继续执行,但执行到bootmain.c的第100行时会暂停,等待gdb的控制。这时可以在gdb中继续输入如下命令来参考kernel的信息:
(gdb) p /x *(struct elfhdr *)0x10000 #按struct elfhdr结构显示0x10000处内容
$7 = {e_magic = 0x464c457f, e_elf = {0x1, 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, e_type = 0x2, e_machine = 0x3, e_version = 0x1, e_entry = 0x100000, e_phoff = 0x34, e_shoff = 0x4550, e_flags = 0x0, e_ehsize = 0x34, e_phentsize = 0x20, e_phnum = 0x3, e_shentsize = 0x28, e_shnum = 0x11, e_shstrndx = 0xe}
查看bootmain函数,可以知道,此时在0x10000处已经读入了kernel的ELF头信息,有三个program header 表(e_phnum值),继续在gdb中敲入命令,可以得到更多信息:
(gdb) next #执行下一条指令
(gdb) p /x *ph #获得text段的program header表信息
$5 = {p_type = 0x1, p_offset = 0x1000, p_va = 0x100000, p_pa = 0x100000, p_filesz = 0x1038, p_memsz = 0x1038, p_flags = 0x5, p_align = 0x1000}
(gdb) next #执行下一条指令
(gdb) next #执行下一条指令
(gdb) p /x *ph #获得data段的program header表信息
$6 = {p_type = 0x1, p_offset = 0x2038, p_va = 0x102038, p_pa = 0x102038, p_filesz = 0x4, p_memsz = 0x4, p_flags = 0x6, p_align = 0x1000}
对照readelf命令输出的信息,可以发现bootloader正确读出了text段和data段的program header表信息,并根据这些信息调用如下函数
-->readseg(ph->p_va, ph->p_memsz, ph->p_offset);
-->readsect((uint8_t *)va, offset);
把这两个段的内容读入到正确的线性内存地址中。然后再根据e_entry = 0x100000,跳转到0x100000处去执行,这其实就是把处理器控制权转移给了ucore了。