MIT6.828-Lab1-Part2-Exercise3-理解bootloader源码

 

详解bootloader源码

理解bootloader源码

boot/boot.Sboot/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操作将cr0bit0置为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指示的物理地址

参考资料

  1. cnblog-fatsheep9146