0
点赞
收藏
分享

微信扫一扫

从零开始搭建一个操作系统(4):BIOS->GRUB->Linux内核入口


Linux初始化过程

  • ​​BIOS->GRUB​​
  • ​​详解vmlinuz文件结构​​
  • ​​从_start到第一个进程​​
  • ​​Linux 内核入口​​
  • ​​总结​​

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 的模块文件动态生成

从零开始搭建一个操作系统(4):BIOS->GRUB->Linux内核入口_linux


如果是从硬盘启动的话,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 文件中

#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()

...
//进入CPU保护模式,不会返回了
go_to_protected_mode();
}

...
//保护模式下长跳转到boot_params.hdr.code32_start
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;
//CPU组早期初始化
cgroup_init_early();
//关中断
local_irq_disable();
//ARCH层初始化
setup_arch(&command_line);
//日志初始化
setup_log_buf(0);
sort_main_extable();
//陷阱门初始化
trap_init();
//内存初始化
mm_init();
ftrace_init();
//调度器初始化
sched_init();
//工作队列初始化
workqueue_init_early();
//RCU锁初始化
rcu_init();
//IRQ 中断请求初始化
early_irq_init();
init_IRQ();
tick_init();
rcu_init_nohz();
//定时器初始化
init_timers();
hrtimers_init();
//软中断初始化
softirq_init();
timekeeping_init();
mem_encrypt_init();
//每个cpu页面集初始化
setup_per_cpu_pageset();
//fork初始化建立进程的
fork_init();
proc_caches_init();
uts_ns_init();
//内核缓冲区初始化
buffer_init();
key_init();
//安全相关的初始化
security_init();
//VFS数据结构内存池初始化
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;
//建立kernel_init线程
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
//建立khreadd线程
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
}

总结

写了挺多了,来总结一下吧,要不然真的看着挺混乱的。

一副经典的图

从零开始搭建一个操作系统(4):BIOS->GRUB->Linux内核入口_加载_02


这篇文章是一步步分析的,所以我来个时间上的先后顺序总结。

首先是一份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 线程建立了第一个用户态进程。


举报

相关推荐

0 条评论