《linux系统内核设计与实现》第二章-从内核触发

时间:2024-04-18 07:21:26

2.1 获取内核源码

登录Linux 内核官方网站http://www.kernel.org,可以随时获取当前版本的Linux源代码,可以是完整的压缩形式(使用tar命令创建的一个压缩文件),也可以是增量补丁形式。除特殊情况下需要Linux 源码的旧版本外一般都希望拥有最新的代码。kernel.org 是源码的库存之处,那些领导潮流的内核开发者所发布的增量补丁也放在这里。

2.1.1 使用git
git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git

这个文件很大,笔者这里使用git很慢

于是乎,在Index of /pub/linux/kernel/网址下找到2.6.0版本的压缩包

书中推荐的是2.6.0,在安装的时候,遇到各种问题,于是乎,换了5.0.1,下面这些问题是2.6.0的时候碰到的。linux是ubuntu22.4

2.1.2 安装内核源代码

如果压缩形式是bzip2,则运行

tar xvjf linux-x.y.z.tar.bz2

如果压缩形式是GNU的zip,则运行:

tar xvzf linux-x.y.z.tar.gz

解压后的源代码位于linux-x.y.z.目录下。如果你是使用git获取和管理内核源代码,那么就不需要下载压缩文件,只要像前面描述的那样运行git clone命令,git就会下载并且解压最新的源代码。

何处安装并触及源码
内核源码一般安装在/usr/src/linux目录下。但请注意,不要把这个源码树用于开发,因为编译你的C库所用的内核版本就链接到这棵树。此外,不要以root身份对内核进行修改,而应当是建立自己的主目录,仅以root身份安装新内核。即使在安装新内核时,/usr/src/linux目录都应当原封不动。

2.1.3 使用补丁

在Linux内核社区中,补丁是通用语。你可以以补丁的形式发布对代码的修改,也可以以补丁的形式接收其他人所做的修改。增量补丁可以作为版本转移的桥梁。你不再需要下载庞大的内核源码的全部压缩,而只需给旧版本打上一个增量补丁,让其旧貌换新颜。这不仅节约了带宽,还省了时间。要应用增量补丁,从你的内部源码树开始,只需运行:

patch -pl < ../patch-x.y.z

一般来说,一个给定版本的内核补丁总是打在前一个版本上。

2.2 内核源码树

在源码树根目录中,COPYING是内核许可证,CREDITS是开发者列表。MAINTAINERS是维护者列表,维护内核子系统和驱动程序。最后,Makefile是Makefile内核的基础

2.3 编译内核

2.3.1 配置内核

在编译内核之前,首先你必须配置它。由于内核提供了数不胜数的功能,支持了难以计数的硬件,因而有许多东西需要配置。可以配置的各种选项,以CONFIG_FEATURE 形式表示,其前缀为CONFIG。例如,对称多处理器(SMP)的配置选项为CONFIG_SMP。如果设置了该选项,则SMP启用,否则,SMP不起作用。配置选项既可以用来决定哪些文件编译进内核,也可以通过预处理命令处理代码。

这些配置项要么是二选一,要么是三选一。二选一就是yes或no。比如CONFIG_PREEMPT就是二选一,表示内核抢占功能是否开启。三选一可以是yes、no或module。module,意味着该配置项被选定了,但编译的时候这部分功能的实现代码是以模块(一种可以动态安装的独立代码段〉的形式生成。在三选一的情况下,显然yes选项表示把代码编译进主内核映像中,而不是作为一个模块。驱动程序一般都用三选一的配置项。

内核提供了各种不同的工具来简化内核配置。最简单的一种是一个字符界面下的命令行工具:

这里需要提前安装make、gcc、bison和 

sudo apt-get install make gcc bison flex

然后再

make config

出现问题了:cc1: error: code model kernel does not support PIC mode

cc1: error: code model kernel does not support PIC mode
make[1]:***[scripts/Makefile.build:175: scripts/empty.o] Error 1
make:***[Makefile:312: scripts] Error 2

参考:编译Linux内核时出现“code model kernel does not support PIC mode”的解决方法-****博客 修改Makefile,添加-fno-pie

继续make config

scripts/kconfig/mconf.c:91:21: error: static declaration of 'current_menu'’follows non-static declaration
91 | static struct menu *current_menu;

参考:linux内核编译_make menuconfig报错(current_menu error)_make menuconfig失败-****博客 注释掉这行

scripts/kconfig/mconf.c:91

继续make config,一直yes即可。

该工具会逐一遍历所有配置项,要求用户选择yes、no或是module(如果是三选一的话)。由于这个过程往往要耗费掉很长时间,所以,除非你的工作是按小时计费的,否则应该多利用基于ncurse库编制的图形界面工具:

make menuconfig

或者,是用基于gtk+的图形工具:

make gconfig

这两个都没有成功......

这三种工具将所有配置项分门别类放置,比如按“处理器类型和特点”。你可以按类移动、浏览内核选项,当然也可以修改其值。

这条命令会基于默认的配置为你的体系结构创建一个配置:

make defconfig

尽管这些缺省值有点随意性(在i386上,据说那就是Linus的配置),但是,如果你从未配置过内核,那它们会提供一个良好的开端。赶快行动吧,运行这条命令,然后回头看看,确保为你的硬件所配置的选项是启用的。

这些配置项会被存放在内核代码树根目录下的.config文件中。你很容易就能找到它(内核开发者差不多都能找到),并且可以直接修改它。

在这里面查找和修改内核选项也很容易。在你修改过配置文件之后,或者在用已有的配置文件配置新的代码树的时候,你应该验证和更新配置:

make oldconfig

事实上,在编译内核之前你都应该这么做。

配置选项CONFIG_IKCONFIG_PROC把完整的压缩过的内核配置文件存放在/proc/config.g下,这样当你编译一个新内核的时候就可以方便地克隆当前的配置。如果你目前的内核已经启用了此选项,就可以从/proc下复制出配置文件并且使用它来编译一个新内核:

zcat /proc/ config.gz > .config
make oldconfig

一旦内核配置好了(不论你是如何配置的),就可以使用一个简单的命令来编译它了:

这里往下是5.0.1版本,5.0.1没有上面这些问题。

make

遇到问题了

In file included from scripts/selinux/genheaders/genheaders.c:19:
./security/selinux/include/classmap.:249:2: error: #error Mew address family def ined,please update secclass_map.249 
#error New address family defined,please update secclass_map.

参考:内核编译问题#error New address family defined, please update secclass_map#multiple definition of yylloc-****博客

解决了,继续make,遇到问题了

scripts/extract-cert.c:21:10: fatal error: openssl/bio.h: No such file or directory
21 |#include <openssl/bio.h>

参考: fatal error: openssl/bio.h: No such file or directory 解决方案-****博客

安装

sudo apt install libssl-dev

解决了,继续make,遇到问题了

error: Cannot generate ORC metadata for CONFITG UNMITNDER ORC=y,please install libelf-dev, 1ibelf-devel or elfutils-libelf-devl
make:***[Makefile:1111: prepare-objtool] Error 1

参考:Cannot generate ORC metadata for CONFIG_UNWINDER_ORC=y, please install libelf-dev-****博客

解决了,继续make,遇到问题了

arch/x86/entry/vdso/vclock_gettime.c: In function ' _vdso_clock_gettime ' :
arch/x86/entry/vdso/vclock_gettime.c:184:1: error:  -mindirect-branch' and ' -fcf-protection’are not compatible

参考:Ubuntu 20.04 gcc9 linux-sgx v2.9出现-fcf-protection -mfunction-return冲突-****博客

其实就是需要gcc-8的版本,笔者这里gcc-8安装不上,参考:https://www.cnblogs.com/devilmaycry812839668/p/17134759.html

再将gcc-8设置成默认编译器,参考:ubuntu16.04 安装gcc8,g++8_e: 未发现软件包 gcc-8-****博客

继续make,遇到问题了

arch/x86/entry/entry_64.S:1663: Warning: no instruction mnemonic suffix given and no register operands; using default for `sysret'
  AS      arch/x86/entry/thunk_64.o
arch/x86/entry/thunk_64.o: warning: objtool: missing symbol table
make[2]: *** [scripts/Makefile.build:367: arch/x86/entry/thunk_64.o] Error 1
make[2]: *** Deleting file 'arch/x86/entry/thunk_64.o'
make[1]: *** [scripts/Makefile.build:492: arch/x86/entry] Error 2
make: *** [Makefile:1043: arch/x86] Error 2

参考:Ubuntu22.04编译linux内核5.4.34失败 - 知乎 (zhihu.com) 和objtool: Don't fail on missing symbol table · Pull Request !141 · openEuler/kernel - Gitee.com

修改代码, tools/objtool/elf.c

继续make,成功,这里要等较长时间

2.3.2 减少编译的垃圾信息

如果你想尽量少地看到垃圾信息,却又不希望错过错误报告与警告信息的话,你可以用以下命令来对输出进行重定向:

make > .. /detritus

一旦你需要查看编译的输出信息,你可以查看这个文件。不过,因为错误和警告都会在屏幕上显示,所以你需要看这个文件的可能性不大。事实上,我只不过输入如下命令:

make > /dev/null

就可把无用的输出信息重定向到永无返回值的黑洞/dev/null。

2.3.3 衍生多个编译作业

默认情况下,make只衍生一个作业,因为Makefiles常会出现不正确的依赖信息。对于不正确的依赖,多个作业可能会互相踩踏,导致编译过程出错。当然,内核的Makefiles没有这样的编码错误,因此衍生出的多个作业编译不会出现失败。为了以多个作业编译内核,使用以下命令:

make -jn

这里,n是要衍生出的作业数。在实际中,每个处理器上一般衍生出一个或者两个作业。例如,在一个16核处理器上,你可以输入如下命令:

make -j32 > /dev/nu1l

利用出色的distcc或者ccache 工具,也可以动态地改善内核的编译时间。

2.3.4 安装新内核

在内核编译好之后,你还需要安装它。怎么安装就和体系结构以及启动引导工具(bootloader)息息相关了——查阅启动引导工具的说明,按照它的指导将内核映像拷贝到合适的位置,并且按照启动要求安装它。一定要保证随时有一个或两个可以启动的内核,以防新编译的内核出现问题。        

所幸,模块的安装是自动的,也是独立于体系结构的。以root身份,只要运行:

sudo make modules_install

就可以把所有已编译的模块安装到正确的主目录/lib/modules 下。

接下来

sudo  make install

编译时也会在内核代码树的根目录下创建一个System.map文件。这是一份符号对照表,用以将内核符号和它们的起始地址对应起来。调试的时候,如果需要把内存地址翻译成容易理解的函数名以及变量名,这就会很有用。

查看现在使用的内核版本

uname -r

启用指定内核作为引导

输入下列命令将内核作为引导,将数字更改为你自己编译的版本号:

sudo  update-initramfs -c -k 5.0.1

更新grub:

sudo update-grub

之后重启即可在启动界面选择需要重启的内核。

如果看不到重启选择界面,执行以下操作,输入命令:

sudo  vim /etc/default/grub

注释掉hidden那一行,将timeout更改为较大值,这里改为了10

之后执行

sudo update-grub

重启即可看到下面此单,选择高级选项,进入后选择编译安装好的内核进入即可,

选择刚刚编译好的内核,5.0.1,倒数第4个

卡着不动了

2.4 内核开发的特点

最重要的差异包括以下几种:

  • 内核编程时既不能访问C库也不能访问标准的C头文件。·内核编程时必须使用GNU C.
  • 内核编程时缺乏像用户空间那样的内存保护机制。
  • 内核编程时难以执行浮点运算。
  • 内核给每个进程只有一个很小的定长堆栈。
  • 由于内核支持异步中断、抢占和SMP,因此必须时刻注意同步和并发。
  • 要考虑可移植性的重要性。

2.4.1 无libc库亦或无标准头文件

与用户空间的应用程序不同,内核不能链接使用标准C函数库——或者其他的那些库也不行。造成这种情况的原因有许多,其中就包括先有鸡还是先有蛋这个悖论。不过最主要的原因还是速度和大小。对内核来说,完整的C库─—哪怕是它的一个子集,都太大且太低效了。

别着急,大部分常用的C库函数在内核中都已经得到了实现。比如操作字符串的函数组就位于lib/string.c文件中。只要包含<linux/string.h>头文件,就可以使用它们。

头文件
当我在本书中谈及头文件时,都指的是组成内核源代码树的内核头文件。内核源代码文件不能包含外部头文件,就像它们不能用外部库一样。
基本的头文件位于内核源代码树*目录下的include目录中。例如,头文件<linux/inotify.h>对应内核源代码树的include/linux/inotify.h。
体系结构相关的头文件集位于内核源代码树的arch/<architecture>/include/asm目录下。例如,如果编译的是x86体系结构,则体系结构相关的头文件就是arch/x86/include/asm。内核代码通过以asm/为前缀的方式包含这些头文件,例如<asm/ioctl.h>。

在所有没有实现的函数中,最著名的就数printf()函数了。内核代码虽然无法调用printf(),但它提供的printk()函数几乎与printf()相同。printk()函数负责把格式化好的字符串拷贝到内核日志缓冲区上,这样,syslog程序就可以通过读取该缓冲区来获取内核信息。printk()的用法很像printf() :

printk("Hello world! A string :'%s' and an integer : '%d '\n", str, i);
2.4.2 GNU C

像所有自视清高的Unix内核一样,Linux内核是用C语言编写的。让人略感惊讶的是,内核并不完全符合ANSI C标准。实际上,只要有可能,内核开发者总是要用到gcc提供的许多语言的扩展部分。( gcc是多种GNU编译器的集合,它包含的C编译器既可以编译内核,也可以编译Linux 系统上用C语言写的其他代码。)

1 内联函数(inline)

2 内联汇编

3 分支声明

2.4.3 没有内存保护机制

如果一个用户程序试图进行一次非法的内存访问,内核就会发现这个错误,发送SIGSEGV信号,并结束整个进程。然而,如果是内核自己非法访问了内存,那后果就很难控制了。(毕竟,有谁能照顾内核呢?)内核中发生的内存错误会导致oops,这是内核中出现的最常见的一类错误。在内核中,不应该去做访问非法的内存地址,引用空指针之类的事情,否则它可能会死掉,却根本不告诉你一声—-在内核里,风险常常会比外面大一些。

此外,内核中的内存都不分页。也就是说,你每用掉一个字节,物理内存就减少一个字节。所以,在你想往内核里加入什么新功能的时候,要记住这一点。

2.4.4 不要轻易在内核中使用浮点数

在用户空间的进程内进行浮点操作的时候,内核会完成从整数操作到浮点数操作的模式转换。在执行浮点指令时到底会做些什么,因体系结构不同,内核的选择也不同,但是,内核通常捕获陷阱并着手于整数到浮点方式的转变。

与用户空间进程不同,内核并不能完美地支持浮点操作,因为它本身不能陷入。在内核中使用浮点数时,除了要人工保存和恢复浮点寄存器,还有其他一些琐碎的事情要做。如果要直截了当地回答,那就是:别这么做了,除了一些极少的情况,不要在内核中使用浮点操作。

2.4.5 容积小而固定的栈

用户空间的程序可以从栈上分配大量的空间来存放变量,甚至巨大的结构体或者是包含数以千计的数据项的数组都没有问题。之所以可以这么做,是因为用户空间的栈本身比较大,而且还能动态地增长(年长的开发者回想一下DOS那个年代,这种低级的操作系统即使在用户空间也只有固定大小的栈)。

内核栈的准确大小随体系结构而变。在x86上,栈的大小在编译时配置,可以是4KB也可以是8KB。从历史上说,内核栈的大小是两页,这就意味着,32位机的内核栈是8KB,而64位机是16KB,这是固定不变的。每个处理器都有自己的栈。

2.4.6 同步和并发

内核很容易产生竞争条件。和单线程的用户空间程序不同,内核的许多特性都要求能够并发地访向共享数据,这就要求有同步机制以保证不出现竞争条件,特别是:

  • Linux是抢占多任务操作系统。内核的进程调度程序即兴对进程进行调度和重新调度。内核必须和这些任务同步。
  • Linux内核支持对称多处理器系统(SMP)。所以,如果没有适当的保护,同时在两个或两个以上的处理器上执行的内核代码很可能会同时访问共享的同一个资源。
  • 中断是异步到来的,完全不顾及当前正在执行的代码。也就是说,如果不加以适当的保护,中断完全有可能在代码访问资源的时候到来,这样,中段处理程序就有可能访问同一资源。
  • Linux内核可以抢占。所以,如果不加以适当的保护,内核中一段正在执行的代码可能会被另外一段代码抢占,从而有可能导致几段代码同时访问相同的资源。
     
2.4.7 可移植性的重要性

尽管用户空间的应用程序不太注意移植问题,然而Linux却是一个可移植的操作系统,并且要一直保持这种特点。也就是说,大部分C代码应该与体系结构无关,在许多不同体系结构的计算机上都能够编译和执行,因此,必须把与体系结构相关的代码从内核代码树的特定目录中适当地分离出来。
诸如保持字节序、64位对齐、不假定字长和页面长度等一系列准则都有助于移植性。对移植性的深度讨论将在后面的章节中进行。
 

参考:

[1] 《linux系统内核设计与实现》第三版

[2] Index of /pub/linux/kernel/

[3] 编译Linux内核时出现“code model kernel does not support PIC mode”的解决方法-****博客

[4] linux内核编译_make menuconfig报错(current_menu error)_make menuconfig失败-****博客

[5] 内核编译问题#error New address family defined, please update secclass_map#multiple definition of yylloc-****博客

[6] fatal error: openssl/bio.h: No such file or directory 解决方案-****博客

[7] Cannot generate ORC metadata for CONFIG_UNWINDER_ORC=y, please install libelf-dev-****博客

[8] Ubuntu 20.04 gcc9 linux-sgx v2.9出现-fcf-protection -mfunction-return冲突-****博客

[9] https://www.cnblogs.com/devilmaycry812839668/p/17134759.html

[10] ubuntu16.04 安装gcc8,g++8_e: 未发现软件包 gcc-8-****客

[11] Ubuntu22.04编译linux内核5.4.34失败 - 知乎 (zhihu.com)

[12] objtool: Don't fail on missing symbol table · Pull Request !141 · openEuler/kernel - Gitee.com

相关文章