上一篇了解了elf文件,本篇继续Linux的征程。Linux的启动过程便是操作系统的构建过程,必须转换视角把Linux看作是一部精密的机器,自然的地Linux分解然后再重构。把Linux的启动过程看作是火箭的发射过程,点火升空,脱离一级助推,脱离二级助推,进入预定轨道,怠速运行…
一.构建
Linux是宏内核,是一个大型c程序。Linux编译后成为一个bzImage二进制文件。
linux-5.4.80/arch/x86/boot/Makefile
$(obj)/bzImage: $(obj)/setup.bin $(obj)/vmlinux.bin $(obj)/tools/build FORCE
$(call if_changed,image)
@$(kecho) 'Kernel: $@ is ready' ' (#'`cat .version`')'
由构建脚本可知bzImage由两部分构成setup.bin和vmlinu.bin构成,查看编译后的源码这两个文件位于boot目录下。那么这个setup.bin又是啥呢
linux-5.4.80/arch/x86/boot/Makefile
setup-y += a20.o bioscall.o cmdline.o copy.o cpu.o cpuflags.o cpucheck.o
setup-y += early_serial_console.o edd.o header.o main.o memory.o
setup-y += pm.o pmjump.o printf.o regs.o string.o tty.o video.o
setup-y += video-mode.o version.o
setup-$(CONFIG_X86_APM_BOOT) += apm.o
由编译脚本可知setup.bin是由boot目录下的c文件编译链接成的,setup.bin的作用是收集硬件信息,将CPU切换到保护模式,随着新的BIOS的出现,EFI的出现,收集信息的功能将通过启动协议由BootLoader来完成,setup.bin承载着内核与BootLoader的信息传递桥梁的功能。那么vmlinux.bin呢
linux-5.4.80/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)
由此可见从boot/compressed目录下的vmlinux去掉.comment段后拷贝过来并重命名为vmlinux.bin
linux-5.4.80/arch/x86/boot/compressed/Makefile
vmlinux.bin.all-y := $(obj)/vmlinux.bin
$(obj)/vmlinux.bin.gz: $(vmlinux.bin.all-y) FORCE
$(call if_changed,gzip)
$(obj)/vmlinux.bin.bz2: $(vmlinux.bin.all-y) FORCE
$(call if_changed,bzip2)
$(obj)/vmlinux.bin.lzma: $(vmlinux.bin.all-y) FORCE
$(call if_changed,lzma)
$(obj)/vmlinux.bin.xz: $(vmlinux.bin.all-y) FORCE
$(call if_changed,xzkern)
$(obj)/vmlinux.bin.lzo: $(vmlinux.bin.all-y) FORCE
$(call if_changed,lzo)
$(obj)/vmlinux.bin.lz4: $(vmlinux.bin.all-y) FORCE
$(call if_changed,lz4)
将vmlinux.bin压缩
linux-5.4.80/arch/x86/boot/compressed/Makefile
vmlinux-objs-y := $(obj)/vmlinux.lds $(obj)/head_$(BITS).o $(obj)/misc.o \
$(obj)/string.o $(obj)/cmdline.o $(obj)/error.o \
$(obj)/piggy.o $(obj)/cpuflags.o
将piggy.o等同vmlinux.bin的压缩文件链接成为新的vmlinux.bin文件,由此vmlinux.bin文件包含piggy.o等(称为非压缩部分吧)和vmlinux.bin.gz,那么vmlinux文件是什么
linux-5.4.80/Makefile
ifeq ($(KBUILD_EXTMOD),)
core-y += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/
vmlinux-dirs := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \
$(core-y) $(core-m) $(drivers-y) $(drivers-m) \
$(net-y) $(net-m) $(libs-y) $(libs-m) $(virt-y)))
vmlinux-alldirs := $(sort $(vmlinux-dirs) Documentation \
$(patsubst %/,%,$(filter %/, $(init-) $(core-) \
$(drivers-) $(net-) $(libs-) $(virt-))))
build-dirs := $(vmlinux-dirs)
clean-dirs := $(vmlinux-alldirs)
由最外层的Makefile可知vmlinux便是我们的本体,linux系统核心。由上面的分析可知Linux系统bzImage的构成。
bzImage = setup.bin(传递启动参数信息) + vmlinux.bin
vmlinux.bin = vmlinux.bin.gz(系统核心) + 非压缩部分(在启动时对vmlinux.bin.gz进行解压)
二.启动
内核代码经过make构建为bzImage,那么内核是如何启动的,要启动内核那么首先硬件机器得做好准备,其次内核代码得载入到内存中。关于硬件环境的建立我们有BIOS和启动引导程序(Bootloader),BIOS固化在硬件ROM上,硬件上电以后CPU执行BIOS程序进行内存,显卡等等的检测,接着BIOS在内存中建立中断向量表和中断服务程序,触发中断服务程序去读磁盘上的Bootloader载入到内存,接着执行Bootloader,BootLoader通过启动协议收集硬件信息和载入内核镜像bzImage到内存。
现在Linux这架火箭已经在内存中架好了,接下来准备起飞了。
火箭一级推进setup.bin开始执行,a20,bioscall,video等程序收集必要的启动参数,以video显示信息为例,收集参数到bootparams
linux-5.4.80/arch/x86/boot/video.c
static void store_video_mode(void)
{
struct biosregs ireg, oreg;
initregs(&ireg);
ireg.ah = 0x0f;
intcall(0x10, &ireg, &oreg);
//收集启动参数信息
boot_params.screen_info.orig_video_mode = oreg.al & 0x7f;
boot_params.screen_info.orig_video_page = oreg.bh;
}
火箭二级推进非压缩部分开始执行,对vmlinux.bin.gz进行解压得到vmlinux
linux-5.4.80/arch/x86/boot/compressed/head_64.S
ENTRY(startup_64)
//要么是从startup_32过来,要么是从64位bootloader跳过来的
//设置段寄存器都为0
xorl %eax, %eax
movl %eax, %ds
movl %eax, %es
movl %eax, %ss
movl %eax, %fs
movl %eax, %gs
......
//解压内核
pushq %rsi /* Save the real mode argument */
movq %rsi, %rdi /* real mode address */
leaq boot_heap(%rip), %rsi /* malloc area for uncompression */
leaq input_data(%rip), %rdx /* input_data */
movl $z_input_len, %ecx /* input_len */
movq %rbp, %r8 /* output target address */
movq $z_output_len, %r9 /* decompressed length, end of relocs */
call extract_kernel //跳转去解压内核,解压后的内核地址存在rax寄存器中
popq %rsi
//跳转到解压后的内核位置
jmp *%rax
......
linux-5.4.80/arch/x86/boot/compressed/misc.c
asmlinkage __visible void *extract_kernel(void *rmode, memptr heap,
unsigned char *input_data,
unsigned long input_len,
unsigned char *output,
unsigned long output_len)
{
const unsigned long kernel_total_size = VO__end - VO__text;
unsigned long virt_addr = LOAD_PHYSICAL_ADDR;
unsigned long needed_size;
/* Retain x86 boot parameters pointer passed from startup_32/64. */
boot_params = rmode;
/* Clear flags intended for solely in-kernel use. */
boot_params->hdr.loadflags &= ~KASLR_FLAG;
//解压缩内核
__decompress(input_data, input_len, NULL, NULL, output, output_len,
NULL, error);
//读取内核代码和数据段
parse_elf(output);
handle_relocations(output, output_len, virt_addr);
debug_putstr("done.\nBooting the kernel.\n");
return output;
}
从ELF格式的vmlinux读取代码和数据段并链接到编译时指定的物理内存处(必要时进行重定向),PT_LOAD代表代码和数据段
linux-5.4.80/arch/x86/boot/compressed/misc.c
static void parse_elf(void *output)
{
#ifdef CONFIG_X86_64
Elf64_Ehdr ehdr;
Elf64_Phdr *phdrs, *phdr;
#else
Elf32_Ehdr ehdr;
Elf32_Phdr *phdrs, *phdr;
#endif
void *dest;
int i;
phdrs = malloc(sizeof(*phdrs) * ehdr.e_phnum);
memcpy(phdrs, output + ehdr.e_phoff, sizeof(*phdrs) * ehdr.e_phnum);
for (i = 0; i < ehdr.e_phnum; i++) {
phdr = &phdrs[i];
switch (phdr->p_type) {
case PT_LOAD:
#ifdef CONFIG_X86_64 //对齐校验
if ((phdr->p_align % 0x200000) != 0)
error("Alignment of LOAD segment isn't multiple of 2MB");
#endif
#ifdef CONFIG_RELOCATABLE //重定向
dest = output;
dest += (phdr->p_paddr - LOAD_PHYSICAL_ADDR);
#else
dest = (void *)(phdr->p_paddr);
#endif
//搬移
memmove(dest, output + phdr->p_offset, phdr->p_filesz);
break;
default: /* Ignore other PT_* */ break;
}
}
free(phdrs);
}
接下来开始执行内核代码了,那么入口在哪,我们之前使用qume + eclipse + gdb搭建了调试环境调试的直接就是vmlinux那么在eclipse中我们可以找到就是startup_64,也是连接脚本指定的入口
最右边startup_64的地址ffffffff81000000也可以从vmlinux文件中找到,代码段的起始地址和startup_64是一致的
linux-5.4.80/arch/x86/kernel/head_64.S
.text
__HEAD
.code64
.globl startup_64
startup_64:
UNWIND_HINT_EMPTY
leaq _text(%rip), %rdi
pushq %rsi
call __startup_64
popq %rsi
/* Form the CR3 value being sure to include the CR3 modifier */
addq $(early_top_pgt - __START_KERNEL_map), %rax
jmp 1f
ENTRY(secondary_startup_64)
pushq %rsi
call __startup_secondary_64
popq %rsi
addq $(init_top_pgt - __START_KERNEL_map), %rax
......
pushq $.Lafter_lret # put return address on stack for unwinder
xorl %ebp, %ebp # clear frame pointer
movq initial_code(%rip), %rax
pushq $__KERNEL_CS # set correct cs
pushq %rax # target address in negative space
lretq //调用x86_64_start_kernel即initial_code
.Lafter_lret:
END(secondary_startup_64)
/* Both SMP bootup and ACPI suspend change these variables */
__REFDATA
.balign 8
GLOBAL(initial_code)
.quad x86_64_start_kernel
GLOBAL(initial_gs)
.quad INIT_PER_CPU_VAR(fixed_percpu_data)
GLOBAL(initial_stack)
linux-5.4.80/arch/x86/kernel/head64.c
asmlinkage __visible void __init x86_64_start_kernel(char * real_mode_data)
{
//一些清理动作
......
x86_64_start_reservations(real_mode_data);
}
void __init x86_64_start_reservations(char *real_mode_data)
{
......
start_kernel();
}
linux-5.4.80/init/main.c
asmlinkage __visible void __init start_kernel(void){}
不考虑其他启动细节的化,内核至此已启动一半,至于start_kernel里面cpu初始化,中断初始化,建立内存管理,进程管理,建立文件系统等等过于庞杂,留到后面分块来看。后面将以start_kernel各函数运行顺序为主线,依此展开,逐个击破。最后再由部分到整体来理解Linux,我想这是合理的阅读分析Linux的策略。面对庞然大物必须分而治之。