/* bootmain - the entry of bootloader */
void
bootmain(void) {
// read the 1st page off disk
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)
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
// call the entry point from the ELF header
// note: does not return
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
bad:
outw(0x8A00, 0x8A00);
outw(0x8A00, 0x8E00);
/* do nothing */
while (1);
}
bootmain.c中的bootmain函数如上,接下来我们来逐步分析一下这个函数。但是里面更多的和硬件相关的东西,我也不是特别清楚,所以在深入层次上会做一个平衡。
// read the 1st page off disk
readseg((uintptr_t)ELFHDR, SECTSIZE * 8, 0);
而在bootmain.c的文件开头定义里这两个常量:
#define SECTSIZE 512
#define ELFHDR ((struct elfhdr *)0x10000) // scratch space
意思就是从磁盘读8个扇区的大小到ELFHDR这个地址上,偏移量为0,接下来看看readseg函数。
/* *
* 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
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.
for (; va < end_va; va += SECTSIZE, secno ++) {
readsect((void *)va, secno);
}
}
函数会从内核的offset位置(因为内核起始位置为一个扇区的起始位置,所以offset % 512 也就是 offset这个位置相对于这个扇区的offset)读取count个字节,放置到va所对应的虚拟内存位置。函数传进来的offset和(offset % sectsize)意思不一样,前者是相对于1号扇区开始位置的偏移量,后者是相对于你要读的那个扇区的偏移量。
va -= offset % SECTSIZE; 这条语句表明我们读字节必须从扇区头部开始读;所以将va减去offset,最后的效果是会多读一点进来(va前面的),但是没有关系。 secno则是调用readsect函数必需的扇区号(从1开始,可以认为0号扇区存放BIOS和bootloader,不知道这么说对不对)
接下来看readsect函数
/* 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
outb(0x1F3, secno & 0xFF);
outb(0x1F4, (secno >> 8) & 0xFF);
outb(0x1F5, (secno >> 16) & 0xFF);
outb(0x1F6, ((secno >> 24) & 0xF) | 0xE0);
outb(0x1F7, 0x20); // cmd 0x20 - read sectors
// wait for disk to be ready
waitdisk();
// read a sector
insl(0x1F0, dst, SECTSIZE / 4);
}
先看waitdisk
/* waitdisk - wait for disk ready */
static void
waitdisk(void) {
while ((inb(0x1F7) & 0xC0) != 0x40)
/* do nothing */;
}
这个函数用了GNU 扩展内联汇编
static inline uint8_t
inb(uint16_t port) {
uint8_t data;
asm volatile ("inb %1, %0" : "=a" (data) : "d" (port));
return data;
}
意思是从指定端口读取一个字节的数据并返回
inb和outb不多说我们看看insl
static inline void
insl(uint32_t port, void *addr, int cnt) {
asm volatile (
"cld;"
"repne; insl;"
: "=D" (addr), "=c" (cnt)
: "d" (port), "0" (addr), "1" (cnt)
: "memory", "cc");
}
首先是下面的约束条件,指定了addr 放到%edi, cnt 放到%ecx,port放到%edx,注意addr,cnt都是既是输入型参数也是输出型参数。 cld指令是和repne指令配合使用的,repne指令是连续执行下一条指令,直到%ecx为0.cld指令初始化%ecx减小的步长为1.
第三个约束clobber说明,在指令执行过程中,内存和cc(这到底是啥?也许是条件码吧)可能会被改变。
综合起来就是每次读入4个字节的数据到addr,读入cnt次。所以read a sector这行注释的下面次数是扇区的字节数除以4
而中间一串outb的就是在向指定端口写读磁盘的指令,这个属于太细节的硬件问题,略过。
当我们完成了读入kernel的ELF文件头之后,就要来解析这个ELF文件头。
// is this a valid ELF?
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad;
}
这里就要提一嘴ELFHDR
#ifndef __LIBS_ELF_H__
#define __LIBS_ELF_H__
#include <defs.h>
#define ELF_MAGIC 0x464C457FU // "\x7FELF" in little endian 小端机
/* 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
};
/* 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
};
#endif /* !__LIBS_ELF_H__ */
如果ELF文件头的指定位置不是这个ELF_MAGIC的话那么我们不认为这是一个有效的文件头
struct proghdr *ph, *eph;
// load each program segment (ignores ph flags)
ph = (struct proghdr *)((uintptr_t)ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
for (; ph < eph; ph ++) {
readseg(ph->p_va & 0xFFFFFF, ph->p_memsz, ph->p_offset);
}
接下来的代码让我们主要关心上面结构体的定义中,我标了中文注释的部分。
主要意思就是将整个ELF文件中存放在不同位置的代码段都读入内存,需要的信息就是第一个struct program header*的位置,和这个数组的长度。然后根据progeam header将可执行代码读入到内存。
// call the entry point from the ELF header
// note: does not return
((void (*)(void))(ELFHDR->e_entry & 0xFFFFFF))();
最后调用了kernel的程序入口点,这个写得有点绕,我们拆开来看
();是函数调用,那么前面得东西就必然是个函数指针,我们知道目前ELFHDR->e_entry & 0xFFFFFF还只是个数字,那么我们把它强制类型转换, (void (*)(void))就是对一个函数指针类型的描述,这个指针描述的函数时返回值为void,参数列表也为void的函数,星号后面没跟名字就是匿名的函数指针变量。最后调用它,启动操作系统内核。