经过bootasm.S中汇编指令执行,处理器为操作系统内核加载准备一系列设置,设置寄存器值,开A20,初始化GDT,进入保护模式,设置段寄存器,建立堆栈,之后便执行从硬盘加载操作系统内核。
https://blog.csdn.net/u014106644/article/details/96965473
首先操作系统内核代码被编译链接称为ELF格式的可执行文件,然后由bootloader加载到内存中,并且跳转到内核程序入口。
OS ELF格式文件
有关ELF文件格式分析可以参考
目标文件和ELF格式详解 https://www.jianshu.com/p/132412ca73ce
ELF文件解析和加载(附代码) https://blog.csdn.net/muaxi8/article/details/79627859
ELF文件格式解析 https://blog.csdn.net/dddxxxx/article/details/80347610
Linux生成的目标文件是标准的ELF文件格式,其基本结构如下所示
ELF文件格式提供了两种视图,分别是链接视图和执行视图。
链接视图是以节(section)为单位,执行视图是以段(segment)为单位。链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。上图左侧的视角是从链接来看的,右侧的视角是执行来看的。总个文件可以分为四个部分:
- ELF header: 描述整个文件的组织。
- Program Header Table: 描述文件中的各种segments,用来告诉系统如何创建进程映像的。
- sections 或者 segments:segments是从运行的角度来描述elf文件,sections是从链接的角度来描述elf文件,也就是说,在链接阶段,我们可以忽略program header table来处理此文件,在运行阶段可以忽略section header table来处理此程序(所以很多加固手段删除了section header table)。从图中我们也可以看出,segments与sections是包含的关系,一个segment包含若干个section。
- Section Header Table: 包含了文件各个segction的属性信息。
对应文件头数据结构如下
/* file header */
struct elfhdr {
uint32_t e_magic; // must equal ELF_MAGIC
uint8_t e_elf[12];
uint16_t e_type; // 1=relocatable, 2=executable, 3=shared object, 4=core image
uint16_t e_machine; // 3=x86, 4=68K, etc.
uint32_t e_version; // file version, always 1
uint32_t e_entry; // entry point if executable
uint32_t e_phoff; // file position of program header or 0
uint32_t e_shoff; // file position of section header or 0
uint32_t e_flags; // architecture-specific flags, usually 0
uint16_t e_ehsize; // size of this elf header
uint16_t e_phentsize; // size of an entry in program header
uint16_t e_phnum; // number of entries in program header or 0
uint16_t e_shentsize; // size of an entry in section header
uint16_t e_shnum; // number of entries in section header or 0
uint16_t e_shstrndx; // section number that contains section name strings
};
e_entry表示可执行程序的入口地址,即系统加载该ELF后,应该从该地址开始运行;
e_phoff程序头的地址
e_phnum程序头的数量
在ELF中把权限相同、又连在一起的段(section)叫做segment,操作系统正是按照“segment”来映射可执行文件的。
描述这些“segment”的结构叫做程序头,它描述了elf文件该如何被操作系统映射到内存空间中。
程序头数据结构如下
/* program section header */
struct proghdr {
uint32_t p_type; // loadable code or data, dynamic linking info,etc.
uint32_t p_offset; // file offset of segment
uint32_t p_va; // virtual address to map segment
uint32_t p_pa; // physical address, not used
uint32_t p_filesz; // size of segment in file
uint32_t p_memsz; // size of segment in memory (bigger if contains bss)
uint32_t p_flags; // read/write/execute bits
uint32_t p_align; // required alignment, invariably hardware page size
};
p_va该段映射到内存中的段虚拟地址;
p_memsz表示该段在内存中的大小;
p_offset该段在可执行文件中的偏移;
bootloader加载ELF格式OS过程
核心代码如下:
/* bootmain - the entry of bootloader */
void
bootmain(void) {
// read the 1st page off disk 首先读取ELF的头部
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
// is this a valid ELF?
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
struct proghdr *ph, *eph;
// load each program segment (ignores ph flags)
// ELF头部有描述ELF文件应加载到内存什么位置的描述表,
// 先将描述表的头地址存在ph
//uint32_t e_phoff; // file position of program header or 0
//uint16_t e_phnum; // number of entries in program header or 0
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
// 按照描述表将ELF文件中数据载入内存
//uint32_t p_va; // virtual address to map segment
//uint32_t p_memsz; // size of segment in memory (bigger if contains bss)
//uint32_t p_offset; // file offset of segment
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// ELF文件0x1000位置后面的0xd1ec比特被载入内存0x00100000
// ELF文件0xf000位置后面的0x1d20比特被载入内存0x0010e000
// call the entry point from the ELF header
// note: does not return
// 根据ELF头部储存的入口信息,找到内核的入口
//uint32_t e_entry; // entry point if executable
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
/* do nothing */
while (1);
}
磁盘第一个扇区存储bootloader代码,从第二个扇区往后存储操作系统内核镜像,镜像使用ELF格式来存储。
readsect函数
从设备的第secno扇区读取数据到dst位置
/* readsect - read a single sector at @secno into @dst */
static void
readsect(void *dst, uint32_t secno) {
// wait for disk to be ready
waitdisk();
outb(0x1F2, 1); // count = 1 设置读取扇区的数目为1
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0); // 上面四条指令联合制定了扇区号
// 在这4个字节线联合构成的32位参数中
// 29-31位强制设为1
// 28位(=0)表示访问"Disk 0"
// 0-27位是28位的偏移量
outb(0x1F7, 0x20); // cmd 0x20 - read sectors 0x20命令,读取扇区
// wait for disk to be ready
waitdisk();
// read a sector
insl(0x1F0, dst, SECTSIZE / 4); //读取到dst位置,幻数4因为这里以DW为单位
}
readseg函数
readseg(uintptr_t va, uint32_t count, uint32_t offset)
从内核镜像offset偏移位置处读取count字节到虚拟地址va位置处。
/* *
* readseg - read @count bytes at @offset from kernel into virtual address @va,
* might copy more than asked.
* */
static void
readseg(uintptr_t va, uint32_t count, uint32_t offset) {
//读取完数据之后,地址指针位置
uintptr_t end_va = va + count;
// round down to sector boundary
va -= offset % SECTSIZE;
// translate from bytes to sectors; kernel starts at sector 1
加1因为0扇区被引导占用 ELF文件从1扇区开始 计算offset位于磁盘扇区号
uint32_t secno = (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.
//每次读取一个扇区内容,直到大于或者等于count为止
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
对于bootmain主函数,首先读取ELF的头部
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
检测ELF文件魔术是否正确
// is this a valid ELF?
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
然后找到程序头描述表的地址以及程序头数量
struct proghdr *ph, *eph;
// load each program segment (ignores ph flags)
// ELF头部有描述ELF文件应加载到内存什么位置的描述表,
// 先将描述表的头地址存在ph
//uint32_t e_phoff; // file position of program header or 0
//uint16_t e_phnum; // number of entries in program header or 0
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
根据描述符表,将每个段从磁盘加载到内存相应位置
// 按照描述表将ELF文件中数据载入内存
//uint32_t p_va; // virtual address to map segment
//uint32_t p_memsz; // size of segment in memory (bigger if contains bss)
//uint32_t p_offset; // file offset of segment
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
跳转到程序入口地址处
// 根据ELF头部储存的入口信息,找到内核的入口
//uint32_t e_entry; // entry point if executable
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
至此已经完成,内核从硬盘加载到内存,操作系统正式启动。