x86架构linux内核引导过程分析


1. BIOS

Bios详细代码解析,在此略过。主要功能概括来说包括如下几部分:

POST:加电自检,检测 CPU 各寄存器、计时芯片、中断芯片、DMA 控制器等

Initial:枚举设备,初始化寄存器,分配中断、IO 端口、DMA 资源等

Setup :进行系统设置,存于 CMOS 中。

常驻程序INT 10hINT 13hINT 15h 等,提供给操作系统或应用程序调用。

启动自举程序POST过程结束后,将调用 INT 19h,启动自举程序,自举程序将读取引导记录,装载操作系统。

BIOS 的启动主要由 POST 过程与自举过程构成。

2. Grub-2.02

2.1 bootstrap image文件

2.1.1 boot.img

这个imageGRUB2第一个被运行的.它被写在MBR(Master Boot Record)或者在分区(partition)boot sector中.因为MBRPC boot sector是固定512字节,这个文件的大小也固定为512byte

boot.img功能很简单,主要是读磁盘中core.img中的第一个扇区(sector)到内存中并跳到该部分运行(如果是硬盘启动,那么该扇区就是下面要介绍的diskboot.img).因为只有512字节,boot.img不能够加载文件系统(比如LinuxEXT4等等),并且只能是从硬盘固定的位置加载.

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.imgkernel.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(或者CHS0柱面、0磁头、1扇区)MBR,共512字节到内存中0x7C00位置

2)从内存0x7C00位置运行

存储在MBR中的正是Grubboot.img,从此,Grub代码开始执行。

boot.img只有512字节,其做的主要工作就是:初始化实模式的堆栈、寄存器等;然后调用BIOS中断INT13LBA-1(一个扇区,512字节)拷贝到内存0x7000:[0x0000],即0x70000位置(暂时缓存);最后再将其拷贝并跳转到0x8000位置,开始执行。

注:这里有个疑问,为什么分两次拷贝,而不直接拷贝到0x8000处?原因是内核设计者考虑到不同的环境,比如:floppydiscLS-120CD等,他们不全是拷贝512字节(比如CD通常是拷贝2048字节)最大的一次可以拷贝32KB。所以,为了复用代码,采用了两级拷贝方式。

总结:boot.img的功能就是将磁盘LBA-10柱面,0磁道,2扇区)的512字节拷贝到0x8000处。LBA-1代码就是core.img的第一个扇区,一般是GRUB2diskboot.img

2.2.2第二步:diskboot.img执行

1.初始化。

2.从磁盘读取core.img第二个扇区开始的剩余扇区(扇区数存储在boot.img0x81fc处)到内存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存在的意义,则是因为MBRPC 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.img32K限制,所以其核心代码(kernel.img)是压缩的。解压后的核心代码位于0x100000(实模式访问的内存上限为1M)。

核心代码startup.S分析:

初始化堆栈、寄存器等,跳转到grub_main,进入grub的主函数。

核心代码main.c分析:

主要工作包括:grub模块化框架初始化;各种命令的注册;各种模块的加载;读取/boot/grub2/grub.cfg,显示启动菜单;根据菜单配置加载linux kernel

2.2.4 第四步:加载Linux Kernel

下面是现代bzImage类型 kernelversion>=2.02)结构:

1.linux16命令

/boot/grub2/grub.cfg中会调用linux16命令来加载bzImagelinux内核。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 systeminitrd16命令的注册在grub-core/loader/i386/pc/linux.c文件的grub_cmd_initrd函数。

4.boot命令

linux16initrd16命令执行完后,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.txtboot相关知识有详细说明,可参考。

3. Linux-4.13.2

3.1 bzImage的组成及内存布局

根据makefile构建规则,bzImage依赖于setup.binvmlinux.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,最后利用mkpiggyvmlinux.bin.gz重定向到piggy.S,从而创建piggy.S

2) head_64.omisc.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.binvmlinux.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) }

在每一部分(headerrodatadatabssend)之间,对齐 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.cpmjump.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_infoscreen_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 初始化参数

初始化 GDTboot_gdt_descr 在数据区中记载了 GDT 表首地址。 lgdt pa(boot_gdt_descr)

清空 BSS

复制实模式中的 boot_params 结构体

复制命令行参数到 boot_command_line (init/main.c 使用)

有关虚拟环境的一些配置

3.6.2 开启分页机制

尽管我们已经在保护模式中,但只有段机制而没有启用页机制。这里设置全局页目录与页表项,并开启分页机制。

如果启用了 PAE,即物理地址扩展到 64G 的机制,不作分析。

不然,就是通常的 4G 线性地址空间。__PAGE_OFFSET 是内核编译时配置的内核地址空间偏移,默认为 3G。默认配置下,进程的用户态地址空间为 0~3G,高 1G 是内核地址空间。

全局页目录大小为 4KB,每项大小为 4B,可以表示 4MB 的线性范围,因此页目录的大小是 __PAGE_OFFSET >> 20

page_pde_offset = (__PAGE_OFFSET >> 20);

初始化页表首地址 %edi、全局页目录地址 %edxPTE 属性(页目录和页表的每项 4 Byte 中后 12 位是属性,这里预先填充 0x67

230         movl $pa(pg0), %edi

231         movl $pa(swapper_pg_dir), %edx

232         movl $PTE_ATTR, %eax

下面是一个双层循环,外层循环填充页目录,内层循环填充页表。

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

添加页目录项的最后一项,页表地址为 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)

有关对称多处理器(SMP)的处理

一些 CPU 参数相关的判断和处理

开启分页机制

# 将页表首地址(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 检查处理器类型

检查是 486 还是 386

l get vendor info

如果是 486,就 set AM, WP, NE, MP;如果是 386,就 set MP

l save PG, PE, ET

l check ET for 287/387

3.6.6 载入 GDTIDT

重新载入修改 GDT 后的段寄存器

l DS/ES 包含着默认用户段

清除 GSLDT

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函数在此不做讨论。

猜你喜欢

转载自blog.csdn.net/gaojy19881225/article/details/80018895