Skip to main content
 Web开发网 » 操作系统 » linux系统

深度探索Linux操作系统:系统构建和原理解析

2021年10月13日9290百度已收录

前言总结送福利 看完记得转发哦 帮助更多学习的朋友

对于编译内核而言,一条make命令就足够了。构建内核最困难的地方不是编译,而是编译前的配置。配置内核时,通常我们都能找到一些参考。

比如,对于桌面系统,可以参考主流发行版的内核配置,比如,对于嵌入式系统,BSP(Board Support Package)中通常也提供内核,但他们通常也仅是个可以工作的内核而已,如果要一个占用空间更小、运行更快的内核,就需要开发人员手动配置内核,也确实存在着在某些情况下,我们找不到任何合适的参考,这时我们只能以手动方式从零开始配置。

构建内核内核的构建系统kbuild基于GNUMake,是一套非常复杂的系统。我们本无意着太多笔墨来分析kbuild,因为作为开发者可能永远不需要去改动内核映像的构建过程,但是了解这一过程,无论是对学习内核,还是进行内核开发都有诸多帮助。所以在构建内核之前,本章首先讨论了内核的构建过程。

3.1 内核映像的组成在讨论内核构建前,我们先来简单了解一下内核映像的组成,如图3-1所示。

深度探索Linux操作系统:系统构建和原理解析  linux系统 第1张

如果将内核的映像比作航天器,则setup.bin部分就类似于火箭的一级推进子系统,负责将内核加载进内存,并为后面内核保护模式的运行建立基本的环境。加载内核的功能被分离到Bootloader中,setup.bin则退化为辅助Bootloader将内核加载到内存,包围在32位保护模式部分外的是非解压缩部分。可以看作是火箭的二级推进子系统,将压缩的内核解压到合适的位置,并进行内核重定位,在完成这个环节后,其从内核映像脱离。内核的32位保护模式部分vmlinux。相当于航天器的有效载荷,最后运行的卫星或者宇宙飞船,只有留在轨道内(内存中)运行。内核构建时,将对有效载荷vmlinux进行压缩,然后与二级推进系统装配为vmlinux.bin。下面我们就来看看内核映像的各个组成部分。

3.1.1 一级推进系统-setup.bin在进行内核初始化时,需要一些信息,如显示信息、内存信息等。曾经,这些信息由工作在实模式下的setup.bin通过BIOS获取,保存在内核中的变量boot_params中,变量boot_params 是结构体 boot params 的一个实例。如setup.bin 中收集显示信息的代码如下:

1inux-3.7.4/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;store_video_mode首先调用函数intcall获取显示方面的信息,并将其保存在boot_params的screen_info中。intcall是调用BIOS中断的封装,0x10是BIOS提供的显示服务(Video Service)的中断号,代码如下:linux-3.7.4/arch/x86/boot/bioscall.s:intcall:/* Self-modify the INT instruction. Ugly, but works. */cmpbgal, 3fje 1fmovbgal, 3fjmp 1f/* Synchronize pipeline */1:....byteOxcd/* INT opcode */3: .byte在代码中我们并没有看到熟悉的调用BIOS中断的身影,如“int$0x10”,但是我们看到了一个特殊的字符——Oxcd。正如其后面的注释所言,Oxcd就是x86汇编指令INT的机器码,如表3-1所示。

深度探索Linux操作系统:系统构建和原理解析  linux系统 第2张

根据x86的INT指令说明,Oxcd后面跟着的1字节就是BIOS中断号,这就是上面代码中标号为3处分配1字节的目的。0函数intcall的开头,比较寄存器al中的值与标号3处占用的1字节,若直接向前跳转至标号1处,否则将寄存器al中的值复制到标号3处的1个字节空间。那么寄存器al中保存的是什么呢?默认情况下,GCC使用树来传递参数。可以使用“_attribute_(regparm(n)”修饰函数,或者通过向GCC传递命令行参数“-mregparm=n”来指定GCC使用寄存器传递参数,其中n表示使用寄存器传递参数的个数。在编译setup.bin时,kbuild使用了后者,编译脚本如下所示:

linux-3.7.4/arch/x86/boot/Makefile:KBUILD_CFLAGS:=...-mregparm=3...如此,函数的第一个参数通过寄存器eax/ax传递,第二个参数通过ebx/bx传递,等等,而不是通过树传递了。因此,上面的寄存器al中保存的是函数intcall的第一个参数,即BIOS中断号。在完成信息收集后,setup.bin将CPU切换到保护模式,并跳转到内核的保护模式部分执行。如我们前面讨论的,setup.bin作为一级推进系统,即将结束历史使命,所以内核将setup.bin收集的保存在setup.bin的数据段的变量boot_params复制到vmlinux的数据段中。

随着BIOS标准的出现,尤其是EFI的出现,为了支持这些新标准,开发者们制定了32位启动协议(32-bit boot protocol)。在32位启动协议下,由Bootloader实现收集这些信息的功能,内核启动时不再需要首先运行实模式部分(即setup.bin),而是直接跳转到内核的保护模式部分。因此,在32位启动协议下,不再需要setup.bin收集内核初始化时需要的相关信息。但是这是否意味着可以彻底放弃setup.bin呢?

二级推进系统-内核非压缩部分

内核经过压缩,因此运行前需要解压缩,但是谁来负责内核映像的解压呢?解铃还须系铃人,既然内核在构建时自己压缩了自己,当然解压缩也要由内核映像自己完成。除了解压以外,非压缩部分还负责内核重定位。内核可以配置为可重定位的(relocatable),所谓可重定位即内核可以被Bootloader加载到内存任何位置。但是在链接内核时,链接器需要假定一个加载地址,然后以这个假定地址为参考,为各个符号分配运行时地址。显然,如果加载地址和链接时假定的地址不同,那么需要对符号的地址进行重新修订,这就是内核重定位。内核非压缩部分工作在保护模式下,其占用的内存在完成使命后将会被释放。

有效载荷-vmlinux

kbuild分别构建内核各个子目录中的目标文件,然后将它们链接为vmlinux。为了缩小内核体积,kbuild删除了vmlinux中一些不必要的信息,并将其命名为vmlinux.bin,最后将vmlinux.bin压缩为vmlinux.bin.gz。那么为什么内核要进行压缩呢?

1.最初,因为在某些体系架构上,特别是1386,系统启动时运行于实模式状态,可以寻址空间只能在1MB以下,内核尺寸过大,将无法正常加载,因此,对内核进行了压缩。

2.另外一个原因是,2.4及更早版本的内核,需要可以容纳在一张软盘上,所以内核也要进行压缩。

映像的格式

Linux作为操作系统的hosted environment环境下,二进制文件使用ELF格式,操作系统也提供ELF文件的加载器。但是,操作系统本身确是工作在freestanding environment 环境下。操作系统显然不能强制要求 Bootloader 也提供ELF加载器。

但是,从Linux 2.6.26版本开始,内核的压缩部分,即有效载荷部分,采用了ELF格式。至于为什么采用ELF格式,Patch的提交者给出了原因:This allows other boot loaders such as the Xen domain builder the opportunity to extract the ELF file.

当内核映像不是裸二进制格式时,我们需要有一个ELF加载器来将ELF格式的内核映像转化为裸二进制格式。那么谁来充当这个ELF加载器呢?正所谓“蜘蜂捕蝉,黄雀在后”。内核的非压缩部分调用函数decompress解压内核后,紧接着就调用了函数parse_elf来处理ELF格式的内核映像,代码如下:

11nux-3.7.4/arch/x86/boot/compressed/misc.c:agml inkage void decompress kernel (...)decompress (input_data, input_len, ...);parse_elf(output);static void parse_elf (void *output)for (i - 0; i < ehdr.e_phnum; i++) {phdr = &phdrs [i];switch (phdr->p_type) {case PT_LOAD:#ifdef CONFIG RELOCATABLEdest = output;

dest += (phdr->p_paddr - LOAD_PHYSICAL_ADDR);#elsedest = (void *) (phdr->p_paddr);#endifmemcpy (dest, output + phdr->p_offset, phdr->p_filesz);break;default:/*Ignore other PT_**/break;HZ BOOKSfree(phdrs);在ELF文件中,存放代码和数据的段的类型是PT_LOAD,因此,仅处理这个类型的段即可。在函数parse_elf中,对于类型是PT_LOAD的段,其按照Program Header Table中的信息,将它们移动到链接时指定的物理地址处,即p_paddr。当然,如果内核是可重定位的,还要考虑内核实际加载地址与编译时指定的加载地址的差值。如果Bootloader不是所谓的“the Xen domain builder”,我们完全没有必要保留内核的压缩部分为ELF格式,并略去启动时进行的“parse_elf”。具体方法如下:(1)将压缩部分链接为裸二进制格式将传递给命令objcopy的参数追加“-Obinary”,如下面使用黑体标识的部分:1inux-3.7.4/arch/x86/boot/compressed/Makefile:OBJCOPYFLAGS_vmlinux.bin:=-R.comment-s-o binary$(obj)/vmlinux.bin:vmlinux FORCE$(call if_changed,objcopy)

总结掉parse_elf 记得转发哦 可帮助更多朋友学习既然内核压缩部分已经是裸二进制格式的了,解压后自然不再需要调用函数parse_elf了

关注+后台私信;资料;两个字可以免费领取 资料内容包括:C/C++,Linux,golang,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,嵌入式 等。。。

深度探索Linux操作系统:系统构建和原理解析  linux系统 第3张

评论列表暂无评论
发表评论
微信