一、前言
这篇文章记录的是我在一个真实工程里跑通 Linux 内核编译的过程:
- 开发环境:Windows + VS Code
- 编译环境:WSL2 Ubuntu-24.04
- 启动验证:QEMU
- 源码版本:
linux-6.1.174.tar.gz
一开始我想要的很简单:不装双系统,也能在 Windows 上完成 Linux 内核编译和启动验证。
后来真正跑下来才发现,这件事的关键不在"能不能编",而在于:
- 任务要足够清楚,方便重复执行;
- 源码必须放对地方,不然一堆奇怪报错会把人绕晕;
- 启动链路要闭环,不能只看到编译成功就收工。
所以这篇不是纯理论介绍,而是我把整个流程收敛成一套可直接复用的最小工作流。
二、最终工作流(稳定版)
最后我把整个工程压缩成 7 个任务。先看总览,再看细节:
| 任务 | 作用 | 脚本 |
|---|---|---|
wsl: setup env |
安装编译和运行依赖 | scripts/setup_wsl_env.sh |
wsl: extract kernel tar to ubuntu fs |
把源码直接解压到 Ubuntu 文件系统 | scripts/extract_kernel_tar_to_ubuntu_fs.sh |
wsl: apply kernel dev fragment (ubuntu fs) |
合并调试友好的内核配置片段 | scripts/apply_kernel_fragment.sh |
wsl: build kernel (ubuntu fs, auto jobs + ccache) |
编译内核 | scripts/build_kernel.sh |
wsl: make initramfs (ubuntu fs) |
制作最小 rootfs | scripts/make_initramfs.sh |
wsl: run qemu (ubuntu fs) |
启动验证 | scripts/run_qemu.sh |
wsl: pipeline (ubuntu fs + build + run) |
一键跑完整流程 | .vscode/tasks.json |
一句话总结:
先在 Ubuntu 文件系统准备源码,再配置、编译、打包 initramfs,最后用 QEMU 启动验证。
如果你只想记住一个结论,那就是:源码不要拿 Windows 盘直接编,尤其是内核这种大工程。
三、为什么要放到 Ubuntu 文件系统?
我在 /mnt/d/... 编译时遇到过经典报错:
No rule to make target 'net/netfilter/xt_TCPMSS.o'- 最终汇总成
Error 2
根因是大小写敏感问题:Linux 源码里有 xt_TCPMSS.c 和 xt_tcpmss.c 两个文件。
Windows 文件系统默认大小写不敏感,先在 Windows 解压可能丢文件,后面构建规则就会断。
所以这个工程的核心原则是:
tar 包可放 Windows 盘,但源码解压必须在 Ubuntu 文件系统(如
~/linux)进行。
四、每个任务到底干了什么?(附对应脚本)
任务 1:wsl: setup env
- 作用:安装编译链和运行依赖
- 对应脚本:
scripts/setup_wsl_env.sh - 关键安装项:
build-essential、bc、bison、flex、libssl-dev、libelf-dev、dwarves、qemu-system-x86、busybox-static、ccache、gdb
这一步只需首次执行,或环境变化后再执行。
任务 2:wsl: extract kernel tar to ubuntu fs
- 作用:把 tar 包直接解压到 Ubuntu 文件系统
- 对应脚本:
scripts/extract_kernel_tar_to_ubuntu_fs.sh - 默认输入:
/mnt/d/LINUX_workspace/tiga/tag/linux-6.1.174.tar.gz - 默认输出:
$HOME/linux
脚本里还做了一个关键校验:
会检查 xt_TCPMSS.c 和 xt_tcpmss.c 是否同时存在,防止大小写冲突。
任务 3:wsl: apply kernel dev fragment (ubuntu fs)
- 作用:把调试友好的配置片段合并进
.config - 对应脚本:
scripts/apply_kernel_fragment.sh - 使用方式:
- 先
make defconfig - 再调用
scripts/kconfig/merge_config.sh - 最后
make olddefconfig
- 先
这个任务本质是在做"最小化且可复现"的配置合并,避免手工 menuconfig 漏项。
任务 4:wsl: build kernel (ubuntu fs, auto jobs + ccache)
- 作用:真正编译内核
- 对应脚本:
scripts/build_kernel.sh - 关键能力:
BUILD_JOBS=auto:按 CPU + 内存自动估算并发USE_CCACHE=1:使用 ccache 加速重复编译
主要产物:
~/linux/arch/x86/boot/bzImage~/linux/vmlinux
任务 5:wsl: make initramfs (ubuntu fs)
- 作用:制作最小用户态根文件系统
- 对应脚本:
scripts/make_initramfs.sh - 默认输出:
$HOME/out/initramfs.cpio.gz
脚本做了这些事:
- 复制
busybox - 建常用软链接(
sh、mount、uname、dmesg等) - 生成
init启动脚本(挂载proc/sys,进入 shell) - 打包为
cpio.gz
任务 6:wsl: run qemu (ubuntu fs)
- 作用:用编译出来的内核 + initramfs 启动系统
- 对应脚本:
scripts/run_qemu.sh - 关键参数:
-kernel "$HOME/linux/arch/x86/boot/bzImage"-initrd "$HOME/out/initramfs.cpio.gz"-append "console=ttyS0 rdinit=/init"-nographic
日志会自动写到:$HOME/out/logs/
任务 7:wsl: pipeline (ubuntu fs + build + run)
- 作用:把任务 2~6 串成一条一键流水线
- 配置位置:
.vscode/tasks.json - 执行顺序:
- extract
- apply fragment
- build
- make initramfs
- run qemu
这个任务就是"日常主入口"。
五、.vscode/tasks.json 的实现思路
这个工程里任务统一采用:
wsl -d Ubuntu-24.04 -- bash -lc '...'
这样做有两个好处:
- 强制固定发行版,避免误跑到
docker-desktop那类发行版 - 每个任务都可独立重放,定位问题时非常清晰
六、关键脚本设计亮点
1)build_kernel.sh 的并发控制
不是盲目 -j$(nproc),而是结合内存估算并发:
- 粗略按每个编译 job ≈ 1.5GB RAM 计算
JOBS = min(CPU核数, 内存可承载job数)
这比固定并发更稳,尤其在 WSL 内存受限时。
2)extract_kernel_tar_to_ubuntu_fs.sh 的文件校验
脚本不只解压,还会检查 case-sensitive 文件对是否齐全。
这一步直接挡住了最容易踩的坑:
xt_TCPMSS.c / xt_tcpmss.c 冲突导致后续构建失败。
3)run_qemu.sh 的日志重定向
通过 tee 同步打印并落盘,出现异常时可回看完整启动日志,不用只盯终端瞬时输出。
七、如何判断流程完全跑通?
至少满足这三条:
- 构建日志出现:
Kernel: arch/x86/boot/bzImage is ready - 构建任务退出码为 0
- QEMU 启动后
uname -a能看到目标内核版本(如6.1.174)
满足这三条就说明:
源码 → 配置 → 编译 → 打包 → 启动 闭环已经打通。
八、常见报错与处理
报错 1:/bin/sh: bash: not found
原因:运行到了错误的 WSL 发行版(常见是 docker-desktop)。
处理:任务里强制 -d Ubuntu-24.04。
报错 2:No rule to make target ... xt_TCPMSS.o
原因:源码在大小写不敏感文件系统解压过,文件丢失。
处理:重新执行 wsl: extract kernel tar to ubuntu fs,确保源码在 ~/linux。
报错 3:Kernel image not found: linux/arch/x86/boot/bzImage
原因:用了默认 run qemu 任务,但你实际在 ~/linux 编译。
处理:运行 wsl: run qemu (ubuntu fs)。
九、总结
这套方案最核心的经验只有一句话:
在 Windows 做 Linux 内核开发没问题,但源码与编译必须尽量放在 Ubuntu 文件系统内。
这个工程已经把流程固定成"任务 + 脚本"模式,后续你要换版本、换配置、做二次验证,都能在这个骨架上直接复用。
如果你也在做类似实践,建议照着"最小任务流"先跑通,再做优化(调试、自动测试、性能调优)。
十、附录:本文用到的完整脚本(可直接复制)
1)scripts/setup_wsl_env.sh
bash
#!/usr/bin/env bash
set -euo pipefail
sudo apt update
sudo apt install -y \
build-essential bc bison flex libssl-dev libelf-dev dwarves \
git cpio qemu-system-x86 busybox-static ccache gdb procps
echo "[OK] WSL build environment is ready."
2)scripts/extract_kernel_tar_to_ubuntu_fs.sh
bash
#!/usr/bin/env bash
set -euo pipefail
ARCHIVE="${1:-/mnt/d/LINUX_workspace/tiga/tag/linux-6.1.174.tar.gz}"
DEST_DIR="${2:-$HOME/linux}"
if [[ ! -f "$ARCHIVE" ]]; then
echo "[ERR] Kernel archive not found: $ARCHIVE"
exit 1
fi
echo "[INFO] Extracting kernel tarball"
echo " from: $ARCHIVE"
echo " to: $DEST_DIR"
rm -rf "$DEST_DIR"
mkdir -p "$DEST_DIR"
tar -xzf "$ARCHIVE" -C "$DEST_DIR" --strip-components=1
# This pair must coexist on case-sensitive filesystem.
for f in net/netfilter/xt_TCPMSS.c net/netfilter/xt_tcpmss.c; do
if [[ ! -f "$DEST_DIR/$f" ]]; then
echo "[ERR] Missing expected file after extraction: $f"
echo "[HINT] Do not extract Linux source on Windows filesystem first; extract directly into Ubuntu FS."
exit 1
fi
done
echo "[OK] Kernel extracted to Ubuntu FS with case-sensitive files preserved: $DEST_DIR"
3)scripts/apply_kernel_fragment.sh
bash
#!/usr/bin/env bash
set -euo pipefail
KERNEL_DIR="${1:-linux}"
FRAGMENT="${2:-configs/kernel-dev.fragment}"
WORKSPACE_DIR="$(pwd)"
if [[ ! -d "$KERNEL_DIR" ]]; then
echo "[ERR] Kernel directory '$KERNEL_DIR' not found."
exit 1
fi
if [[ ! -f "$FRAGMENT" ]]; then
echo "[ERR] Fragment file '$FRAGMENT' not found."
exit 1
fi
if [[ "$FRAGMENT" = /* ]]; then
FRAGMENT_PATH="$FRAGMENT"
else
FRAGMENT_PATH="$WORKSPACE_DIR/$FRAGMENT"
fi
MERGE_SCRIPT="$KERNEL_DIR/scripts/kconfig/merge_config.sh"
if [[ ! -x "$MERGE_SCRIPT" ]]; then
echo "[ERR] merge_config.sh not found. Build tree may be incomplete: $MERGE_SCRIPT"
exit 1
fi
pushd "$KERNEL_DIR" >/dev/null
make defconfig
"$PWD/scripts/kconfig/merge_config.sh" -m .config "$FRAGMENT_PATH"
make olddefconfig
popd >/dev/null
echo "[OK] Applied fragment: $FRAGMENT"
echo "[OK] Result config: ${KERNEL_DIR}/.config"
4)scripts/build_kernel.sh
bash
#!/usr/bin/env bash
set -euo pipefail
KERNEL_DIR="${1:-linux}"
USE_CCACHE="${USE_CCACHE:-0}"
BUILD_JOBS="${BUILD_JOBS:-auto}"
calc_jobs() {
local cpu_jobs mem_kb mem_jobs
cpu_jobs="$(nproc)"
mem_kb="$(awk '/MemTotal/ {print $2}' /proc/meminfo)"
# Rough rule: ~1.5GB RAM per compile job
mem_jobs=$(( mem_kb / 1572864 ))
if [[ "$mem_jobs" -lt 1 ]]; then
mem_jobs=1
fi
if [[ "$mem_jobs" -lt "$cpu_jobs" ]]; then
echo "$mem_jobs"
else
echo "$cpu_jobs"
fi
}
if [[ ! -d "$KERNEL_DIR" ]]; then
echo "[ERR] Kernel directory '$KERNEL_DIR' not found. Run clone script first."
exit 1
fi
pushd "$KERNEL_DIR" >/dev/null
if [[ ! -f .config ]]; then
make defconfig
fi
if [[ "$BUILD_JOBS" == "auto" ]]; then
JOBS="$(calc_jobs)"
else
JOBS="$BUILD_JOBS"
fi
echo "[INFO] Parallel jobs: $JOBS"
if [[ "$USE_CCACHE" == "1" ]]; then
make CC="ccache gcc" -j"$JOBS"
else
make -j"$JOBS"
fi
popd >/dev/null
echo "[OK] Build complete: ${KERNEL_DIR}/arch/x86/boot/bzImage"
5)scripts/make_initramfs.sh
bash
#!/usr/bin/env bash
set -euo pipefail
OUT_DIR="${1:-out}"
INITRAMFS_DIR="$OUT_DIR/initramfs"
INITRAMFS_IMG="$OUT_DIR/initramfs.cpio.gz"
mkdir -p "$INITRAMFS_DIR"/{bin,sbin,etc,proc,sys,usr/bin,usr/sbin,dev}
if [[ ! -x /usr/bin/busybox ]]; then
echo "[ERR] /usr/bin/busybox not found. Install busybox-static first."
exit 1
fi
cp -f /usr/bin/busybox "$INITRAMFS_DIR/bin/busybox"
for cmd in sh mount echo cat uname dmesg ls grep head poweroff reboot; do
ln -sf busybox "$INITRAMFS_DIR/bin/$cmd"
done
cat > "$INITRAMFS_DIR/init" <<'EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
echo "[init] minimal userspace started"
uname -a
CMDLINE="$(cat /proc/cmdline)"
if echo "$CMDLINE" | /bin/grep -qw "smoke=1"; then
echo "[SMOKE] START"
/bin/uname -a
/bin/dmesg | /bin/head -n 20
echo "[SMOKE] OK"
/bin/poweroff -f || /bin/reboot -f || exit 0
fi
echo "[init] type commands, Ctrl+A then X to quit QEMU"
exec /bin/sh
EOF
chmod +x "$INITRAMFS_DIR/init"
(
cd "$INITRAMFS_DIR"
find . -print0 | cpio --null -ov --format=newc 2>/dev/null | gzip -9 > "../initramfs.cpio.gz"
)
echo "[OK] initramfs created: $INITRAMFS_IMG"
6)scripts/run_qemu.sh
bash
#!/usr/bin/env bash
set -euo pipefail
KERNEL_DIR="${1:-linux}"
INITRAMFS_IMG="${2:-out/initramfs.cpio.gz}"
LOG_DIR="${3:-out/logs}"
KERNEL_IMG="$KERNEL_DIR/arch/x86/boot/bzImage"
if [[ ! -f "$KERNEL_IMG" ]]; then
echo "[ERR] Kernel image not found: $KERNEL_IMG"
exit 1
fi
if [[ ! -f "$INITRAMFS_IMG" ]]; then
echo "[ERR] initramfs not found: $INITRAMFS_IMG"
exit 1
fi
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/qemu-run-$(date +%Y%m%d-%H%M%S).log"
echo "[INFO] Saving runtime log to: $LOG_FILE"
exec > >(tee -a "$LOG_FILE") 2>&1
qemu-system-x86_64 \
-kernel "$KERNEL_IMG" \
-initrd "$INITRAMFS_IMG" \
-nographic \
-m 2048 \
-append "console=ttyS0 rdinit=/init"
十一、附录:.vscode/tasks.json 完整示例(拷贝即用)
jsonc
{
"version": "2.0.0",
"tasks": [
{
"label": "wsl: setup env",
"type": "shell",
"command": "wsl",
"args": [
"-d",
"Ubuntu-24.04",
"--",
"bash",
"-lc",
"bash /mnt/d/LINUX_workspace/tiga/scripts/setup_wsl_env.sh"
],
"problemMatcher": []
},
{
"label": "wsl: extract kernel tar to ubuntu fs",
"type": "shell",
"command": "wsl",
"args": [
"-d",
"Ubuntu-24.04",
"--",
"bash",
"-lc",
"bash /mnt/d/LINUX_workspace/tiga/scripts/extract_kernel_tar_to_ubuntu_fs.sh /mnt/d/LINUX_workspace/tiga/tag/linux-6.1.174.tar.gz \"$HOME/linux\""
],
"problemMatcher": []
},
{
"label": "wsl: apply kernel dev fragment (ubuntu fs)",
"type": "shell",
"command": "wsl",
"args": [
"-d",
"Ubuntu-24.04",
"--",
"bash",
"-lc",
"bash /mnt/d/LINUX_workspace/tiga/scripts/apply_kernel_fragment.sh \"$HOME/linux\" /mnt/d/LINUX_workspace/tiga/configs/kernel-dev.fragment"
],
"problemMatcher": []
},
{
"label": "wsl: build kernel (ubuntu fs, auto jobs + ccache)",
"type": "shell",
"command": "wsl",
"args": [
"-d",
"Ubuntu-24.04",
"--",
"bash",
"-lc",
"BUILD_JOBS=auto USE_CCACHE=1 bash /mnt/d/LINUX_workspace/tiga/scripts/build_kernel.sh \"$HOME/linux\""
],
"group": "build",
"problemMatcher": []
},
{
"label": "wsl: make initramfs (ubuntu fs)",
"type": "shell",
"command": "wsl",
"args": [
"-d",
"Ubuntu-24.04",
"--",
"bash",
"-lc",
"bash /mnt/d/LINUX_workspace/tiga/scripts/make_initramfs.sh \"$HOME/out\""
],
"problemMatcher": []
},
{
"label": "wsl: run qemu (ubuntu fs)",
"type": "shell",
"command": "wsl",
"args": [
"-d",
"Ubuntu-24.04",
"--",
"bash",
"-lc",
"bash /mnt/d/LINUX_workspace/tiga/scripts/run_qemu.sh \"$HOME/linux\" \"$HOME/out/initramfs.cpio.gz\" \"$HOME/out/logs\""
],
"problemMatcher": []
},
{
"label": "wsl: pipeline (ubuntu fs + build + run)",
"dependsOrder": "sequence",
"dependsOn": [
"wsl: extract kernel tar to ubuntu fs",
"wsl: apply kernel dev fragment (ubuntu fs)",
"wsl: build kernel (ubuntu fs, auto jobs + ccache)",
"wsl: make initramfs (ubuntu fs)",
"wsl: run qemu (ubuntu fs)"
],
"problemMatcher": []
}
]
}