问题:linux
过程:
按下电源开关 ——> 主板发送信号给电源 ——> 电源收到信号给电脑供电 ——> 主板收到“电源备妥信号” ——> 尝试启动CPU ——> CPU复位全部寄存器数据,并设置预约值
注意:
处理器开始在“实模式”工做,它有20位的寻址总线,寻址空间是0~2^20(1MB),但它的寄存器却只有16位(2^16即64KB),因此实模式使用“段式内存管理”来管理整个内存空间。
替代方法:chrome
PhysicalAddress = Segment * 16 + offset
但:数组
>>> hex((0xffff << 4) + 0xffff) '0x10ffef'
已经超出1MB范围。既然实模式下, CPU 只能访问 1MB 地址空间,0x10ffef变成有A20缺陷的0x00ffef(CPU只有20位,最高位将被舍弃)网络
CS:IP 两个寄存器指示了 CPU 当前将要读取的指令的地址 电脑复位后,CPU寄存器中的预约义数据:app
IP 0xfff0 CSselector 0xf000 CSbase 0xffff000
逻辑地址: CS:IPide
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给出了答案:spa
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. 寻找可引导设备(可引导程序,可引导扇区)
拓展知识:指针
遗留问题:
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
遗留问题:
从技术上说,内核尚未被运行起来,由于首先咱们须要正确设置内核,启动内存管理,进程管理等等。
而内核设置代码的运行起点是 _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标号开始的代码须要完成下面这些事情:
将DS和ES段寄存器的内容设置同样:
movw %ds, %ax movw %ax, %es sti
将DS和CS段寄存器设置相同的值:
pushw %ds //将DS寄存器的值入栈 pushw $6f //将标号为6的代码段地址入栈 lretw //执行lretw指令将把标号为6的内存地址放入ip寄存器
绝大部分的 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 标志。
movw heap_end_ptr, dx
overflow dx + STACK_SIZE, CF_flag //判断dx是否在栈顶,意思是若是在栈顶 0+STACK_SIZE=ss jn 2f
movw dx + STACK_SIZE, dx //同理 jmp 2f
在咱们正式执行C代码以前,还有2件事情须要完成:
首先检查 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管理。
到目前为止,咱们完成了堆栈和 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开始执行代码
遗留问题:
main() 函数定义在 arch/x86/boot/main.c, 下一篇文章将会详细介绍在Linux内核设置过程当中调用的第一个C代码(main()),也将介绍诸如 memset, memcpy, earlyprintk 这些底层函数的实现,敬请期待!