BIOS->GRUB
硬件工程师设计 CPU 时,硬性地规定在加电的瞬间,强制将 CS 寄存器的值设置为 0XF000,IP 寄存器的值设置为 0XFFF0,所以CS:IP 就指向了 0XFFFF0 这个物理地址。
这个物理地址上连接了ROM(只读内存),该ROM固化了BIOS程序,此时,BIOS启动,进入自检(加电自检)
当设备初始化和检查步骤完成之后,BIOS 会在内存中建立中断表和中断服务程序(实模式)
为了启动外部储存器中的程序,BIOS 会搜索可引导的设备。当然,Linux 通常是从硬盘中启动的。
硬盘上的第 1 个扇区(每个扇区 512 字节空间),被称为 MBR(主启动记录),其中包含有基本的 GRUB 启动程序和分区表,安装 GRUB 时会自动写入到这个扇区,当 MBR 被 BIOS 装载到 0x7c00 地址开始的内存空间中后,BIOS 就会将控制权转交给了 MBR。在当前的情况下,其实是交给了 GRUB。
GRUB 的加载分成了多个步骤,同时 GRUB 也分成了多个文件。一般MBR中只放置了boot.img,它能做的最重要的一个事情就是加载 grub2 的另一个镜像 core.img。
core.img 文件是由 GRUB 安装程序根据安装时环境信息,用其它 GRUB 的模块文件动态生成
如果是从硬盘启动的话,core.img 中的第一个扇区的内容就是 diskboot.img 文件。diskboot.img 文件的作用是,读取 core.img 中剩余的部分到内存中,最后将控制权交给 kernel.img 文件,最后是各个模块 module 对应的映像
正因为 GRUB2 大量使用了动态加载功能模块,这使得 core.img 文件的体积变得足够小。而 GRUB 的 core.img 文件一旦开始工作,就可以加载 Linux 系统的 vmlinuz 内核文件了。
在这之前,我们所有遇到过的程序都非常非常小,完全可以在实模式下运行,但是随着我们加载的东西越来越大,实模式这 1M 的地址空间实在放不下了,所以在真正的解压缩之前,lzma_decompress.img 做了一个重要的决定,就是调用 real_to_prot,切换到保护模式,这样就能在更大的寻址空间里面,加载更多的东西。
详解vmlinuz文件结构
内核映像文件vmlinuz
这个文件是怎么来的?其实它是由 Linux 编译生成的 bzImage 文件复制而来的。
生成 bzImage 文件需要三个依赖文件:setup.bin、vmlinux.bin,linux/arch/x86/boot/tools 目录下的 build
其中,build 只是一个 HOSTOS(正在使用的 Linux)下的应用程序,它的作用就是将 setup.bin、vmlinux.bin 两个文件拼接成一个 bzImage 文件。
setup.bin 文件是由 objcopy 命令根据 setup.elf 生成的, setup.bin 文件是由 /arch/x86/boot/ 目录下一系列对应的程序源代码文件编译链接产生(其中的 head.S 文件和 main.c 文件格外重要)
下面我们先看看 vmlinux.bin 是怎么产生的,构建 vmlinux.bin 的规则依然在 linux/arch/x86/boot/ 目录下的 Makefile 文件中
OBJCOPYFLAGS_vmlinux.bin := -O binary -R .note -R .comment -S$(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE $(call if_changed,objcopy)
这段代码的意思是,vmlinux.bin 文件依赖于 linux/arch/x86/boot/compressed/ 目录下的 vmlinux 目标
这里有必要提一下piggy.o文件,这也是被生成vmlinux的一个对象文件,他是由piggy.S生成的,但是该文件有些特殊,他的第一个依赖文件是suffix-y,它表示内核压缩方式对应的后缀。
vmlinux(elf格式,消除了文件的符号信息和重定位信息)经过工具软件压缩后变成gz格式,它把输出方式重定向到文件,从而产生 piggy.S 汇编文件,然后加入解压信息变成了新的vmlinux
其实,vmlinux 文件就是编译整个 Linux 内核源代码文件生成的
从_start到第一个进程
CPU是无法识别压缩文件中的指令的,这个时候就要用上setup.bin 文件了,_start 正是 setup.bin 文件的入口
setup.bin 大部分代码都是 16 位实模式下的
_start:main()
...
go_to_protected_mode();
}
...
protected_mode_jump(boot_params.hdr.code32_start, (u32)&boot_params + (ds() << 4));
}
protected_mode_jump 是个汇编函数,跳转到 boot_params.hdr.code32_start 中的地址,调用startup_32 函数
code32_start:
long 0x100000
需要注意的是,GRUB 会把 vmlinuz 中的 vmlinux.bin 部分,放在 1MB 开始的内存空间中。通过这一跳转,正式进入 vmlinux.bin 中。
startup_32 中需要重新加载段描述符,之后计算 vmlinux.bin 文件的编译生成的地址和实际加载地址的偏移,然后重新设置内核栈,检测 CPU 是否支持长模式,接着再次计算 vmlinux.bin 加载地址的偏移,来确定对其中 vmlinux.bin.gz 解压缩的地址
如果 CPU 支持长模式的话,就要设置 64 位的全局描述表,开启 CPU 的 PAE 物理地址扩展特性。再设置最初的 MMU 页表,最后开启分页并进入长模式,跳转到 startup_64
startup_64 函数中,初始化长模式下数据段寄存器,确定最终解压缩地址,然后拷贝压缩 vmlinux.bin 到该地址,跳转到 decompress_kernel 地址处,开始解压 vmlinux.bin.gz
.code64
.org 0x200
SYM_CODE_START(startup_64)
cld
cli
#初始化长模式下数据段寄存器
xorl %eax, %eax
movl %eax, %ds
movl %eax, %es
movl %eax, %ss
movl %eax, %fs
movl %eax, %gs
#……重新确定内核映像加载地址的代码略过
#重新初始化64位长模式下的栈
leaq rva(boot_stack_end)(%rbx), %rsp
#……建立最新5级MMU页表的代码略过
#确定最终解压缩地址,然后拷贝压缩vmlinux.bin到该地址
pushq %rsi
leaq (_bss-8)(%rip), %rsi
leaq rva(_bss-8)(%rbx), %rdi
movl $(_bss - startup_32), %ecx
shrl $3, %ecx
std
rep movsq
cld
popq %rsi
#跳转到重定位的Lrelocated处
leaq rva(.Lrelocated)(%rbx), %rax
jmp *%rax
SYM_CODE_END(startup_64)
.text
SYM_FUNC_START_LOCAL_NOALIGN(.Lrelocated)
#清理程序文件中需要的BSS段
xorl %eax, %eax
leaq _bss(%rip), %rdi
leaq _ebss(%rip), %rcx
subq %rdi, %rcx
shrq $3, %rcx
rep stosq
#……省略无关代码
pushq %rsi
movq %rsi, %rdi
leaq boot_heap(%rip), %rsi
#准备参数:被解压数据的开始地址
leaq input_data(%rip), %rdx
#准备参数:被解压数据的长度
movl input_len(%rip), %ecx
#准备参数:解压数据后的开始地址
movq %rbp, %r8
#准备参数:解压数据后的长度
movl output_len(%rip), %r9d
#调用解压函数解压vmlinux.bin.gz,返回入口地址
call extract_kernel
popq %rsi
#跳转到内核入口地址
jmp *%rax
SYM_FUNC_END(.Lrelocated)
上述代码中最后到了 extract_kernel 函数,它就是解压内核的函数
extract_kernel 函数根据 piggy.o 中的信息从 vmlinux.bin.gz 中解压出 vmlinux。(解压算法是编译内核的配置选项决定的)
Linux 内核入口
这个 startup_64 函数定义在 linux/arch/x86/kernel/head_64.S 文件中,它是内核的入口函数
当SMP系统加电之后,总线仲裁机制会选出多个 CPU 中的一个 CPU,称为 BSP,也叫第一个 CPU,直接执行 secondary_startup_64 函数,最后就会调用 x86_64_start_kernel 函数,最后调用了 x86_64_start_reservations 函数,其中处理了平台固件相关的东西
start_kernel 函数中调用了大量 Linux 内核功能的初始化函数
void start_kernel(void){
char *command_line;
char *after_dashes;
cgroup_init_early();
local_irq_disable();
setup_arch(&command_line);
setup_log_buf(0);
sort_main_extable();
trap_init();
mm_init();
ftrace_init();
sched_init();
workqueue_init_early();
rcu_init();
early_irq_init();
init_IRQ();
tick_init();
rcu_init_nohz();
init_timers();
hrtimers_init();
softirq_init();
timekeeping_init();
mem_encrypt_init();
setup_per_cpu_pageset();
fork_init();
proc_caches_init();
uts_ns_init();
buffer_init();
key_init();
security_init();
vfs_caches_init();
pagecache_init();
signals_init();
arch_call_rest_init();
}
我们只关注一个 arch_call_rest_init 函数
void __init __weak arch_call_rest_init(void){
rest_init();
}
noinline void __ref rest_init(void){ struct task_struct *tsk;
int pid;
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
}
总结
写了挺多了,来总结一下吧,要不然真的看着挺混乱的。
一副经典的图 这篇文章是一步步分析的,所以我来个时间上的先后顺序总结。
首先是一份Linux内核源码,形成了setup.bin和vmlinux.bin文件,经过build后形成了vmlinuz内核映像文件,此时还是放在硬盘中。
当BIOS把控制权交给grub后(前面的过程就不做概述了),此时计算机是实模式,内存较小,因此只能加载boot.img文件,该文件加载core.img,后面再加载了各个img文件,其中有kernel.img文件,该文件加载vmlinuz文件;此时lzma_decompress.img让计算机进入保护模式。
GRUB 加载 vmlinuz 文件之后,会把控制权交给 vmlinuz 文件的 setup.bin 的部分中 _start,它会设置好栈,清空 bss,设置好 setup_header 结构,调用 16 位 main 切换到保护模式,最后跳转到 1MB 处的 vmlinux.bin 文件中。
从 vmlinux.bin 文件中 startup32、startup64 函数开始建立新的全局段描述符表和 MMU 页表,切换到长模式下解压 vmlinux.bin.gz。释放出 vmlinux 文件之后,由解析 elf 格式的函数进行解析,释放 vmlinux 中的代码段和数据段到指定的内存。然后调用其中的 startup_64 函数,在这个函数的最后调用 Linux 内核的第一个 C 函数。
Linux 内核第一个 C 函数重新设置 MMU 页表,随后便调用了最有名的 start_kernel 函数, start_kernel 函数中调用了大多数 Linux 内核功能性初始化函数,在最后调用 rest_init 函数建立了两个内核线程,在其中的 kernel_init 线程建立了第一个用户态进程。
|