1. BIOS
Bios详细代码解析,在此略过。主要功能概括来说包括如下几部分:
POST:加电自检,检测 CPU 各寄存器、计时芯片、中断芯片、DMA 控制器等
Initial:枚举设备,初始化寄存器,分配中断、IO 端口、DMA 资源等
Setup :进行系统设置,存于 CMOS 中。
常驻程序:INT 10h、INT 13h、INT 15h 等,提供给操作系统或应用程序调用。
启动自举程序:在POST过程结束后,将调用 INT 19h,启动自举程序,自举程序将读取引导记录,装载操作系统。
BIOS 的启动主要由 POST 过程与自举过程构成。
2. Grub-2.02
2.1 bootstrap image文件
2.1.1 boot.img
这个image是GRUB2第一个被运行的.它被写在MBR(Master Boot Record)或者在分区(partition)的boot sector中.因为MBR或PC boot sector是固定512字节,这个文件的大小也固定为512byte。
boot.img功能很简单,主要是读磁盘中core.img中的第一个扇区(sector)到内存中并跳到该部分运行(如果是硬盘启动,那么该扇区就是下面要介绍的diskboot.img).因为只有512字节,boot.img不能够加载文件系统(比如Linux的EXT4等等),并且只能是从硬盘固定的位置加载.
2.1.2 diskboot.img
当从硬盘启动的时候这是core.img第一个扇区(sector)的内容,主要功能是读剩下的core.img到内存中并开始运行kernel.img. 同样diskboot.img没有文件系统的功能(XFS,EXT4等),当他读取剩余的core.img时候,依然从硬盘固定位置读取.
2.1.3 kernel.img
这个文件包含了GRUB2基本的运行时支撑:对设备及文件的框架,环境变量,恢复模式下的命令行等等.一般我们不会直接使用它,但是它是core.img中必不可少的一部分
2.1.4 core.img
这个是GRUB的核心.他是被grub2-mkimage命令生存,包含了diskboot.img、kernel.img以及一些必须必要的modules. 通常core.img包含了足够的模块(modules)为了访问XFS/EXT4文件系统/boot/grub2目录,并且在运行时加载从文件系统(XFS)所有剩余的模块,这些剩余模块包含
启动目录处理,加载操作系统等等功能.
2.1.5 *.mod
所有GRUB的其他部分被称为模块,他们大部分被core.img在运行时自动动态加载,其中一小部分被整合到core.img中,这小部分是必须,比如文件系统支持(xfs.mod)模块可以手工加载,请参考insmod command
2.2 Grub2加载步骤
2.2.1第一步:boot.img加载、执行
BIOS运行的最后两步操作我们必须知道 :
1)加载LBA-0(或者CHS的0柱面、0磁头、1扇区)的MBR,共512字节到内存中0x7C00位置
2)从内存0x7C00位置运行
存储在MBR中的正是Grub的boot.img,从此,Grub代码开始执行。
boot.img只有512字节,其做的主要工作就是:初始化实模式的堆栈、寄存器等;然后调用BIOS中断INT13把LBA-1(一个扇区,512字节)拷贝到内存0x7000:[0x0000],即0x70000位置(暂时缓存);最后再将其拷贝并跳转到0x8000位置,开始执行。
注:这里有个疑问,为什么分两次拷贝,而不直接拷贝到0x8000处?原因是内核设计者考虑到不同的环境,比如:floppy,disc,LS-120,CD等,他们不全是拷贝512字节(比如CD通常是拷贝2048字节)最大的一次可以拷贝32KB。所以,为了复用代码,采用了两级拷贝方式。
总结:boot.img的功能就是将磁盘LBA-1(0柱面,0磁道,2扇区)的512字节拷贝到0x8000处。LBA-1代码就是core.img的第一个扇区,一般是GRUB2的diskboot.img。
2.2.2第二步:diskboot.img执行
1.初始化。
2.从磁盘读取core.img第二个扇区开始的剩余扇区(扇区数存储在boot.img的0x81fc处)到内存0x7000:[0x0000],即0x70000位置(暂时缓存)。
3.拷贝缓存的内容至0x8200位置。
4.跳转至0x8200处执行。
总结:diskboot.img就是实现把core.img剩余部分拷贝到内存。
注:
这里又有个疑问,为什么要通过boot拷贝diskboot,再通过diskboot拷贝core.img,而不直接用boot拷贝core.img呢?甚至直接BIOS拷贝core.img呢?
diskboot.img是压缩在core.img中第一个扇区的,用于引导core.img剩余部分。这里有个前提是,我们讨论的是磁盘启动,如果是光盘启动,则diskboot.img就会被替换为cdboot.img,当然还有软盘或其他启动方式。boot.img并不能具体知道core.img的组成。
至于boot.img存在的意义,则是因为MBR或PC boot sector是固定512字节,这是PC设计的约定,BIOS只认MBR来启动。
2.2.3 第三步:core.img执行
剩余的core.img分两部分内容:
1.开始部分是startup_raw.S代码;
2.后一部分为压缩的Grub2核心代码
startup_raw.S分析:
1.初始化堆栈、寄存器
2.进入保护模式
3.解压核心代码,并跳转执行
因为core.img有32K限制,所以其核心代码(kernel.img)是压缩的。解压后的核心代码位于0x100000(实模式访问的内存上限为1M)。
核心代码startup.S分析:
初始化堆栈、寄存器等,跳转到grub_main,进入grub的主函数。
核心代码main.c分析:
主要工作包括:grub模块化框架初始化;各种命令的注册;各种模块的加载;读取/boot/grub2/grub.cfg,显示启动菜单;根据菜单配置加载linux kernel。
2.2.4 第四步:加载Linux Kernel
下面是现代bzImage类型 kernel(version>=2.02)结构:
1.linux16命令
在/boot/grub2/grub.cfg中会调用linux16命令来加载bzImage的linux内核。linux16命令的注册在grub-core/loader/i386/pc/linux.c文件的grub_cmd_linux函数。
2.grub_cmd_linux函数
1)打开内核:file = grub_file_open (argv[0]);
2)读入内核头部(struct linux_kernel_header),里面包含了内核基本信息,后面加载时用:grub_file_read (file, &lh, sizeof (lh))
3)计算实模式地址(一般为0x90000): grub_linux_real_target = grub_find_real_target ();
4)分别计算用于实模式和保护模式的内核尺寸:
real_size = setup_sects << GRUB_DISK_SECTOR_BITS;(在内核头部定义)
grub_linux16_prot_size = grub_file_size (file) - real_size - GRUB_DISK_SECTOR_SIZE;(内核文件大小减去实模式大小,再减去1个扇区大小)
疑问:此处为什么还要减去一个扇区大小?
5)读取实模式代码到0x90000
6)准备linux内核启动的传递参数
7)读取linux保护模式代码到0x100000
3.initrd16命令
在/boot/grub2/grub.cfg中会调用initrd16命令来初始化,并加载ram disk file system。initrd16命令的注册在grub-core/loader/i386/pc/linux.c文件的grub_cmd_initrd函数。
4.boot命令
当linux16和initrd16命令执行完后,boot命令就会执行。其对应代码在grub-core/commands/boot.c文件grub_cmd_boot函数。
函数调用关系如下:
grub_cmd_boot ---> grub_loader_boot ---> grub_loader_boot_func(在linux16命令的处理函数中此钩子函数被赋值为grub_linux16_boot)=grub_linux16_boot ---> grub_relocator16_boot ---> grub_relocator_prepare_relocs(此函数为根据ELF格式进行代码重定位<抽空仔细研究下>)---> ((void (*) (void)) relst) ();
从此跳入linux内核代码执行(入口地址0x9020:[0x0000]),永不回来。
注:内核源码目录下Documentation/x86/boot.txt对boot相关知识有详细说明,可参考。
3. Linux-4.13.2
3.1 bzImage的组成及内存布局
根据makefile构建规则,bzImage依赖于setup.bin和vmlinux.bin,所以在构建bzImage前,make将自动先去构建它们,以此类推,vmlinux的构建也是同样的道理。因此,组成内核映像的各个部分的构建顺序如下:
1) 构建内核镜像linux-4.13.2/vmlinux,并利用objcopy拷贝为linux-4.13.2/arch/x86/boot/compressed/vmlinux.bin,再调用gzip将其压缩为vmlinux.bin.gz,最后利用mkpiggy将vmlinux.bin.gz重定向到piggy.S,从而创建piggy.S。
2) 将head_64.o、misc.o以及包含压缩映像的piggy.o等目标文件链接为linux-4.13.2/arch/x86/boot/compressed/vmlinux,并通过objcopy拷贝到上级目录下linux-4.13.2/arch/x86/boot/vmlinux.bin。
3) 构建linux-4.13.2/arch/x86/boot/setup.elf,利用objcopy拷贝为相同目录下的setup.bin。
4) 将setup.bin和vmlinux.bin组合为bzImage。
大内核情况下的内存分布图:
3.2 setup.ld文件解析
在进入源代码的世界之前,我们先看看用于控制 arch/x86/boot 下代码进行链接的 setup.ld。
ld 文件用于控制 ld 的链接过程:
•描述输入文件的各节如何对应到输出文件的各节
•控制输入文件各节及符号的内存布局
每个对象文件有一个节(section)列表、一个符号列表,一个符号可以是已定义或未定义的。每个已定义的符号有地址。未定义的符号则要在链接时从其他文件中寻找其定义。
1.指定输出文件格式 OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
2.指定目标体系结构 OUTPUT_ARCH(i386)
3.设置入口点 ENTRY(_start)
4.输入文件各节到输出文件的映射 SECTIONS
{
. = 0 // 从 0 开始
.bstext : { *(.bstext) } // 所有输入文件的 .bstext 节组合成输出文件的 .bstext 节
.bsdata : { *(.bsdata) } // 所有输入文件的 .bsdata 节...
. = 495 // 填充 512 字节的 bootloader(见下一节 header.S)
.header : { *(.header) }
在每一部分(header、rodata、data、bss、end)之间,对齐 16 字节内存边界:
. = ALIGN(16);
最后用断言保证链接后的目标文件不太大,且偏移量正确。
3.3 入口文件header.S
start2:
movw %cs, %ax # CS = 0x7c00
movw %ax, %ds # 初始化段寄存器
movw %ax, %es
movw %ax, %ss
xorw %sp, %sp
sti # 开中断
cld # di++, si++
................................
msg_loop: # 打印字符例程
................................
bs_die: # 错误处理例程
.ascii "Direct booting from floppy is no longer supported.\r\n"
.ascii "Please use a boot loader program instead.\r\n"
.ascii "\n"
.ascii "Remove disk and press any key to reboot . . .\r\n"
.byte 0
这段代码编译链接后,会生成 512 字节的 bootsector,其中 .section ".header", "a" 中的变量共 17字节。注意到 setup.ld (Linker script for the i386 setup code) 中加入了 495字节的空白,事实上恰好凑够 512 字节。
事实上,上一节我们提到,MBR 是由 GRUB 写入的,因此这里的 bootsector 对于硬盘启动是用不到的。GRUB 等 boot loader 将 setup.bin 读到 0x90000 处,将 vmlinux.bin 读到 0x100000 处,然后跳转到 0x90200 开始执行,恰好跳过了 512 字节的 bootsector。
有意思的是,从软盘启动时,header.S 生成的 bootsector 做的惟一一件事就是打印错误信息(bs_die),不支持从软盘启动。
下面就是 0x90200(_start)了,目的就是跳到 start_of_setup。
# Part 2 of the header, from the old setup.S
................................
# End of setup header #####################################################
上面这两行之间的代码是一个庞大的数据结构,与 include/asm/bootparam.h 中的 struct setup_header 一一对应。这个数据结构定义了启动时所需的默认参数,其中一些参数可以通过命令选项 overwrite。下表列出了部分参数的意义。
名称 |
偏移 |
大小(字节) |
意义 |
root_flags |
0x1f2 |
2 |
根目录是否只读,可用 ro 或 rw 选项指定 |
root_dev |
0x1fc |
2 |
默认的 root 设备,即 /boot 所在目录,可用 root= 选项指定 |
boot_flag |
0x1fe |
2 |
0xAA55,即主引导扇区结束标志 |
header |
0x202 |
4 |
HdrS (0x53726448),内核标志 |
version |
0x206 |
2 |
启动协议版本号: major * 64 + minor |
kernel_version |
0x20e |
2 |
内核版本号 |
type_of_loader |
0x210 |
1 |
Boot loader ID: Boot loader ID * 64 + Version No. Boot loader IDs: 0 LILO 1 Loadlin 2 bootsect-loader 3 SYSLINUX 4 EtherBoot 5 ELILO 7 GRuB 8 U-BOOT 9 Xen A Gujin B Qemu |
loadflags |
0x211 |
1 |
启动选项的掩码。 · Bit 0: LOADED_HIGH (1表示保护模式代码加载到 0x100000) · Bit 7: CAN_USE_HEAP (为1表示 heap_end_ptr 有效) |
code32_start |
0x214 |
4 |
内核解压缩前立即跳转到的 32 位 flat-mode 入口 |
ramdisk_image |
0x218 |
4 |
initramfs 的 32 位线性地址 |
cmd_line_ptr |
0x228 |
4 |
内核命令行的 32 位线性地址 |
下面我们迎来了真正的起点(start_of_setup),主要流程为:
1. 复位硬盘控制器
2. 如果 %ss 无效,重新计算栈指针
3. 初始化栈,开中断
4. 将 cs 设置为 ds,与 setup.bin 的入口地址一致
5. 检查主引导扇区末尾标志,如果不正确则跳到 setup_bad
6. 清空 bss 段
7. 跳到 main(定义在 boot/main.c)
3.4 初始化与保护模式(main.c、pm.c、pmjump.S)
我们终于暂时离开了汇编代码,走进 “主要” 的启动部分。这一部分在 arch/x86/boot/main.c 中。
main() 中的几个函数调用都有比较详细的注释,主要作用是初始化 boot_params,将来会经常被用到。
include/asm/bootparam.h 中定义的 boot_params 结构体 (即 zeropage) 在此完成初始化:
· copy_boot_params() 初始化 boot_params.hdr (将 hdr 复制过来)
· detect_memory() 初始化 boot_params.e820_map 和 boot_params.e820_entries
· query_apm_bios() 初始化 apm_bios_info、screen_info
go_to_protected_mode() 进入保护模式,代码在 boot/pm.c。
1. realmode_switch_hook():boot_params.hdr 中有 realmode_swtch,记录了 hook 函数地址,如果有的话就执行之
2. reset_coprecessor(): 重启协处理器
3. make_all_interrupts(): 关闭所有旧 PIC 上的中断。其中的 io_delay 等待 I/O 操作完成。
4. setup_idt(): 初始化中断描述符表 (空的)
5. setup_gdt(): 初始化 GDT:
o GDT_ENTRY_BOOT_CS
o GDT_ENTRY_BOOT_DS
o GDT_ENTRY_BOOT_TSS
其中 GDT_ENTRY_BOOT_CS 和 GDT_ENTRY_BOOT_DS 基地址都为零,段限长都是 4G。
6. protected_mode_jump(): 汇编代码,位于boot/pmjump.S 中。传参说明:进入保护模式后将采用段访问内存地址,因此要将传入的参数转换为线性地址。
3.5 自解压内核
上节末尾的protected_mode_jump函数中的jmpl 指令把我们带入了 vmlinux.bin 的世界。注意到,vmlinux.bin 包含了压缩的内核镜像(piggy.S中的vmlinux.bin.gz),因此内核首先的工作就是把真正的内核解压出来。
循着 Makefile 的踪迹,我们找到了 arch/x86/boot/compressed/head_64.S,这就是大内核模式下 0x100000开始的内存内容,入口为ENTRY(startup_32),工作流程如下:
1. 找到 vmlinux.bin 的入口地址,并将其存入 ebp。
2. 如果设置了可重入内核,就将 ebp 按照 kernel_alignment 对齐,放入 ebx。
3. 确定解压内核的内存地址
4. 设置栈
5. 将 vmlinux.bin 复制到安全地区(ebx 指定的地方):保存 esi 到栈中,首先计算出需要复制的字节数目,然后4个字节为一组地复制过去,再从栈中恢复 esi。
6. 进入 relocated,清空 BSS,初始化解压函数所用的栈
7. 将 decompress_kernel 所用的参数入栈:内核加载地址、内核长度、压缩内核安全地址、堆地址、启动参数结构体指针。
8. 调用 decompress_kernel 解压内核
9. 如果设置了可重入内核,进行一些 relocate
10. 跳转到解压后的内核。
注:以上是针对32位流程,64位流程待有空详细梳理下 !!
至此,arch/x86/boot 下的流程基本分析完毕。
3.6 解压后内核入口:startup_64
真正的内核入口是 arch/x86/kernel/head_64.S。
注:下面以32位即arch/x86/kernel/head_32.S,来进行流程分析,64位流程待有空详细梳理!!
汇编函数 startup_32 依次完成以下动作:
3.6.1 初始化参数
l 初始化 GDT。boot_gdt_descr 在数据区中记载了 GDT 表首地址。 lgdt pa(boot_gdt_descr)
l 清空 BSS 段
l 复制实模式中的 boot_params 结构体
l 复制命令行参数到 boot_command_line (供 init/main.c 使用)
l 有关虚拟环境的一些配置
3.6.2 开启分页机制
尽管我们已经在保护模式中,但只有段机制而没有启用页机制。这里设置全局页目录与页表项,并开启分页机制。
l 如果启用了 PAE,即物理地址扩展到 64G 的机制,不作分析。
l 不然,就是通常的 4G 线性地址空间。__PAGE_OFFSET 是内核编译时配置的内核地址空间偏移,默认为 3G。默认配置下,进程的用户态地址空间为 0~3G,高 1G 是内核地址空间。
l 全局页目录大小为 4KB,每项大小为 4B,可以表示 4MB 的线性范围,因此页目录的大小是 __PAGE_OFFSET >> 20。
page_pde_offset = (__PAGE_OFFSET >> 20);
l 初始化页表首地址 %edi、全局页目录地址 %edx、PTE 属性(页目录和页表的每项 4 Byte 中后 12 位是属性,这里预先填充 0x67)
230 movl $pa(pg0), %edi
231 movl $pa(swapper_pg_dir), %edx
232 movl $PTE_ATTR, %eax
l 下面是一个双层循环,外层循环填充页目录,内层循环填充页表。
233 10:
# %edi: 页表首地址
234 leal PDE_ATTR(%edi),%ecx /* Create PDE entry */
# 将页目录项填充到页目录中,%edx 为页目录地址
235 movl %ecx,(%edx) /* Store identity PDE entry */
236 movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */
# 填充下一个页目录项
237 addl $4,%edx
238 movl $1024, %ecx
239 11: # 内层循环,填充 4KB 的 PTD
240 stosl # es:edi= eax,edi++
# 表面上看是将 0x1000 加到属性上,事实上是 %eax 的后 12 位属性不变,前面的 20 位页地址加 1。
241 addl $0x1000,%eax
# 继续内层循环
242 loop 11b
243 /*
244 * End condition: we must map up to and including INIT_MAP_BEYOND_END
245 * bytes beyond the end of our own page tables; the +0x007 is
246 * the attribute bits
247 */
# 计算何时应停止
248 leal (INIT_MAP_BEYOND_END+PTE_ATTR)(%edi),%ebp
# 如果 %eax < %ebp,继续外层循环
249 cmpl %ebp,%eax
250 jb 10b
l 添加页目录项的最后一项,页表地址为 swapper_pg_fixmap,用于 fixmap area
251 movl %edi,pa(init_pg_tables_end)
252
253 /* Do early initialization of the fixmap area */
254 movl $pa(swapper_pg_fixmap)+PDE_ATTR,%eax
255 movl %eax,pa(swapper_pg_dir+0xffc)
l 有关对称多处理器(SMP)的处理
l 一些 CPU 参数相关的判断和处理
l 开启分页机制
# 将页表首地址(swapper_pg_dir)放入 cr3
331 movl $pa(swapper_pg_dir),%eax
332 movl %eax,%cr3 /* set the page table pointer.. */
# 设置 cr0 的 paging 位,打开 cr0 的分页机制
333 movl %cr0,%eax
334 orl $X86_CR0_PG,%eax
335 movl %eax,%cr0 /* ..and set paging (PG) bit */
# 目前已经开启分页机制,完全进入保护模式。
336 ljmp $__BOOT_CS,$1f /* Clear prefetch and normalize %eip */
3.6.3 初始化 Eflags
3.6.4 初始化中断向量表
在实模式中,已经初始化了 IDT,不过现在我们要对保护模式再做一次这样的工作。由于这段代码比较长,放在了单独的函数里。
485 setup_idt:
# 默认中断处理例程,后面有定义,做一件事情:如果开启了 CONFIG_PRINTK,就通过 printk 输出内核信息。
486 lea ignore_int,%edx
# 这里是内核代码段,注意已经是保护模式了,所以要用代码段选择子
487 movl $(__KERNEL_CS << 16),%eax
488 movw %dx,%ax /* selector = 0x0010 = cs */
489 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
490
# 载入 IDT 表的首地址
491 lea idt_table,%edi
# 共有 256 个中断向量
492 mov $256,%ecx
493 rp_sidt:
# 这是一个循环,用默认中断处理例程初始化 256 个中断向量
494 movl %eax,(%edi)
495 movl %edx,4(%edi)
496 addl $8,%edi
497 dec %ecx
498 jne rp_sidt
499
# 设置几个已定义的中断向量
# 宏定义
500 .macro set_early_handler handler,trapno
501 lea \handler,%edx
502 movl $(__KERNEL_CS << 16),%eax
503 movw %dx,%ax
504 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
505 lea idt_table,%edi
506 movl %eax,8*\trapno(%edi)
507 movl %edx,8*\trapno+4(%edi)
508 .endm
509 # 预先设置的中断向量
510 set_early_handler handler=early_divide_err,trapno=0 # 被零除
511 set_early_handler handler=early_illegal_opcode,trapno=6 # 操作码异常
512 set_early_handler handler=early_protection_fault,trapno=13 # 保护错误
513 set_early_handler handler=early_page_fault,trapno=14 # 缺页异常
514 # 后面一段代码定义了这四个中断向量的中断处理例程。
# 它们都调用了 early_fault,即将当前状态、中断向量号等信息通过 early_printk 或 printk 输出。
515 ret
3.6.5 检查处理器类型
l 检查是 486 还是 386
l get vendor info
l 如果是 486,就 set AM, WP, NE, MP;如果是 386,就 set MP
l save PG, PE, ET
l check ET for 287/387
3.6.6 载入 GDT、IDT
l 重新载入修改 GDT 后的段寄存器
l DS/ES 包含着默认用户段
l 清除 GS、LDT
3.6.7 i386_start_kernel
如果是 SMP 架构,则由第一个 CPU 调用 start_kernel,其余 CPUs 调用 initialize_secondary。跳转到 i386_start_kernel(在 arch/x86/kernel/head32.c)
head_32.S 中的其余代码是 BSS 段、数据段。
arch/x86/kernel/head32.c 中的 i386_start_kernel 主要作用就是调用start_kernel(),然后跳转到其实现文件:init/main.c ,执行体系结构无关的核心数据结构初始化。
start_kernel函数在此不做讨论。