Linux启动流程

上一篇了解了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的策略。面对庞然大物必须分而治之。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值