详解bootloader源码
理解bootloader源码
boot/boot.S
和 boot/main.c
共同组成了 bootloader,BIOS运行完后,CPU的控制权就转移到了 boot.S
文件上,接下来对其进行分析。
boot.S
.global start
start:
.code16 # 16-bit汇编模式
cli # 禁止中断
cld # 串处理操作指针由低内存地址向高内存地址移动
# 设定重要的段寄存器(DS, ES, SS)
xorw %ax, %ax # 寄存器ax值通过异或置为0
movw %ax, %ds # data segment
movw %ax, %es # extra segment
movw %ax, %ss # stack segment,如上三条将此三个段寄存器置0
# Enable A20:
# 为了保证和之前的20位地址线兼容,一开始工作在实模式,高于A20(含)的地址线都是置为0,接下来取消置零
seta20.1
inb $0x64, %al
testb $0x2, %al
jnz seta20.1
# 将端口0x64的设备寄存器的值输入到寄存器al,当al寄存器的值bit1=1(说明控制器繁忙,CPU不能传数据),则一直重复
movb $0xd1, %al
outb %al, $0x64 # 当0x64端口准备好后,将0xd1 写入 0x64 端口
# 查阅 http://bochs.sourceforge.net/techspec/PORTS.LST 的 0064 可以发现
# 0xd1 代表下一个写入到 0x60 端口的字节会被写入到键盘控制器804x的输出端口
seta20.2:
inb $0x64, %al
testb $0x2, %al
jnz seta20.2
# 再一次等待 0x64 端口空闲
movb $0xdf, %al
outb %al, $0x60 # 将 0xdf 写入到 0x60端口
# 根据上面所述,0xdf会写到804x的输出端口即0x64,
# 0xdf 经查阅,其功能为使能地址线A20,即置为1,代表处理器即将进入保护模式
# 不太理解这里为什么需要 lgdt 这条指令,求解答
lgdt gdtdsec # gdtdsec 标识符见下一个框
movl %cr0, %eax
orl $CR0_PE_ON, %eax # $CR0_PE_ON=0x1
movl %eax, %cr0 # 通过or操作将cr0的bit0置为1,保护模式启动
# gdtdsec是一个标识符,标识着一个内存地址,从这个地址开始的6-byte(48-bit)存放着GDT表信息
# GDT表信息由48位表示:48 = 32H(内存起始地址):16L(表长度)
# lgdt gdtdsec: 就是将gdtdsec标识的内存地址开始6-byte(48-bit)数据加载到GDTR(全局段描述符表寄存器48-bit)
# Bootstrap GDT
.p2align 2 # 强制 4-byte 对齐
gdt: # 此标识符标识从这里开始就是GDT表了,一共有三个表项,每个表项 8-byte
SEG_NULL # null seg
SEG(SAT_X|SAT_R, 0x0, 0xffffffff) # code seg: 读和执行权限
SEG(SAT_W, 0x0, 0xffffffff) # data seg: 写权限
# SEG程序定义于mmu.h,三个参数分别代表:段访问权限、段起始地址、段大小
# xv6 没有采用分段机制,所以代码段和数据段共享0xffffffff(4GB)大小的空间
gdtdsec:
.word 0x17 # sizeof(gdt) - 1 = 3(表项) * 8(byte) - 1 = 23 = 0x17
.long gdt # address gdt,即表的起始地址
ljmp $PROT_MODE_CSEG, $protcseg # 跳转指令,在保护模式下为 [ljmp 段选择子, 段内偏移]
.code32 # 32-bit汇编模式
protcseg:
# 设定保护模式的数据段寄存器
movw $PROT_MODE_DSEG, %ax # 将数据段选择子存到ax寄存器,然后写到各种段寄存器
movw %ax, %ds
movw %ax, %es
movw %ax, %fs
movw %ax, %gs
movw %ax, %ss
# 当重新加载GDTR时,GDT信息可能和段寄存器内存放的旧值不符,所以需要将新的段描述符信息写入到相应的段寄存器
movl $start, %esp # 设定栈指针寄存器esp,为之后退栈,即回到这一位置做准备
call bootmain # 调用 /boot/boot.c 中的 bootmain()
此时需要进入到 boot.c
分析源码
boot.c
ps: readseg=read segment 表示读取段;readsec=readsector 表示读取扇区。
#define SECTSIZE 512
#define ELFHDR ((struct Elf *) 0x10000)
void bootmain(void)
{
struct Proghdr *ph, *eph; // Program header Table的pointer,header Table存放了所有段的信息
readseg((uint32_t) ELFHDR, SECTSIZE*8, 0); // 将距离内核offset偏移量的8个扇区(4KB=1Page)的内容读取到以ELFHDR为起点的内存当中。实现的功能为读取elf头部放入内存
if (ELFHDR->e_magic != ELF_MAGIC) {
goto bad; // 验证elf头部信息是否合法:http://wiki.osdev.org/ELF
}
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff); // phoff字段代表Program Header Table距离表头的偏移量
eph = ph + ELFHDR->e_phnum; // e_phnum字段表示表项个数,此时eph指向Program Header Table尾部
for (; ph < eph; ph++) {
readseg(ph->pa, ph->pmemsz, ph->p_offset); // 读取所有表项(段信息)到各自的pa字段表示的物理地址
}
((void (*)(void)) (ELFHDR->e_entry))(); // ELF文件的执行入口,开始执行。且这个ELF文件为内核文件,此时bootloader将控制权转交给了操作系统的内核。
}
//!!!! readseg是此函数的重点调用的函数,很重要。而且其第一个参数并不是真实的起始地址,有一个偏移,具体见下面对readseg的解释,掩码部分
void readseg(uint32_t pa, uint32_t count, uint32_t offset)
{
uint32_t end_pa;
end_pa = pa + count;
pa &= ~(SECTSIZE -1); // 掩码~(SECTSIZE -1)为0x...1100,即将pa的低8位置零,此时pa的地址一定是扇区起始地址的整数倍
offset = (offset / SECTSIZE) + 1; // 将offset由32-bit换算为表示第几个扇区
while (pa < end_pa) {
readsect((int8_t *)pa, offset); // 读取第offset扇区到pa
pa += SECTSIZE; // pa 指示到下一个位置
offset++; // 为读取第offset+1扇区做准备
}
}
回答问题
Q:在什么时候处理器开始运行于32bit模式?到底是什么把CPU从16位切换为32位工作模式?
A:执行完 movl %eax, %cr0
后,就表示CPU运行在保护模式下了,CPU从16位切换为32位工作模式
Q:boot loader中执行的最后一条语句是什么?内核被加载到内存后执行的第一条语句又是什么?
A:最后一条语句是 ((void (*)(void)) (ELFHDR->e_entry))()
,进入到内核程序的起始指令处。内核执行的第一条语句位于 /kern/entry.S
,其为 movw $0x1234, 0x472
Q:boot loader是如何知道它要读取多少个扇区才能把整个内核都送入内存的呢?在哪里找到这些信息?
A:操作系统有多少段,每个段有多少扇区的信息存放在 Program Header Table,这个Table的信息又由ELFHDR的字段进行管理,而ELFHDR固定存放在启动盘的第一个扇区。之后通过 readseg(ph->pa, ph->pmemsz, ph->p_offset);
就把kernel对应的程序,其大小为ph->phmemsz,加载到ph->pa指示的物理地址