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

前言

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

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

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

3.1 内核映像的组成
在讨论内核构建前,我们先来简单了解一下内核映像的组成,如图3-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. */
cmpb
gal, 3f
je 1f
movb
gal, 3f
jmp 1f
/* Synchronize pipeline */
1:
...
.byte
Oxcd
/* INT opcode */
3: .byte
在代码中我们并没有看到熟悉的调用BIOS中断的身影,如“int$0x10”,但是我们看到了一个特殊的字符——Oxcd。正如其后面的注释所言,Oxcd就是x86汇编指令INT的机器码,如表3-1所示。

根据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 RELOCATABLE
dest = output;

dest += (phdr->p_paddr - LOAD_PHYSICAL_ADDR);
#else
dest = (void *) (phdr->p_paddr);
#endif
memcpy (dest, output + phdr->p_offset, phdr->p_filesz);
break;
default:/*Ignore other PT_**/break;
HZ BOOKS
free(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)

总结

(0)

相关推荐

  • 从创建进程到进入main函数,发生了什么?

    前几天,读者群里有小伙伴提问:从进程创建后,到底是怎么进入我写的main函数的? 今天这篇文章就来聊聊这个话题. 首先先划定一下这个问题的讨论范围:C/C++语言 这篇文章主要讨论的是操作系统层面上对 ...

  • UC头条:linux应用程序控制底层硬件原理解析

    #defineMYMAJOR200#defineMYNAME'testchar'#defineGPJ0CONS5PV210_GPJ0CON#defineGPJ0DATS5PV210_GPJ0DAT#d ...

  • Linux系统Shell脚本如何运行?linux操作系统

    Shell脚本语言很适合用于处理纯文本类型的数据,而Linux系统中几乎所有的配置文件.日志文件以及绝大多数的启动文件都是纯文本类型的文件.因此,学好Shell脚本语言,能够更好的操作Linux系统. ...

  • 什么是Linux操作系统?Linux系统的特点

    无论工作还是学习,我们必然会接触到操作系统,而且现在操作系统发展成熟.稳定,种类也有很多,其中包含Windows.Linux.Unix等,Windows不用多说想必大家都很熟悉,Linux与Unix类 ...

  • 文库构建技术原理

    基因文库用途广泛,如用于人类及动植物基因组学研究,基因表达调控研究,分析.分离特定的基因片段等.文库已经越来越多地应用于研究领域,如高通量药物靶点筛选,蛋白质工程定向进化,基因疫苗.抗体改造,生物遗传 ...

  • cDNA文库构建技术原理

    cDNA文库,是指含一种生物体所有基因编码的cDNA分子的克隆群.以mRNA为模板,经反转录酶催化,在体外反转录成cDNA,与适当的载体(常用噬菌体或质粒载体)连接后转化受体菌,则每个细菌含有一段cD ...

  • gRNA文库构建技术原理

    CRlSPR/Cas9技术是基因编辑历史上的又一里程碑.该技术通过gRNA指导Cas核酸酶对靶向基因进行特定DNA修饰.gRNA文库是药物筛选或靶向筛选特定通路的理想工具,gRNA文库的建立将在功能基 ...

  • 基因突变体文库构建技术原理

    分子定向进化模拟自然选择过程,改变原有蛋白的氨基酸序列,以期获得具有功能的突变蛋白.基因突变文库是DNA变体序列的组合,是基因合成.基因突变和定向进化研究相结合的产物.基因突变文库已经越来越多地应用于 ...

  • 裸鼠肝癌模型构建实验技术原理

    裸鼠肝癌模型构建:取对数生长期细胞消化后离心,定量的PBS缓冲液及培养基将细胞吹打成3个不同细胞浓度组.将裸小鼠分为6组,每组10只,将以PBS缓冲液混悬的细胞和以完全培养基混悬的细胞分别接种在小鼠左 ...

  • 荷瘤鼠动物模型构建实验技术原理

    (1)荷瘤鼠动物模型构建方法:皮下成瘤模型:把肿瘤细胞接种到裸鼠的前肢腋下或背部皮下等位置,观察成瘤结果,是肿瘤成瘤研究中*常用的模型.裸鼠是具有一定缺陷的小鼠,利用这一特点可以将肿瘤细胞移植到裸鼠皮 ...