一、内核引导过程
1. 从引导加载程序内核
问题:
- 底层是如何工作的
- 程序是如何运行的
- 如何在内存定位的
- 内核是如何管理进程和内存的
- 网络堆是如何在底层工作的
过程:
按下电源开关 ——> 主板发送信号给电源 ——> 电源收到信号给电脑供电 ——> 主板收到“电源备妥信号” ——> 尝试启动CPU ——> CPU复位所有寄存器数据,并设置预定值
注意:
处理器开始在“实模式”工作,它有20位的寻址总线,寻址空间是0~2^20(1MB),但它的寄存器却只有16位(2^16即64KB),所以实模式使用“段式内存管理”来管理整个内存空间。
替代方法:
PhysicalAddress = Segment * 16 + offset
但:
>>> hex((0xffff << 4) + 0xffff)
'0x10ffef'
已经超出1MB范围。既然实模式下, CPU 只能访问 1MB 地址空间,0x10ffef变成有A20缺陷的0x00ffef(CPU只有20位,最高位将被舍弃)
- CS:代码段寄存器
- IP:指令指针寄存器
CS:IP 两个寄存器指示了 CPU 当前将要读取的指令的地址 电脑复位后,CPU寄存器中的预定义数据:
IP 0xfff0
CSselector 0xf000
CSbase 0xffff000
逻辑地址: CS:IP
0xffff0000:0xfff0
>>> 0xffff0000 + 0xfff0
'0xfffffff0'
这个地方是复位向量(Reset vector)。这是CPU在重置后期望执行的第一条指令的内存地址。它包含一个jump指令,这个指令通常指向BIOS入口点。
在初始化和检查硬件之后,需要寻找到一个可引导设备。可引导设备列表存储在在 BIOS 配置中, BIOS 将根据其中配置的顺序,尝试从不同的设备上寻找引导程序。对于硬盘,BIOS 将尝试寻找引导扇区。
一个真实的启动扇区包含了分区表,已经用来启动系统的指令,而不是像我们上面的程序,只是输出了一个感叹号就结束了。从启动扇区的代码被执行开始,BIOS 就将系统的控制权转移给了引导程序。
实模式下的1MB地址空间分配表:
0x00000000 - 0x000003FF - Real Mode Interrupt Vector Table 实模式中断向量表
0x00000400 - 0x000004FF - BIOS Data Area BIOS数据区
0x00000500 - 0x00007BFF - Unused 未被使用
0x00007C00 - 0x00007DFF - Our Bootloader 我们的引导加载程序
0x00007E00 - 0x0009FFFF - Unused
0x000A0000 - 0x000BFFFF - Video RAM (VRAM) Memory 视频RAM(VRAM)存储器
0x000B0000 - 0x000B7777 - Monochrome Video Memory 单色视频存储器
0x000B8000 - 0x000BFFFF - Color Video Memory 彩色视频存储器
0x000C0000 - 0x000C7FFF - Video ROM BIOS 视频ROMD的BIOS
0x000C8000 - 0x000EFFFF - BIOS Shadow Area BIOS阴影区
0x000F0000 - 0x000FFFFF - System BIOS 系统BIOS
问题:CPU 执行的第一条指令是在地址0xFFFFFFF0处,这个地址远远大于0xFFFFF ( 1MB )。那么实模式下的 CPU 是如何访问到这个地址的呢?文档coreboot给出了答案:
0xFFFE_0000 - 0xFFFF_FFFF: 128 kilobyte ROM mapped into address space
0xFFFFFFF0这个地址被映射到了 ROM,因此 CPU 执行的第一条指令来自于 ROM,而不是RAM。
小总结:
1. CPU寄存器复位(0xffff0)
2. 读取ROM指令(jump)
3. 跳转至BIOS入口点
4. 寻找可引导设备(可引导程序,可引导扇区)
拓展知识:
- boot protocol
遗留问题:
- BIOS设置存储在哪里?
2. 引导程序
Linux有多种引导程序,比如GRUB 2和syslinux.
当 BIOS 已经选择了一个启动设备,并且将控制权转移给了启动扇区中的代码(boot.img,非常简单只做必要初始化),跳转到 GRUB 2's core image(diskboot.img,一般是在磁盘上存储在启动扇区之后到第一个可用分区之前)去执行。
core image 的初始化代码会把整个 core image (包括 GRUB 2的内核代码和文件系统驱动)引导到内存中。引导完成之后,grub_main将被调用。
grub_main 初始化控制台,计算模块基地址,设置 root 设备,读取 grub 配置文件,加载模块。最后,将 GRUB 置于 normal 模式,在这个模式中,grub_normal_execute (from grub-core/normal/main.c) 将被调用以完成最后的准备工作,然后显示一个菜单列出所用可用的操从引导加载程序内核1作系统。当某个操作系统被选择之后,grub_menu_execute_entry 开始执行,它将调用 GRUB的boot命令,来引导被选中的操作系统。
正如kernel boot protocol 所描述的,引导程序必须填充 kernel setup header (位于 kernelsetup code 偏移0x01f1处)的必要字段。
当 bootloader 完成任务,将执行权移交给 kernel.
小总结:
// 读取可引导程序,包括:
1. 启动扇区代码(boot.img),必要的初始化,跳转至 GRUB 2's core image
2. core image 的初始化代码将core image(内核代码、文件系统驱动)引导到内存中
3. 引导完成后,调用grub_main,初始化控制台、计算模块基地址、设置root设备、读取grub配置文件、加载模块。将GRUB置于normal模式,最后调用grub_normal_execute显示可用的操作系统
4. 当操作系统被选择之后,grub_menu_execute_entry 开始执行,将调用GRUB的 boot 命令,引导被选中的操作系统
5. 当 bootloader完成任务后,将执行权移交给kernel
遗留问题:
- bootloader作用是什么?
3. 内核设置
从技术上说,内核还没有被运行起来,因为首先我们需要正确设置内核,启动内存管理,进程管理等等。
而内核设置代码的运行起点是 _start函数,但在其开始之前,还有很多代码(bootloader)。去除这些作为 bootloader 使用的代码,真正的内核代码就从_start开始了。其他的 bootloader (grub2 and others) 知道 _start 所在的位置(从MZ头开始偏移0x200字节),所以这些 bootloader 就会忽略所有在这个位置前的代码(这些之前的代码位于.bstext段中),直接跳转到这个位置启动内核。从引导加载程序内核。
_start 开始就是一个 jmp 语句,短跳转至 start_of_setup - 1f。在_start标号之后的第一个标号为1的代码段中包含了剩下的 setup header 结构。在标号为1的代码段结束之后,紧接着就是标号为start_of_setup的代码段(这个代码段位于.entrytext代码区,这个代码段中的第一条指令实际上是内核开始执行之后的第一条指令)
从start_of_setup标号开始的代码需要完成下面这些事情:
- 将所有段寄存器的值设置成一样的内容
- 设置堆栈
- 设置bss(静态变量区)
- 跳转到main.c开始执行代码
(1)段寄存器设置
- 代码段寄存器CS(Code Segment) 存放当前正在运行的程序代码所在段的段基址,表示当前使用的指令代码可以从该段寄存器指定的存储器段中取得,相应的偏移量则由IP提供。
- 数据段寄存器DS(Data Segment) 指出当前程序使用的数据所存放段的最低地址,即存放数据段的段基址。
- 堆栈段寄存器SS(Stack Segment) 指出当前堆栈的底部地址,即存放堆栈段的段基址。
- 附加段寄存器ES(Extra Segment) 指出当前程序使用附加数据段的段基址,该段是串操作指令中目的串所在的段。
将DS和ES段寄存器的内容设置一样:
movw %ds, %ax
movw %ax, %es
sti
将DS和CS段寄存器设置相同的值:
pushw %ds //将DS寄存器的值入栈
pushw $6f //将标号为6的代码段地址入栈
lretw //执行lretw指令将把标号为6的内存地址放入ip寄存器
(2)设置堆栈
绝大部分的 setup 代码都是为 C 语言运行环境做准备。在设置了ds和es寄存器之后,接下来step的代码将检查ss寄存器的内容,如果寄存器的内容不对,那么将进行更正:
movw %ss, %dx
cmpw %ax, %dx //比较ss是否等于ax
movw %sp, %dx //将sp值保存到dx
je 2f //两数相等跳转至标号2段代码
2: andw $~3, %dx //将dx寄存器的值(就是当前sp寄存器的值)4字节对齐
jnz 3f //检查是否为0,不为0跳转
movww $0xfffc, %dx //为0表示栈区已满,需要将sp重置至栈底一个字节前 0xfffc
3: movw %ax, %ss //因为ss和ax相等,所以设置ss栈底地址为0x10000
movzwl %dx, %esp //不为0保留当前sp的值
sti
特别地,当 ss != ds 时,要先将setup code 的结束地址 _end 写入 dx寄存器,然后再检查 loadflags 中是否设置了 CAN_USE_HEAP 标志。
1)CAN_USE_HEAP被置位
movw heap_end_ptr, dx
overflow dx + STACK_SIZE, CF_flag //判断dx是否在栈顶,意思是如果在栈顶 0+STACK_SIZE=ss
jn 2f
2)CAN_USE_HEAP没被置位
movw dx + STACK_SIZE, dx //同理 jmp 2f
(3)BSS段设置
在我们正式执行C代码之前,还有2件事情需要完成:
- 设置正确的BSS段
- 检查 magic 签名
首先检查 magic 签名 setup_sig,如果签名不对,直接跳转到 setup_bad 部分执行代码:
cmpl $0x5a5aaa55, setup_sig
jne setup_bad
如果 magic 签名是对的,那么我们只要设置好 BSS 段就可以考试执行C代码了。
BSS 段用来存储那些没有被初始化的静态变量。对于这个段使用的内存, Linux 首先使用下面的代码将其全部清零:
movw $__bss_start, %di
movw $_end+3, %cx
xorl %eax, %eax
subw %di, %cx
shrw $2, %cx
rep; stopsl
在这段代码中,首先将__bss_start地址放入di寄存器,然后将_end + 3(4字节对齐)地址放入cx,接着使用xor指令将ax寄存器清零,接着计算 BSS 段的大小(cx -di),让后将大小放入cx寄存器。接下来将cx寄存器除4,最后使用rep; stosl指令将ax寄存器的值(0)写入寄存器整个 BSS 段。
-
BSS段 :通常是指用来存放程序中 未初始化的全局变量、静态变量(全局变量未初始化时默认为0)的一块内存区域
-
数据段 :通常是指用来存放程序中 初始化后的全局变量和静态变量
-
代码段 :通常是指用来存放程序中 代码和常量
-
堆 :通常是指用来存放程序中 进程运行时被动态分配的内存段 ( 动态分配:malloc / new,者动态释放:free / delete)
-
栈 :通常是指用来存放程序中 用户临时创建的局部变量、函数形参、数组(局部变量未初始化则默认为垃圾值)也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除 以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。它是由操作系统分配的,内存的申请与回收都由OS管理。
(4)跳转到main函数
到目前为止,我们完成了堆栈和 BSS 的设置,现在我们可以正式跳入main()函数来执行 C代码了:
calll main
小总结:
1. 执行_start函数
2. 开始是一个jmp语句,跳转至start_of_setip - 1f (该代码段中包含了剩下的setup header结构)
3. 在 start_of_setip - 1f 代码段结束之后,执行 start_of_setup 的代码段,实际上这是内核开始执行之后的第一条指令
4. start_of_setup 代码开始后,将完成以下工作:
- 将所有段寄存器的值设置成一样的内容
- 设置堆栈
- 设置bss(静态变量区)
- 跳转到main.c开始执行代码
遗留问题:
- 为什么要将所有段寄存器的值设置成一样?
- 设置堆栈时,只设置了ss与sp的值,没有设置堆的操作?
main() 函数定义在 arch/x86/boot/main.c, 下一篇文章将会详细介绍在Linux内核设置过程中调用的第一个C代码(main()),也将介绍诸如 memset, memcpy, earlyprintk 这些底层函数的实现,敬请期待!
来源:oschina
链接:https://my.oschina.net/u/4198654/blog/3134214