WSL 里全量编译 Linux 内核:从踩坑到跑通
上一篇 公司内网安装 WSL2 把环境搭好了。接下来的目标是编译一次 Linux 内核——后面要做单片机的内核裁剪,先在 PC 上把全量编译跑通,确认工具链和环境没问题,后续裁剪时心里才有底。
选了 allmodconfig 全量配置,也就是把所有能编译成模块的选项全部打开。这是最暴力的配置——产物巨大、编译时间最长,但不需要手动处理任何 config 问题。如果这都能跑通,说明工具链没问题。
目录规划的选择
上一篇提过 /mnt/d/(NTFS)和 ext4 路径的区别。内核源码对大小写敏感有严格依赖——include/uapi/linux/netfilter/ 下同时存在 xt_CONNMARK.h 和 xt_connmark.h、xt_DSCP.h 和 xt_dscp.h 等十几组仅大小写不同的文件对。在 NTFS 上解压时后者会覆盖前者,直接导致编译失败。加上内核编译涉及上万个源文件和数十万次文件 I/O,9P 协议的性能损耗也会被放大到难以忍受的程度。
所以必须在 ext4 路径上编译,这一点没有商量余地。最终方案:
| 用途 | 路径 | 文件系统 | 原因 |
|---|---|---|---|
| 源码解压 + 编译 | /home/<user>/build | ext4 | 大小写敏感 + I/O 性能 |
| 源码压缩包存放 | /mnt/d/<build-dir> | NTFS | Windows 侧方便下载和管理 |
| 最终产物输出 | /mnt/d/<build-dir>/artifacts | NTFS | 方便 Windows 侧查看和传输 |
源码包放在 Windows 侧下载,cp 到 ext4 路径再解压编译,产物拷回 Windows 侧。中间编译过程全部在 ext4 上完成,只在首尾做一次跨文件系统拷贝。
依赖安装
Ubuntu 24.04 默认没装编译工具链,需要先补齐:
sudo apt-get update
sudo apt-get install -y \
build-essential bc bison flex libssl-dev libelf-dev libncurses-dev \
dwarves pahole rsync cpio这里有个容易混淆的点:dwarves 和 pahole 的关系。它们来自同一个源码包 dwarves,但二进制包名在不同 Ubuntu 版本间有变化——Ubuntu 22.04 之前主包叫 dwarves,Ubuntu 24.04 起主包改名为 pahole,原来的 dwarves 变成了指向 pahole 的过渡包。所以两个名字都能装,apt install dwarves 和 apt install pahole 最终装的是同一个东西。缺了这个工具的症状是编译到后期报 BTF: .tmp_vmlinux.btf: pahole is not available 然后挂掉,错误信息不太直观。
脚本演进
最终的编译脚本不是一次写成的,经历了好几个版本的迭代。
第一版 build-ext4.sh 非常粗糙——没有错误处理,日志和状态文件直接写到 /mnt/d/,用 defconfig(最小配置)编译。能跑通就行。
第二版 rebuild-ext4.sh 加了 tee 同时输出到终端和日志,捕获了 make 的退出码写入状态文件。但源码路径是硬编码的,换版本还得手动改。
第三版 rebuild-kernel.sh 引入了 VERSION 变量和 tarball 回退逻辑(优先用 ext4 本地副本,没有再从 /mnt/d/ 拷贝),仍然用 defconfig。
最终版 rebuild-allmodconfig.sh 才切到 allmodconfig,加了模块计数和体积统计。脚本放在 Windows 侧方便编辑,执行时从 WSL 调用:
#!/usr/bin/env bash
set -euo pipefail
VERSION="6.18.19"
BUILD_ROOT="/home/<user>/build"
SRC_DIR="$BUILD_ROOT/linux-$VERSION"
TARBALL_WIN="/mnt/d/<build-dir>/linux-$VERSION.tar.xz"
TARBALL_LOCAL="$BUILD_ROOT/linux-$VERSION.tar.xz"
LOG="$BUILD_ROOT/build-allmod.log"
STATUS="$BUILD_ROOT/status-allmod.txt"
mkdir -p "$BUILD_ROOT"
if [[ ! -f "$TARBALL_LOCAL" ]]; then
cp -f "$TARBALL_WIN" "$TARBALL_LOCAL"
fi
cd "$BUILD_ROOT"
rm -rf "$SRC_DIR"
tar -xf "$TARBALL_LOCAL"
cd "$SRC_DIR"
make mrproper
yes "" | make allmodconfig
: > "$LOG"
{
echo "START: $(date '+%F %T')"
echo "SRC: $SRC_DIR"
echo "CORES: $(nproc)"
} >> "$LOG"
if make -j"$(nproc)" >> "$LOG" 2>&1; then
{
echo "BUILD_RUNNING=NO"
echo "BUILD_RESULT=SUCCESS"
echo "MODULE_COUNT=$(find . -name '*.ko' | wc -l)"
echo "MODULES_SIZE=$(find . -name '*.ko' -print0 | du --files0-from=- -ch 2>/dev/null | tail -n 1)"
if [[ -f arch/x86/boot/bzImage ]]; then
echo "BZIMAGE=READY"
ls -lh arch/x86/boot/bzImage
else
echo "BZIMAGE=NOT_READY"
fi
echo "END: $(date '+%F %T')"
} > "$STATUS"
else
{
echo "BUILD_RUNNING=NO"
echo "BUILD_RESULT=FAILED"
echo "BZIMAGE=NOT_READY"
echo "END: $(date '+%F %T')"
} > "$STATUS"
exit 2
fi几个值得说明的设计:
make mrproper 而不是 make clean。mrproper 比 clean 更彻底——它会删除 .config 和所有生成的文件,确保每次都是从干净状态开始。对于全量编译来说,"干净"比"快"更重要,一个残留的旧 .config 可能导致难以定位的编译错误。
yes "" | make allmodconfig。allmodconfig 生成配置时偶尔会弹出交互式问题(新内核引入了新选项但 allmodconfig 的规则没覆盖到),yes "" 对所有问题回车选默认值,避免脚本卡住。
状态文件和日志分离。status-allmod.txt 是结构化的键值对,方便脚本解析;build-allmod.log 是完整的编译输出,出问题时用来排查。
脚本保存后,先处理换行符再执行:
sed -i 's/\r$//' rebuild-allmodconfig.sh
chmod +x rebuild-allmodconfig.sh
bash rebuild-allmodconfig.shsed 那行很重要——Windows 下编辑的脚本带 \r\n 换行,bash 不认 \r,会报 $'\r': command not found 这种让人摸不着头脑的错误。
产物打包
编译完的产物分散在源码树各处,需要收集到一个目录:
#!/usr/bin/env bash
set -euo pipefail
VERSION="6.18.19"
ART="/mnt/d/<build-dir>/artifacts"
SRC="/home/<user>/build/linux-$VERSION"
mkdir -p "$ART/modules"
cp -f "$SRC/arch/x86/boot/bzImage" "$ART/"
cp -f "$SRC/vmlinux" "$ART/"
cp -f "$SRC/System.map" "$ART/"
cp -f "$SRC/.config" "$ART/config-$VERSION-allmodconfig"
cd "$SRC"
find . -name '*.ko' -exec cp --parents {} "$ART/modules/" \;
echo "PACKAGED_OK"
ls -lh "$ART/bzImage" "$ART/vmlinux" "$ART/System.map" "$ART/config-$VERSION-allmodconfig"
du -sh "$ART/modules"这里用 cp --parents 保留 .ko 文件的相对目录结构——直接 cp 会把所有模块拍平到一个目录下,上万个文件混在一起根本没法找。
同样需要处理换行符:
sed -i 's/\r$//' package-artifacts.sh
chmod +x package-artifacts.sh
bash package-artifacts.sh实际产物
Linux 6.18.19 allmodconfig 的真实构建结果:
| 产物 | 大小 | 说明 |
|---|---|---|
bzImage | 40.7 MB | 压缩内核镜像 |
vmlinux | 192.5 MB | 未压缩内核(含调试符号) |
System.map | 12.5 MB | 符号表 |
.config | 0.4 MB | allmodconfig 生成的配置文件 |
.ko 模块 | 10642 个,共 2.2 GB | 所有可编译的内核模块 |
一万多个模块、2.2 GB 总大小,看着吓人,但 allmodconfig 本来就是"把能编译的全编译"。实际部署时只会加载需要的几十个模块。保留全量产物的意义在于验证工具链的完整性——如果一万多个模块都能编译通过,说明环境没有任何缺失。
后续更新
换内核版本的流程很简单:
- 下载新版本的
linux-x.y.z.tar.xz到 Windows 侧目录 - 改脚本里的
VERSION变量 - 重新跑两个脚本
bash rebuild-allmodconfig.sh
bash package-artifacts.sh第一次编译要安装依赖、配置环境,比较折腾。但一旦跑通,后续就是改个版本号的事。这也是把流程固化成脚本的价值——把一次性的踩坑成本分摊到后续每次使用上。
附录:完整脚本
rebuild-allmodconfig.sh
更新时只需修改开头的 VERSION 和 TARBALL_WIN 路径。
#!/usr/bin/env bash
set -euo pipefail
VERSION="6.18.19"
BUILD_ROOT="/home/<user>/build"
SRC_DIR="$BUILD_ROOT/linux-$VERSION"
TARBALL_WIN="/mnt/d/<build-dir>/linux-$VERSION.tar.xz"
TARBALL_LOCAL="$BUILD_ROOT/linux-$VERSION.tar.xz"
LOG="$BUILD_ROOT/build-allmod.log"
STATUS="$BUILD_ROOT/status-allmod.txt"
mkdir -p "$BUILD_ROOT"
if [[ ! -f "$TARBALL_LOCAL" ]]; then
cp -f "$TARBALL_WIN" "$TARBALL_LOCAL"
fi
cd "$BUILD_ROOT"
rm -rf "$SRC_DIR"
tar -xf "$TARBALL_LOCAL"
cd "$SRC_DIR"
make mrproper
yes "" | make allmodconfig
: > "$LOG"
{
echo "START: $(date '+%F %T')"
echo "SRC: $SRC_DIR"
echo "CORES: $(nproc)"
} >> "$LOG"
if make -j"$(nproc)" >> "$LOG" 2>&1; then
{
echo "BUILD_RUNNING=NO"
echo "BUILD_RESULT=SUCCESS"
echo "MODULE_COUNT=$(find . -name '*.ko' | wc -l)"
echo "MODULES_SIZE=$(find . -name '*.ko' -print0 | du --files0-from=- -ch 2>/dev/null | tail -n 1)"
if [[ -f arch/x86/boot/bzImage ]]; then
echo "BZIMAGE=READY"
ls -lh arch/x86/boot/bzImage
else
echo "BZIMAGE=NOT_READY"
fi
echo "END: $(date '+%F %T')"
} > "$STATUS"
else
{
echo "BUILD_RUNNING=NO"
echo "BUILD_RESULT=FAILED"
echo "BZIMAGE=NOT_READY"
echo "END: $(date '+%F %T')"
} > "$STATUS"
exit 2
fipackage-artifacts.sh
将产物收集到 Windows 侧目录。
#!/usr/bin/env bash
set -euo pipefail
VERSION="6.18.19"
ART="/mnt/d/<build-dir>/artifacts"
SRC="/home/<user>/build/linux-$VERSION"
mkdir -p "$ART/modules"
cp -f "$SRC/arch/x86/boot/bzImage" "$ART/"
cp -f "$SRC/vmlinux" "$ART/"
cp -f "$SRC/System.map" "$ART/"
cp -f "$SRC/.config" "$ART/config-$VERSION-allmodconfig"
cd "$SRC"
find . -name '*.ko' -exec cp --parents {} "$ART/modules/" \;
echo "PACKAGED_OK"
ls -lh "$ART/bzImage" "$ART/vmlinux" "$ART/System.map" "$ART/config-$VERSION-allmodconfig"
du -sh "$ART/modules"首次使用
sed -i 's/\r$//' rebuild-allmodconfig.sh
sed -i 's/\r$//' package-artifacts.sh
chmod +x rebuild-allmodconfig.sh
chmod +x package-artifacts.sh
bash rebuild-allmodconfig.sh
bash package-artifacts.sh