Skip to content

WSL 里全量编译 Linux 内核:从踩坑到跑通

上一篇 公司内网安装 WSL2 把环境搭好了。接下来的目标是编译一次 Linux 内核——后面要做单片机的内核裁剪,先在 PC 上把全量编译跑通,确认工具链和环境没问题,后续裁剪时心里才有底。

选了 allmodconfig 全量配置,也就是把所有能编译成模块的选项全部打开。这是最暴力的配置——产物巨大、编译时间最长,但不需要手动处理任何 config 问题。如果这都能跑通,说明工具链没问题。

目录规划的选择

上一篇提过 /mnt/d/(NTFS)和 ext4 路径的区别。内核源码对大小写敏感有严格依赖——include/uapi/linux/netfilter/ 下同时存在 xt_CONNMARK.hxt_connmark.hxt_DSCP.hxt_dscp.h 等十几组仅大小写不同的文件对。在 NTFS 上解压时后者会覆盖前者,直接导致编译失败。加上内核编译涉及上万个源文件和数十万次文件 I/O,9P 协议的性能损耗也会被放大到难以忍受的程度。

所以必须在 ext4 路径上编译,这一点没有商量余地。最终方案:

用途路径文件系统原因
源码解压 + 编译/home/<user>/buildext4大小写敏感 + I/O 性能
源码压缩包存放/mnt/d/<build-dir>NTFSWindows 侧方便下载和管理
最终产物输出/mnt/d/<build-dir>/artifactsNTFS方便 Windows 侧查看和传输

源码包放在 Windows 侧下载,cp 到 ext4 路径再解压编译,产物拷回 Windows 侧。中间编译过程全部在 ext4 上完成,只在首尾做一次跨文件系统拷贝。

依赖安装

Ubuntu 24.04 默认没装编译工具链,需要先补齐:

bash
sudo apt-get update
sudo apt-get install -y \
  build-essential bc bison flex libssl-dev libelf-dev libncurses-dev \
  dwarves pahole rsync cpio

这里有个容易混淆的点:dwarvespahole 的关系。它们来自同一个源码包 dwarves,但二进制包名在不同 Ubuntu 版本间有变化——Ubuntu 22.04 之前主包叫 dwarves,Ubuntu 24.04 起主包改名为 pahole,原来的 dwarves 变成了指向 pahole 的过渡包。所以两个名字都能装,apt install dwarvesapt 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 调用:

bash
#!/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 cleanmrproperclean 更彻底——它会删除 .config 和所有生成的文件,确保每次都是从干净状态开始。对于全量编译来说,"干净"比"快"更重要,一个残留的旧 .config 可能导致难以定位的编译错误。

yes "" | make allmodconfigallmodconfig 生成配置时偶尔会弹出交互式问题(新内核引入了新选项但 allmodconfig 的规则没覆盖到),yes "" 对所有问题回车选默认值,避免脚本卡住。

状态文件和日志分离status-allmod.txt 是结构化的键值对,方便脚本解析;build-allmod.log 是完整的编译输出,出问题时用来排查。

脚本保存后,先处理换行符再执行:

bash
sed -i 's/\r$//' rebuild-allmodconfig.sh
chmod +x rebuild-allmodconfig.sh
bash rebuild-allmodconfig.sh

sed 那行很重要——Windows 下编辑的脚本带 \r\n 换行,bash 不认 \r,会报 $'\r': command not found 这种让人摸不着头脑的错误。

产物打包

编译完的产物分散在源码树各处,需要收集到一个目录:

bash
#!/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 会把所有模块拍平到一个目录下,上万个文件混在一起根本没法找。

同样需要处理换行符:

bash
sed -i 's/\r$//' package-artifacts.sh
chmod +x package-artifacts.sh
bash package-artifacts.sh

实际产物

Linux 6.18.19 allmodconfig 的真实构建结果:

产物大小说明
bzImage40.7 MB压缩内核镜像
vmlinux192.5 MB未压缩内核(含调试符号)
System.map12.5 MB符号表
.config0.4 MBallmodconfig 生成的配置文件
.ko 模块10642 个,共 2.2 GB所有可编译的内核模块

一万多个模块、2.2 GB 总大小,看着吓人,但 allmodconfig 本来就是"把能编译的全编译"。实际部署时只会加载需要的几十个模块。保留全量产物的意义在于验证工具链的完整性——如果一万多个模块都能编译通过,说明环境没有任何缺失。

后续更新

换内核版本的流程很简单:

  1. 下载新版本的 linux-x.y.z.tar.xz 到 Windows 侧目录
  2. 改脚本里的 VERSION 变量
  3. 重新跑两个脚本
bash
bash rebuild-allmodconfig.sh
bash package-artifacts.sh

第一次编译要安装依赖、配置环境,比较折腾。但一旦跑通,后续就是改个版本号的事。这也是把流程固化成脚本的价值——把一次性的踩坑成本分摊到后续每次使用上。

附录:完整脚本

rebuild-allmodconfig.sh

更新时只需修改开头的 VERSIONTARBALL_WIN 路径。

bash
#!/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

package-artifacts.sh

将产物收集到 Windows 侧目录。

bash
#!/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"

首次使用

bash
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

最后更新于:

Hosted by GitHub Pages