Windows + VS Code 编译 Linux 内核并用 QEMU 启动:从踩坑到跑通(任务 + 脚本全拆解)

一、前言

这篇文章记录的是我在一个真实工程里跑通 Linux 内核编译的过程:

  • 开发环境:Windows + VS Code
  • 编译环境:WSL2 Ubuntu-24.04
  • 启动验证:QEMU
  • 源码版本:linux-6.1.174.tar.gz

一开始我想要的很简单:不装双系统,也能在 Windows 上完成 Linux 内核编译和启动验证。

后来真正跑下来才发现,这件事的关键不在"能不能编",而在于:

  1. 任务要足够清楚,方便重复执行;
  2. 源码必须放对地方,不然一堆奇怪报错会把人绕晕;
  3. 启动链路要闭环,不能只看到编译成功就收工。

所以这篇不是纯理论介绍,而是我把整个流程收敛成一套可直接复用的最小工作流


二、最终工作流(稳定版)

最后我把整个工程压缩成 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.cxt_tcpmss.c 两个文件。

Windows 文件系统默认大小写不敏感,先在 Windows 解压可能丢文件,后面构建规则就会断。

所以这个工程的核心原则是:

tar 包可放 Windows 盘,但源码解压必须在 Ubuntu 文件系统(如 ~/linux)进行。


四、每个任务到底干了什么?(附对应脚本)

任务 1:wsl: setup env

  • 作用:安装编译链和运行依赖
  • 对应脚本:scripts/setup_wsl_env.sh
  • 关键安装项:build-essentialbcbisonflexlibssl-devlibelf-devdwarvesqemu-system-x86busybox-staticccachegdb

这一步只需首次执行,或环境变化后再执行。


任务 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.cxt_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
  • 建常用软链接(shmountunamedmesg 等)
  • 生成 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
  • 执行顺序:
    1. extract
    2. apply fragment
    3. build
    4. make initramfs
    5. run qemu

这个任务就是"日常主入口"。


五、.vscode/tasks.json 的实现思路

这个工程里任务统一采用:

  • wsl -d Ubuntu-24.04 -- bash -lc '...'

这样做有两个好处:

  1. 强制固定发行版,避免误跑到 docker-desktop 那类发行版
  2. 每个任务都可独立重放,定位问题时非常清晰

六、关键脚本设计亮点

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 同步打印并落盘,出现异常时可回看完整启动日志,不用只盯终端瞬时输出。


七、如何判断流程完全跑通?

至少满足这三条:

  1. 构建日志出现:Kernel: arch/x86/boot/bzImage is ready
  2. 构建任务退出码为 0
  3. 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": []
      }
   ]
}

相关推荐
lcreek12 小时前
Kali Linux WSL 从零安装 XFCE4 桌面完整指南
linux·wsl
努力搬砖的鱼12 小时前
深信服aTrust集群从节点访问失败排查:交换机策略重定向引发的“回包丢失”之谜
运维·网络·华为
加点油。。。。12 小时前
【远程桌面连接提示你的凭据不工作怎么办?】
运维·服务器
cen__y12 小时前
Linux13(数据库)
linux·服务器·c语言·开发语言·数据库
梦奇不是胖猫12 小时前
[ 计算机网络 | 第三章 ] 数据链路层03 CSMA/CD
运维·服务器·网络·计算机网络
Shadow(⊙o⊙)13 小时前
文件-语言-系统:基础IO-2.0——IO重定向接口,语言层缓冲区,系统级缓冲区。内核级分析!
linux·运维·服务器·开发语言·c++
玖釉-13 小时前
「接雨水」问题的算法建模与双指针优化分析
c++·windows·算法
志栋智能13 小时前
超自动化巡检:连接运维数据孤岛的桥梁
运维·网络·人工智能·自动化
Kingairy13 小时前
Dockerfile
linux·运维·服务器