Boot Loader即引导程序,它在BIOS执行完毕后被执行,它的代码在JOS中由两部分组成,boot.S汇编语言文本文件和main.c的C语言文本文件,之前说到汇编代码跳转到叫做bootmain的地方,它是main.c的一个函数:

void
bootmain(void)
{struct Proghdr *ph, *eph;// read 1st page off diskreadseg((uint32_t) ELFHDR, SECTSIZE*8, 0);// is this a valid ELF?if (ELFHDR->e_magic != ELF_MAGIC)goto bad;// load each program segment (ignores ph flags)ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);eph = ph + ELFHDR->e_phnum;for (; ph < eph; ph++)// p_pa is the load address of this segment (as well// as the physical address)readseg(ph->p_pa, ph->p_memsz, ph->p_offset);// call the entry point from the ELF header// note: does not return!((void (*)(void)) (ELFHDR->e_entry))();bad:outw(0x8A00, 0x8A00);outw(0x8A00, 0x8E00);while (1)/* do nothing */;
}

粗略地看这个函数,它的功能正如注释所说,是read segment,就是读取磁盘的一段数据,readseg这个函数的具体实现不再描述,但是简单研究会发现,它本质上就是IDE磁盘的原始I/O实现。

readseg的函数原型是:

readseg(uint32_t pa, uint32_t count, uint32_t offset)

第一个参数是物理地址,表示读取磁盘的内容放在物理内存地址pa处,第二个是读取的字节数,第三个是从磁盘的第几个字节开始读取。

那么根据bootmain中的调用,显然是:从磁盘位置0(一开始)读取了SECTSIZE*8个字节到物理内存ELFHDR处,SECTSIZE和ELFHDR分别是:

#define SECTSIZE 512
#define ELFHDR      ((struct Elf *) 0x10000) // scratch space

这样,我们可以认为读取了4KB(一个页)的内容,并加载到了0x10000的位置。在后面就会知道,这就是内核(Kernal)的加载地址。bootmain把内核当作一个Elf文件,并用一个Elf的指针(指针的值是写死在宏里的的:0x10000)访问它的对象,然后用一个for循环把内核全部加载进来。最后,也是最重要的这一行:

 ((void (*)(void)) (ELFHDR->e_entry))();

它读取了Elf文件的e_entry的内容,它是一个函数指针,然后执行了这个entry函数,启动了内核开始的代码。因此,bootmain函数不会返回。

随后就结束main.c的任务了,接下来进入了内核的entry.S汇编代码:

这段代码是.text段的:

.globl       _start
_start = RELOC(entry).globl entry
entry:movw  $0x1234,0x472           # warm boot# We haven't set up virtual memory yet, so we're running from# the physical address the boot loader loaded the kernel at: 1MB# (plus a few bytes).  However, the C code is linked to run at# KERNBASE+1MB.  Hence, we set up a trivial page directory that# translates virtual addresses [KERNBASE, KERNBASE+4MB) to# physical addresses [0, 4MB).  This 4MB region will be suffice# until we set up our real page table in mem_init in lab 2.# Load the physical address of entry_pgdir into cr3.  entry_pgdir# is defined in entrypgdir.c.movl $(RELOC(entry_pgdir)), %eaxmovl %eax, %cr3# Turn on paging.movl %cr0, %eaxorl   $(CR0_PE|CR0_PG|CR0_WP), %eaxmovl   %eax, %cr0# Now paging is enabled, but we're still running at a low EIP# (why is this okay?).  Jump up above KERNBASE before entering# C code.mov  $relocated, %eaxjmp *%eax
relocated:# Clear the frame pointer register (EBP)# so that once we get into debugging C code,# stack backtraces will be terminated properly.movl   $0x0,%ebp           # nuke frame pointer# Set the stack pointermovl $(bootstacktop),%esp# now to C codecall i386_init# Should never get here, but in case we do, just spin.
spin:   jmp spin

这段代码的核心就是设置CR3和CR0,然后跳转到i386_init,这是内核的C语言入口。其实,之所以这段是汇编语言,是迫不得已,因为C语言要求我们使用虚拟内存,访问虚拟地址,然而我们并没有设置地址翻译所需要的环境,所以需要汇编语言来帮忙。

汇编代码的注释表明,一开始,我们需要一个手工编写的页表,来解决C语言代码执行上的需求,页表的物理地址就是entry_pgdir,然而我们发现它被套上了一个宏RELOC(),它的定义是:

#define  RELOC(x) ((x) - KERNBASE)

直接把x的值减去KERNBASE,KERNBASE就是0xf0000000,之后我们会知道,这就是内核的起始虚拟地址(回想Linux的虚拟地址空间,内核的地址很高)。减去的原因是编译器在链接时会把entry_pgdir解释为虚拟地址,一个高于KERNBASE的值,这部分的原理不再深究,过于复杂。

可以查看entry_pgdir是怎样手工编写的:

pde_t entry_pgdir[NPDENTRIES] = {// Map VA's [0, 4MB) to PA's [0, 4MB)[0]= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P,// Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)[KERNBASE>>PDXSHIFT]= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W
};

回忆经典的两级页表结构,在PD中有许多pde条目,每个pde中保存的是指向一个PT页的物理地址,然后每个PT中有许多pte,每个pte保存一个物理地址。

可以注意到entry_pgdir有两项,分别表明了两个映射,却都指向了同一个PT(详见注释),这个PT,即entry_pgtable,的形式是:

pte_t entry_pgtable[NPTENTRIES] = {0x000000 | PTE_P | PTE_W,0x001000 | PTE_P | PTE_W,0x002000 | PTE_P | PTE_W,0x003000 | PTE_P | PTE_W,0x004000 | PTE_P | PTE_W,0x005000 | PTE_P | PTE_W,0x006000 | PTE_P | PTE_W,0x007000 | PTE_P | PTE_W,0x008000 | PTE_P | PTE_W,0x009000 | PTE_P | PTE_W,0x00a000 | PTE_P | PTE_W,0x00b000 | PTE_P | PTE_W,0x00c000 | PTE_P | PTE_W,0x00d000 | PTE_P | PTE_W,0x00e000 | PTE_P | PTE_W,0x00f000 | PTE_P | PTE_W,0x010000 | PTE_P | PTE_W,0x011000 | PTE_P | PTE_W,

如此一直到:

        0x3fd000 | PTE_P | PTE_W,0x3fe000 | PTE_P | PTE_W,0x3ff000 | PTE_P | PTE_W,
};

非常简单地逐个手工映射……(只映射了4MB,但作为启动的临时页表,够用了)

接下来就是使用它的时候:可以注意到它被加载到CR3里去了,然后设置了CR0,开启了分页功能(让这个页表生效,实现映射),在之前,已经设置了一次CR0了(打开保护模式),但是并没有打开分页,那么当时bootmain是怎样被正确链接的,C代码为什么可以执行?其实是设置了GDT,把虚拟地址直接变成物理地址(没有任何转化),当时并没有想清楚……所以当时其实链接地址就是加载地址,还是在操作物理地址。

那么尽管现在打开分页,但内核基地址(KERNBASE)是0xf0000000,现在EIP需要跳转到那个地方去,让EIP改变的最简单方法是:

mov  $relocated, %eaxjmp *%eax
relocated:

这段代码如同之前进入保护模式一样巧妙,这里relocated是虚拟地址(大于KERNBASE),但是这条跳转语句仍可以在开启分页后执行在原来的EIP(一个很低的位置),原因就是之前设置了手工编写的页表entry_pgdir,把最初的4MB映射到了KERNBASE以上的4MB,又映射到了虚拟空间最初的4MB,所以EIP在这之间自由切换是没有问题的。

所以,汇编代码真的很有技巧性,回想起之前的进程切换,使用汇编代码改变ESP,这是C代码无法实现的,逻辑上也不直观了……

最后,又进入C代码,初始化内核:

 call    i386_init

这次就先到这里了。

计算机启动流程分析--以JOS为例(从boot loader 到kernal)相关推荐

  1. u-boot启动流程分析

    u-boot启动流程分析 以smdk2410为例,分析u-boot的启动流程.u-boot的启动流程是指从cpu上电开机执行u-boot到u-boot成功加载完操作系统的过程.这一过程可以分为两个阶段 ...

  2. SpringBoot启动流程分析(四):IoC容器的初始化过程

    SpringBoot系列文章简介 SpringBoot源码阅读辅助篇: Spring IoC容器与应用上下文的设计与实现 SpringBoot启动流程源码分析: SpringBoot启动流程分析(一) ...

  3. 常见SOC启动流程分析

    常见SOC启动流程分析 本文以s5pv210这款SOC为例,分析了其启动流程 在s5pv210的SOC内部,存在着一个内部的ROM和一个内部的RAM 这个内部的ROM叫做 IROM,它是norflas ...

  4. Bootm启动流程分析

    Bootm启动流程分析 如何引导内核 uboot启动命令 内核镜像介绍 内核启动前提 Bootm命令详解 Bootm命令格式 do_bootm do_bootm_subcommand images全局 ...

  5. SpringBoot(十二)启动流程分析之创建应用上下文AnnotationConfigServletWebServerApplicationContext

    SpringBoot版本:2.1.1      ==>启动流程分析汇总 接上篇博客Spring Boot 2.1.1(十一)启动流程分析之设置系统属性spring.beaninfo.ignore ...

  6. u-boot启动流程分析(1)_平台相关部分

    转自:http://www.wowotech.net/u-boot/boot_flow_1.html 1. 前言 本文将结合u-boot的"board->machine->arc ...

  7. Alian解读SpringBoot 2.6.0 源码(八):启动流程分析之刷新应用上下文(中)

    目录 一.背景 1.1.刷新的整体调用流程 1.2.本文解读范围 二.调用后处理器 2.1.调用在上下文中注册为beanFactory的后置处理器 2.2.invokeBeanFactoryPostP ...

  8. Alian解读SpringBoot 2.6.0 源码(七):启动流程分析之准备应用上下文

    目录 一.背景 1.1.run方法整体流程 1.2.本文解读范围 二.准备应用上下文 2.1.整体流程 2.2.设置环境 2.3.应用上下文进行后置处理 2.4.应用所有初始化器 2.5.发布应用上下 ...

  9. Alian解读SpringBoot 2.6.0 源码(六):启动流程分析之创建应用上下文

    目录 一.背景 1.1.run方法整体流程 1.2.本文解读范围 二.创建应用上下文 2.1.初始化入口 2.2.初始化AbstractApplicationContext 2.3.初始化Generi ...

最新文章

  1. 【Plant Cell】突破!加入一种酵母,可显著提高水稻氮利用率及产量!
  2. 【Usaco2014Open银组】照相(pairphoto)
  3. Ids4 认证保护 API 方案更新
  4. 解决linux下创建用户时出现Creating mailbox file: File exists
  5. 判断Python输入是否为数字
  6. mxnet img2rec的使用,生成数据文件
  7. Python正则表达式,看完这篇文章就够了...#华为云·寻找黑马程序员#
  8. C#将DataGridView中的数据导出为EXCEL
  9. unity NGUI下载 支持unity高版本
  10. Android Studio 开发支付宝小程序
  11. 期权与期货有哪些不同?
  12. 转载-谈谈我这些年的互联网赚钱经历
  13. 用python做个聊天机器人与群发助手~再也不怕没时间回女友,闺蜜被胖揍了~
  14. 2020年下半年系统架构设计师论文真题
  15. 虾皮运营之不实折扣有多严重?堪比双十一 虾皮运营技巧
  16. gt; 和 lt; 代表大于号gt; 和小于号lt; 以及其英文的全称
  17. 操作系统实验五 基于内核栈切换的进程切换(哈工大李治军)
  18. c/c++实现五子棋
  19. Linux系统aboutyou,Linux字符设备驱动高级
  20. 《谁还能说〈周易〉读不懂、没读懂?》系列论文(三):《周易》象数思维方式演绎中国传统文化(吉 华)...

热门文章

  1. WireGuard Over VLESS——一个更稳定的三层隧道
  2. 谈谈中行E令可能存在的问题
  3. 深入浅出TensorFlow2函数——tf.keras.layers.Embedding
  4. java令牌_基于令牌桶算法的Java限流实现
  5. 讴 mysql 首字母_汉字转全拼音函数优化方案(SQLServer),值得你看看
  6. 经典语义分割FCN网络的学习记录(PPT,附有文章链接)
  7. 价格奥秘-在超市遇见亚当斯密--第十三章 把未来留给未知的一切
  8. 仅需三步,保存Bing主页壁纸
  9. 如何做一个基于微信物业维修报修小程序系统毕业设计毕设作品
  10. 拉斯维加斯算法结合回溯法求解n后问题