Linux内核编译实战:配置裁剪、deb打包与自定义系统调用 说实话我第一次编译内核的理由特别朴素想给自己的机器加一个自定义的系统调用看看用户态程序是怎么“掉进”内核里的。结果那一次折腾了两个通宵中间经历了编译报符号错误、装完重启进不了图形界面、模块版本对不上、安全启动拦下未签名模块等一连串问题。现在回头看Linux 内核编译流程本身并不复杂真正的成本在“配置取舍”和“装完之后的收尾”上尤其是跑在 ubuntu 这类 desktop 发行版上发行版自行添加的一套东西会让标准的make make install走不通。这篇就把我整套流程完整拆一遍从环境依赖、配置裁剪、并行编译、打包安装、回滚预案一直讲到怎么在源码树里加一个属于自己的系统调用并验证它。内容偏向已经会用命令行、想深入内核层面的读者完全没接触过内核编译的人也能跟着走只是需要多一点耐心。1. 编译内核前需要决策的几件事1.1 先明确目的三种动机对应三种完全不同的策略很多人一上来就问“编译内核要多久”这个问题其实没有统一答案因为目的不同工作量差一个数量级。我把常见动机归成三类你可以先找自己那一类再进行手工操作。第一类是学习目的。想看看.config里几千个选项到底是什么想知道自己写的系统调用怎么被用户态调用到想理解 initramfs 和内核模块的关系。这类需求的核心是“看得见”所以我强烈建议用make menuconfig而不是直接make defconfig也不要用O输出目录把中间产物藏到另一个文件夹里产物的可追溯性比干净整洁重要得多。学习目的也意味着你不需要追求体积最小CONFIG_DEBUG_INFO该开就开后面用objdump、gdb看符号全靠它。第二类是功能定制。典型场景是改了内核里某个参数、打了某个补丁、加了自己写的模块或系统调用必须重新生成内核镜像。这种需求的核心是“改完的东西要被加载进去”所以配置基线必须来自你当前正在跑的那个内核的/boot/config-$(uname -r)而不是上游defconfig否则你会在一台正常的机器上莫名其妙丢掉一堆驱动。第三类是优化和精简。比如给嵌入式板子或虚拟机裁一个体积小、启动快的内核镜像。这类需求的核心是“删得对”localmodconfig是关键工具但它有副作用后面第 3 节详细说明。三类动机里前两类的操作路线几乎一样第三类会多出一轮反复试错。我下面主要按“第二类 顺便学习”的路线写这是绝大多数人在个人机器上实际会遇到的情况。1.2 版本选择优先沿用当前运行的版本不要一开始就追最新一个很常见的误区是“既然要编译那就编最新的”。我不建议这么做至少第一次不要。原因有三个层面。第一你当前系统里跑的内核版本必然和这套系统的用户态组件、固件包、initramfs 工具链是磨合过的。发行版在打包内核时改动过很多细节比如模块签名策略、initramfs 生成脚本的钩子、DKMS 的适配。你换成上游最新版本这些适配很可能不再成立第一次编译就同时面对“新版本兼容问题”和“我的操作对不对”两个变量排查起来极其痛苦。第二版本跨度大的时候配置项的语义会变。老.config里的一些选项在新内核里被删除、重命名或拆分make oldconfig会让你一个个回答新选项答错一个可能就导致某个子系统整个没编进去而启动过程中你只看到一句“VFS: Unable to mount root fs”根本不知道是哪一步丢的。第三学习成本。同一版本的内核源码目录结构、syscalls表的写法在不同主版本之间有差异。5.x 之后 x86 的系统调用表从“直接写明函数名”变成了带 ABI 标记的写法6.x 之后sys_call_table的生成方式又变过一次。你在网上搜到的教程大概率和你手上的源码不是同一个大版本照着抄十有八九对不上。所以版本尽量贴近uname -r让教程和源码的差距小一点。具体操作上先uname -r看当前版本比如6.8.0-45-generic那核心版本就是6.8。你可以去源码站下载6.8.x里最新的一个小版本。小版本之间的差异远小于大版本风险可控。如果你想试新版本我建议先成功走通一次“同版本重编”确认自己的流程没问题再换版本。1.3 三条安装落地路径各自适合什么场景编译完之后把内核装进系统有三种做法差别很大我在实际使用中三种都用过。方式命令产物位置优点主要缺点直接安装make install make modules_install/boot与/lib/modules步骤少直接生效卸载麻烦容易留垃圾包管理器“不认识”它打成本地 deb 包make bindeb-pkg上层目录生成.deb可用包管理器安装和卸载干净可控需要额外的打包依赖首次配置稍繁琐输出到自定义目录make O/path/outINSTALL_PATH你指定的路径完全不影响宿主系统适合实验需要手工处理模块路径和 initramfs不适合日常启动我现在的固定做法是第一次跑通流程用打 deb 包的方式。理由很实在编译内核几乎必然要试错几轮直接make install每轮都会往/boot和/lib/modules里堆一份新东西三轮之后你的/boot里就有六七个内核镜像和一堆System.map备份/boot分区本来就小很快会满满了之后连apt都跑不动。deb 包的好处是装和卸都只有一条命令dpkg -r之后干干净净不会残留。O输出目录我一般只用在两种场景一是在服务器上做纯编译验证不打算装到自己机器二是需要给不同配置并行编译多份做对比。日常使用反而增加心智负担因为make modules_install的路径推断会变得不那么直观。2. 环境准备依赖、空间和时间这三笔账2.1 依赖清单每一条为什么必须装在 ubuntu 上直接一条命令装齐但我想逐条解释一下它们各自负责什么这样出错的时候你能判断是哪个环节的问题。sudo apt update sudo apt install -y build-essential libncurses-dev bison flex \ libssl-dev libelf-dev bc dwarves rsync cpio zstd kmodbuild-essential提供编译器、make、链接器这一整套。注意内核某些部分依赖的编译器版本有下限太老的发行版自带的gcc可能编不过遇到奇怪的语法错误先gcc --version看一眼。libncurses-dev是make menuconfig的界面库。缺了它你会得到Unable to find the ncurses package并且menuconfig相关的几条命令全部不可用。很多人图省事用make config或直接改.config我认为不可取几千个选项逐条问答会疯掉手改.config更容易因为依赖关系没满足被make悄悄改掉你以为改了其实没生效。bison和flex是语法分析器和词法分析器生成工具内核里解析器、dtc 之类的地方要用。缺了它们的报错通常出现在编译中段比如flex: command not found这个时候前面已经编了十几分钟特别浪费。libssl-dev提供加密库内核签名、模块签名、部分加密算法实现要用。libelf-dev是CONFIG_UNWINDER_ORC和 BTF 生成相关。这两个是最容易漏的尤其是你抄了一份精简的依赖列表的时候。bc很多人会忽略但内核编译过程中有脚本拿它做整数运算缺了会报bc: not found出现在kernel/timeconst.h相关的规则里。dwarves提供pahole这个是给CONFIG_DEBUG_INFO_BTF用的。如果开了 BTF 又没装它你会看到.tmp_vmlinux.btf: pahole (pahole) is not available然后编译失败。如果你不想装就得把CONFIG_DEBUG_INFO_BTF关掉但很多 eBPF 工具依赖 BTF关掉之后bpftool之类的东西会不好用所以我建议装上。rsync、cpio、zstd是打包和压缩用的。cpio是生成 initramfs 的底层工具zstd是当前主流内核镜像压缩格式rsync在make install的某些路径上会用到。2.2 磁盘、内存、CPU 这三笔账要提前算这部分是我踩坑最多的地方尤其是磁盘。源码树解压后大概 1.3 到 2 GB看版本。这个还不是问题问题是编译中间产物。如果不带调试信息O目录大概 3 到 5 GB带上CONFIG_DEBUG_INFO默认值通常是DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT或DEBUG_INFO_DWARF5中间产物的.o文件会膨胀到 15 到 30 GB我见过开DEBUG_INFO又没裁剪的情况下直接冲到 40 GB 的。所以在动手之前先df -h看一眼/或者你放源码的分区至少留出 30 GB 余量。我因为这个问题浪费过一次完整编译编到 90% 报No space left on device清完之后重编。内存的账是这样算的make -jN里每个并行任务在编译大文件时峰值占用大概 1 到 2 GB 内存开了 LTO 或者调试信息会更高。所以并行度的朴素规则是“nproc和 内存GB数/2 取较小值”。比如 8 核 16 GB 内存取 88 核 8 GB 内存取 4 就更稳。内存不够的后果是触发 OOM killer你可能会看到某个cc1进程被 killmake报Killed然后停在那里看起来像编译错误其实不是。时间上我实测一组参考值供你判断“卡住了”还是“就是慢”8 核 16 GB NVMe 的机器全量编译一次 6.x 内核带调试信息大概 15 到 25 分钟增量编译改一两个文件几十秒到两分钟。老的四核笔记本配机械盘一小时以上不奇怪。所以别在编译开始五分钟就觉得出问题了。2.3 源码获取与完整性校验源码从kernel.org或者镜像站拿linux-6.8.x.tar.xz同时下载同目录下的sha256sums校验文件sha256sum -c跑一遍。这一步很多人跳过但下载中断导致 tar 包截断的情况真的会发生解压的时候报Unexpected EOF in archive还算幸运更麻烦的是解压成功但某个文件内容不完整编译时报一堆莫名奇妙的语法错误你会怀疑人生。源码放到哪儿也有讲究。我习惯放~/src/linux-6.8.x不放/usr/src因为/usr/src通常有包管理器装的头文件目录混在一起容易误删。解压前顺手确认文件系统支持大小写敏感涉及跨系统拷贝的情况自己机器一般不用担心。3. 配置环节从当前内核中获取的起点3.1 拿/boot/config当基线再用 localmodconfig 做减法这是整套流程里最关键的一步配置错了后面全白搭。步骤如下cd ~/src/linux-6.8.x cp /boot/config-$(uname -r) .config make olddefconfigmake olddefconfig的作用是把你这份旧配置里已经不存在于当前源码的选项丢掉对新增选项全部采用默认值并且不产生任何交互。相比make oldconfig逐条问你olddefconfig更适合自动化但代价是那些“默认值不是我想要的”的选项会被悄悄设成默认。所以我一般先用olddefconfig生成一份可用的然后make menuconfig手工检查几个关键项见 3.2。如果你要减体积可以在olddefconfig之后跑make localmodconfig它的逻辑是看当前系统里已经加载了哪些模块lsmod然后只保留这些模块对应的配置把用不到的驱动全关掉。效果非常明显配置项能从一万多条减到三四千条编译时间砍掉一半以上。但这里有巨大的隐患localmodconfig只保留“当前已加载”的模块。当前没加载不等于不需要比如你用的是 USB 网卡但这次开机没插比如你现在用的是有线网但日常会用无线比如某个存储控制器的模块在启动时加载后可能被卸载了。这些都会被裁掉结果是新内核启动后再插上设备毫无反应。我见过最典型的情况是无线网卡驱动被裁掉装完重启发现连不了网只能靠手机共享网络或者再切回旧内核。所以跑完localmodconfig之后务必在menuconfig里把网络、存储、输入设备这几大类重新检查一遍。3.2 六个必须重点检查的配置项不管用什么方式生成.config我都会过一遍下面这几项这些都是我出过问题的地方。配置项建议值为什么CONFIG_MODULESy关掉后所有模块都无法使用很多驱动、文件系统依赖模块形式基本不能关CONFIG_MODULE_SIG视安全启动而定开了安全启动会拦截未签名模块详见第 4 节CONFIG_SYSTEM_TRUSTED_KEYS发行版配置里通常指向一个证书路径你的源码树里没有这个文件闲着就会编译失败CONFIG_DEBUG_INFO学习/调试开日常发行可关决定你能不能拿到完整符号也直接决定磁盘占用CONFIG_DEBUG_INFO_BTF装了pahole就保持eBPF 相关工具依赖缺pahole会编译失败CONFIG_IKCONFIGy推荐编完之后可以把.config塞进内核镜像用extract-ikconfig还原便于日后追踪“这个内核到底是哪份配置”CONFIG_SYSTEM_TRUSTED_KEYS这一项尤其值得说它是 ubuntu 系用户最容易在第一时间遇到的错误。发行版的配置里这一项写的是debian/certs/xxx.pem之类的相对路径校验机制会在编译某个阶段检查这个文件是否存在。上游源码树里当然没有于是直接报No rule to make target debian/certs/xxx.pem, needed by certs/x509_certificate_list报错行内容会因版本略有不同但关键字一定是certs/。解决办法是把它清空。用menuconfig的路径是Cryptographic API - Certificates for signature checking - Additional X.509 keys for default system keyring把里面内容删掉或者直接改.configscripts/config --set-str SYSTEM_TRUSTED_KEYS scripts/config --set-str SYSTEM_REVOCATION_KEYS 注意第二项SYSTEM_REVOCATION_KEYS也经常是同一个坑两个一起清。改完之后再跑一次make olddefconfig让依赖关系重新计算。3.3 ubuntu 桌面环境特有的两个问题第一个是安全启动。ubuntu 桌面版很多机器默认开了固件安全启动这意味着内核加载未签名模块会被拒绝。你自己编的内核没有发行版的签名所以可能出现两种情况一是内核本身根本起不来二是内核起来了但模块全加载失败表现为没网、没声音、分辨率不对。解决路径有两条进固件设置关掉安全启动最简单或者给自己的内核和模块做自签名并注册密钥流程较长日常自己用不划算。我是在自己的测试机上直接关掉安全启动的符合规则也省事但你要清楚这会降低启动链的校验强度做在个人实验机上可以生产设备上要评估。第二个是闭源显卡驱动。这类驱动是编译好的内核模块强依赖内核版本和符号版本。你换了自编内核它必然加载失败表现是进不了图形界面卡在登录管理器或者直接黑屏转文本。我的处理方式是在配置里保证开源驱动栈完整CONFIG_DRM、CONFIG_DRM_I915或CONFIG_DRM_AMDGPU之类按显卡选然后新内核启动后先用开源驱动把桌面跑起来需要的话再重装闭源驱动。千万不要在一次重启里同时切换内核和重装显卡驱动一旦出问题你分不清是哪个引起的。4. 编译与安装命令链每一步在做什么4.1 并行度与输出目录配置确认之后就是编译。我平时用的命令是time make -j$(nproc) 21 | tee build.log-j$(nproc)是最朴素的写法但前面说过内存吃紧的机器要往下调。time是为了以后估算时间有个依据。tee build.log是我强烈建议加上的一步内核编译的报错有时候会刷出去几十屏你往上翻的时候日志已经被后续输出淹没了有个完整日志文件可以grep -i error build.log快速定位。我甚至习惯在第一次编译时加V0默认只有排查具体某条规则时才用make V1重编那个目标因为V1会把每条命令都打出来日志会膨胀好几倍。如果你要用独立的输出目录make O~/build/linux-6.8.x -j$(nproc) menuconfig make O~/build/linux-6.8.x -j$(nproc) make O~/build/linux-6.8.x modules_install注意每次都要带上同一个O漏一次它就会在源码树里新建一套产物然后你会遇到“明明改了配置为什么没生效”的诡异情况。这是我在实战中真正被坑过的地方因为我在另一个终端窗口里忘了带O结果两份产物混在一起症状是模块签名报错查了两小时才发现是路径问题。4.2modules_install和install分别落在哪里编译完成之后会看到Kernel: arch/x86/boot/bzImage is ready字样两条命令的实际动作如下。sudo make modules_install会把所有编成模块m的部分装到/lib/modules/版本号/下同时生成modules.dep、modules.alias、modules.builtin等索引文件并调用depmod。这个目录名里的版本号来自内核源码的Makefile里EXTRAVERSION加上编译时的CONFIG_LOCALVERSION。如果你忘了设置CONFIG_LOCALVERSION装上去的目录名可能和发行版已有的目录名冲突这是很危险的情况会被覆盖。所以我习惯在配置里设一个明显不同的后缀比如CONFIG_LOCALVERSION-myk或-test1这样/lib/modules里一眼就能区分。sudo make install会做三件事把bzImage复制到/boot/vmlinuz-版本把System.map复制过去调用当前系统的installkernel脚本。在 ubuntu 上最后这一步会触发update-initramfs和update-grub。这里有个隐含依赖只有当你的内核被正确识别为“已安装”grub 才会给它生成菜单项。如果你用的是O输出目录并且INSTALL_PATH没设make install可能根本不往/boot里写东西然后你重启发现压根没有新内核选项。4.3 打 deb 包我现在默认用的方式前面说过我偏好打包。命令只有一行make -j$(nproc) bindeb-pkg它会在源码树的上一级目录生成几个.deb通常包括linux-image-版本_*.deb、linux-headers-版本_*.deb、linux-libc-dev_*.deb。然后用sudo dpkg -i装 image 和 headers 就行。这条路线的优势在卸载时体现得最明显sudo dpkg -r linux-image-版本干净利落/boot里的镜像、/lib/modules里的目录、grub 菜单项都会跟着清理。而make install装进去的东西包管理器认为它“不存在”你得手工删/boot/vmlinuz-xxx、/boot/initrd.img-xxx、/lib/modules/xxx再update-grub漏一步就会在 grub 菜单里留下一个启动不了的死项。有个细节要注意bindeb-pkg依赖debhelper、fakeroot之类缺了会报错。如果你机器上没装sudo apt install -y debhelper补上。另外打包过程比直接make install多花几分钟做压缩这是正常的。4.4 initramfs 与回滚预案无论用哪种方式装完之后确认两件事第一initramfs 是否生成。新内核如果/分区在 LVM、加密卷、RAID 或者用了某些存储控制器启动初期必须靠 initramfs 里的模块才能挂上根文件系统。ubuntu 上手工处理是sudo update-initramfs -c -k 6.8.0-myk ls -lh /boot/initrd.img-6.8.0-myk如果第二条命令报“No such file”那几乎必然启动失败不要继续往下走。用 deb 包安装的话这一步由包管理器的钩子自动完成通常不用管但装完还是瞄一眼/boot比较安心。第二回滚路径。这一步是保命的必须在重启前想清楚。ubuntu 上的做法是重启时按住Esc或狂点Shift进 grub 菜单选Advanced options for Ubuntu里面能看到旧内核。所以在重启之前先确认旧内核在 grub 菜单里还看得见办法是grep -c menuentry /boot/grub/grub.cfg大致看条目数量或者直接update-grub看输出。如果旧内核的initrd.img因为/boot满了被删了那就没有退路了。我给自己定的规矩是/boot分区至少保留两份完整的内核旧的和新的并且不在同一台机器上一次装两个自编内核。/boot空间不足时先卸掉更早的那份别硬塞。5. 自己加一个系统调用从 syscall 指令到 sys_call_table5.1 一次系统调用在内核中走的几步先把原理说通不然改代码就是照抄。以 x86_64 为例用户态调一个系统调用大致是这么走的用户态的库函数比如write最终执行一条syscall指令同时把系统调用号放在rax寄存器里参数依次放在rdi、rsi、rdx、r10、r8、r9注意第三个之后不是rcxrcx被用来存返回地址了。CPU 切到内核态跳到entry_SYSCALL_64这段汇编保存用户态寄存器现场然后进入 C 代码do_syscall_64。do_syscall_64里做参数检查nr是否超出范围然后从sys_call_table这个函数指针数组里按nr取出对应函数地址并调用。被调用的函数通常由SYSCALL_DEFINE1这类宏定义宏负责把参数从寄存器里解出来、做类型转换并生成一个形如__x64_sys_xxx的真实符号。返回值通过rax带回用户态。负数表示错误-ERANGE、-EFAULT之类libc 会把它转成errno并返回-1。理解了这条链路你就知道要加一个系统调用需要动哪几处编号表告诉分发逻辑多少号对应哪个函数、函数定义真正的实现。在老版本内核里还要在include/linux/syscalls.h加一行声明但在近几年的 x86 架构上声明是由表自动生成的不用手工加。5.2 三处改动与号码的选择第一处打开系统调用表arch/x86/entry/syscalls/syscall_64.tbl这个文件按 ABI 分段开头的列分别是“号”、“ABI 类型”、“名字”、“实现入口”。ABI 类型有三种common64 位和 x32 共用、64仅 64 位、x32仅 x32。文件里会看到类似这样的段0 common read sys_read 1 common write sys_write ... 462 common landlock_create_ruleset sys_landlock_create_ruleset ... 512 x32 rt_sigaction compat_sys_rt_sigaction0到462附近是已经用掉的512开始那段是 x32 专用绝对不能碰。中间463到511是相对安全的空洞。所以正确做法是先grep一下确认awk $1 ~ /^[0-9]$/ $1 450 {print} arch/x86/entry/syscalls/syscall_64.tbl | sort -n | tail -20看清楚最大公共号之后往后取一个没被占用的。我第一次做这个的时候随手写了个548结果正好撞进 x32 段编译过了调用的时候行为莫名其妙最后发现是号冲突。这个坑值得记一下。假设我们选定463在表里合适的位置加一行463 common hello sys_hello第二处实现函数。我一般加在kernel/sys.c文件末尾因为那是系统调用实现比较集中的地方方便找。无参数的版本SYSCALL_DEFINE0(hello) { pr_info(hello from my kernel syscall\n); return 0; }有参数、需要从用户态拷数据的版本SYSCALL_DEFINE2(hello_name, const char __user *, name, int, len) { char kbuf[64]; int n; if (len 0 || len (int)sizeof(kbuf)) return -EINVAL; /* 关键绝对不能直接解引用用户态指针 */ if (copy_from_user(kbuf, name, len)) return -EFAULT; kbuf[len] \0; pr_info(hello, %s\n, kbuf); return len; }这里有一个必须说清楚的事情__user标记的指针在内核态不能直接读写。用户态指针指向的可能是无效地址直接解引用轻则触发缺页处理异常重则被恶意构造的地址搞崩。正确的做法是copy_from_user/copy_to_user它们内部做了地址合法性检查。这条规矩在新手代码里出错率极高我见过有人直接printk(%s, name)在自己机器上跑得好好的换个地址就整个内核 panic。第三处如果没有走common sys_xxx这种老式写法而是用了__x64_sys_xxx的形式记得不要手工在include/linux/syscalls.h里重复声明否则会报重复定义。判断方法很简单编译一次看报错说什么如果提示找不到__x64_sys_hello就说明表里的写法要改成common hello sys_hello。改完之后重新编译。因为是增量编译只改了kernel/sys.c和那个表通常一两分钟就出镜像。这一步最能体现“循环迭代”的效率这也是我建议用O输出目录做这种实验的原因源码树干净重编快。5.3 用户态测试与 strace 验证装好新内核、重启进新系统后验证代码大概是这样/* hello_test.c */ #include unistd.h #include sys/syscall.h #include stdio.h #include string.h #include errno.h #define __NR_hello 463 #define __NR_hello_name 464 int main(void) { long r; r syscall(__NR_hello); printf(hello() - %ld\n, r); const char *msg test-user; r syscall(__NR_hello_name, msg, (int)strlen(msg)); if (r 0) { printf(hello_name failed, errno%d (%s)\n, errno, strerror(errno)); return 1; } printf(hello_name() - %ld\n, r); return 0; }编译运行gcc -O2 -o hello_test hello_test.c ./hello_test sudo dmesg | tail -5正常情况下dmesg里能看到你自己的pr_info输出。如果看不到先确认dmesg没有因为内核配置把日志级别过滤掉可以用sudo dmesg -w实时看同时另一个终端跑测试程序。strace是另一个很有用的验证手段strace -e trace463,464 ./hello_test输出里会显示syscall_0x1cf(...)463 的十六进制是 0x1cf。如果你的号没被识别strace会显示ENOSYS那就说明表里那一行没生效或者你装的内核根本不是刚编译的那个检查uname -r。还有一个常见困惑/usr/include/asm/unistd_64.h里没有你新加的号。这是正常的那个头文件是编译时从表生成的在构建目录里arch/x86/include/generated/uapi/asm/unistd_64.h系统的头文件目录不会自动更新。所以我测试程序里直接用#define硬编码号而不是#include。5.4 直接修改内核并非唯一选择kprobe 和 eBPF 的更温和方案如果你只是想在某个事件发生时打印点东西、收集点数据完全不必为了这个去改系统调用表。修改内核的代价是以后每次升级内核都得重新打补丁而且这个号在别人的内核上不存在你的程序换台机器就跑不起来。更轻量的路子有两条。一是kprobe可以在运行时把探针挂到任意内核函数的入口或返回处通过tracefs就能配置不用重编译sudo mount -t tracefs nodev /sys/kernel/tracing echo p:myprobe do_sys_open | sudo tee /sys/kernel/tracing/kprobe_events二是 eBPF配合bpftrace或bcc工具集能做的事情比 kprobe 更进一步包括读参数、聚合统计。这两条路都不需要改源码也不需要重启。我把系统调用表这一套当成“学习用途 特定功能必须改内核”的手段日常观测优先用 eBPF。分清这两者的适用边界能省掉大量重复劳动。6. 常见故障速查与排查实录6.1 编译阶段报错关键字原因处理No rule to make target debian/certs/xxx.pem发行版配置里的证书路径在你的源码树不存在把SYSTEM_TRUSTED_KEYS和SYSTEM_REVOCATION_KEYS置空pahole (pahole) is not available开了 BTF 但没装dwarves装dwarves或关掉DEBUG_INFO_BTFflex: command not found缺依赖补装flex、bisonKilled/ 进程被杀内存不足触发 OOM降-j并行度No space left on device磁盘满清空间或关掉DEBUG_INFOundefined reference to __x64_sys_xxx表里写了新名字但没有对应实现检查SYSCALL_DEFINE是否加了、函数名是否拼错编译到一半突然大量语法错误头文件被改坏或源码不完整重新解压源码别在原树上硬改这张表里最后一条我想多说两句。内核编译的报错有个特点第一个错误才是真的后面的往往是它的连锁反应。所以正确的排查顺序永远是grep -iE error|错误 build.log | head -20看最前面几条不要被最后几屏吓到。6.2 启动阶段黑屏/卡在启动画面。第一件事是判断卡在哪开机时去掉quiet splashgrub 里按e编辑启动参数让内核日志打出来。如果日志停在Loading initial ramdisk那基本是 initramfs 里缺模块如果日志走了一段之后停在VFS: Unable to mount root fs那是根文件系统驱动没进 initramfs如果一路走到systemd相关输出就停住通常是图形驱动的问题。进不了 grub 菜单。ubuntu 默认隐藏 grub 菜单开机时按住Esc或者直接改/etc/default/grub把GRUB_TIMEOUT_STYLE改成menu、GRUB_TIMEOUT设成 5然后sudo update-grub。这一步建议在装新内核之前就做不然出问题的时候你连选择旧内核的机会都没有。模块加载失败。dmesg | grep -i module verification failed能看到签名相关的失败信息。先确认安全启动状态mokutil --sb-state如果是 enabled要么关掉要么接受这个现实只用开源驱动栈。6.3 系统调用相关ENOSYSerrno为 38。说明内核不认这个号。依次检查uname -r是不是新内核表里那一行是否真的写进了源码grep hello arch/x86/entry/syscalls/syscall_64.tbl编译输出里是否真的重新生成了syscall_64.tbl相关的中间文件。我遇到过一次是因为用了O输出目录但第二次编译忘了带O编出来的是另一份产物make install装的却是我以为的那份。返回-EFAULT。copy_from_user失败最常见的原因是长度传得比字符串实际长度大跨过了字符串结尾进入未映射的页。这种错误定位方法是把len打印出来和strlen对一下。调用后系统崩溃或 panic。八成是直接解引用了用户态指针或者返回值用了int而内核期望long。SYSCALL_DEFINEn宏生成的函数返回类型固定是long如果你手工写asmlinkage long sys_xxx就要注意类型一致。7. 几次反复折腾后才养成的习惯我现在做这套流程有几个固定动作都是被坑出来的分享出来供你参考。第一源码目录的名字里带上版本号和用途比如linux-6.8.9-syscall-lab。编译一份内核要花半小时到一小时最痛苦的不是过程中的报错而是三天后你回来想知道“这个内核我到底改了什么”。目录名和CONFIG_LOCALVERSION双保险uname -r一看就知道是哪次实验的产物。第二每次编译前先df -h /和free -g两秒钟的事能避免一次白干一小时的编译。我现在的习惯是把这两个命令和make写进一个脚本里脚本第一件事就是检查磁盘余量够不够不够直接退出。第三任何一次“装新内核”都当成一次可能失败的操作重启前先确认旧内核在 grub 菜单里可见。这个习惯救过我至少三次尤其是那次我在配置里误关了存储控制器驱动新内核起来根本找不到根分区靠 grub 菜单切回旧内核才把问题救回来。第四改内核代码前先在一个O独立目录里验证编译通过确认没问题再往正式的源码树上搬。这个小步骤能让你在实验失败时随时删掉整个目录重来不用担心污染主力源码树。第五系统调用号、模块名、打印信息里都带上自己的标识比如统一用myk前缀这样dmesg、lsmod、strace的输出里你一眼就能挑出自己那一份和系统自带的混在一起也不至于看错。这些细节不写进任何文档但真出事的时候能节省下来的时间远超当初多打几个字符的成本。