这里使用WSL(Windows Subsystem for Linux)下安装的Linux系统进行内核的编译,使用的版本:
work@HOME:~$ uname -aLinux HOME 6.6.87.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux使用如下的命令下载源码:
work@HOME:~/code$ wget https://mirrors.aliyun.com/linux-kernel/v5.x/linux-5.4.239.tar.xz--2026-01-07 22:01:36-- https://mirrors.aliyun.com/linux-kernel/v5.x/linux-5.4.239.tar.xzResolving mirrors.aliyun.com (mirrors.aliyun.com)... 111.1.160.30, 39.174.180.10, 39.174.180.4, ...Connecting to mirrors.aliyun.com (mirrors.aliyun.com)|111.1.160.30|:443... connected.HTTP request sent, awaiting response... 200 OKLength: 113011896 (108M) [application/octet-stream]Saving to: ‘linux-5.4.239.tar.xz’linux-5.4.239.tar.xz 100%[=================================================>] 107.78M 10.3MB/s in 11s2026-01-07 22:01:46 (10.2 MB/s) - ‘linux-5.4.239.tar.xz’ saved [113011896/113011896]这里从镜像网站下载某版本的内核。然后解压缩:
work@HOME:~/code$ tar -xvf linux-5.4.239.tar.xz# 中间略work@HOME:~/code$ lslinux-5.4.239 linux-5.4.239.tar.xzwork@HOME:~/code$ sudo apt install -y build-essential libncurses-dev bison flex libssl-dev libelf-dev cpio rsync make dosfstools可能并不是所有工具都需要,不过装了也没有什么坏处。
work@HOME:~/code/linux-5.4.239$ make defconfig HOSTCC scripts/kconfig/conf.o HOSTCC scripts/kconfig/confdata.o HOSTCC scripts/kconfig/expr.o LEX scripts/kconfig/lexer.lex.c YACC scripts/kconfig/parser.tab.[ch] HOSTCC scripts/kconfig/lexer.lex.o HOSTCC scripts/kconfig/parser.tab.o HOSTCC scripts/kconfig/preprocess.o HOSTCC scripts/kconfig/symbol.o HOSTLD scripts/kconfig/conf*** Default configuration is based on 'x86_64_defconfig'## configuration written to .config#work@HOME:~/code/linux-5.4.239$ make -j$(nproc) bzImage$(nproc)指定了编译使用的CPU核心数。
一切正常的话就可以得到bzImage文件:
work@HOME:~/code/linux-5.4.239$ ls ./arch/x86/boot/bzImage -al-rw-r--r-- 1 work work 9048896 Jan 7 22:04 ./arch/x86/boot/bzImage光一个内核还不够,直接启动(通过QEMU虚拟机)的话会得到如下的结果:

从报错信息可以看到,为了能够正常启动,还需要一个根文件系统。下面介绍如何创建一个简单的根文件系统:
work@HOME:~/code$ mkdir buildwork@HOME:~/code$ cd buildwork@HOME:~/code/build$ mkdir -p rootfs/{bin,dev,proc,sys,tmp,lib64,lib}work@HOME:~/code/build$ cd ./rootfs/work@HOME:~/code/build/rootfs$ lsbin dev lib lib64 proc sys tmpwork@HOME:~/code$ wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2--2026-01-07 22:08:26-- https://busybox.net/downloads/busybox-1.37.0.tar.bz2Resolving busybox.net (busybox.net)... 140.211.167.122Connecting to busybox.net (busybox.net)|140.211.167.122|:443... connected.HTTP request sent, awaiting response... 200 OKLength: 2565764 (2.4M) [application/x-bzip2]Saving to: ‘busybox-1.37.0.tar.bz2’busybox-1.37.0.tar.bz2 100%[=================================================>] 2.45M 6.83KB/s in 7m 5s2026-01-07 22:15:26 (5.89 KB/s) - ‘busybox-1.37.0.tar.bz2’ saved [2565764/2565764]然后解压:
work@HOME:~/code$ tar -xvf busybox-1.37.0.tar.bz2再进行配置,还是使用默认配置:
work@HOME:~/code/busybox-1.37.0$ make defconfig不过需要注意使用静态编译,因为它依赖的动态库在前面内核构成的系统中并不存在,会导致异常。
这里需要修改.config(通过make defconfig生成)中的如下配置:
## Build Options## CONFIG_STATIC is not set可以通过make menuconfig打开图形配置界面,勾选“Settings”下的如下配置:

另外由于WSL内核版本与busybox的版本兼容问题,还需要关闭另外的选项(当然也可以通过更新busybox代码来解决),它是位于“Networking Utilities”下的如下配置:

之后就可以进行编译了:
work@HOME:~/code/busybox-1.37.0$ make -j$(nproc)最后安装到前面创建的目录中,这通过指定CONFIG_PREFIX参数来实现:
work@HOME:~/code/busybox-1.37.0$ make install CONFIG_PREFIX=~/code/build/rootfs/ /home/home/code/build/rootfs///bin/arch -> busybox /home/home/code/build/rootfs///bin/ash -> busybox /home/home/code/build/rootfs///bin/base32 -> busybox /home/home/code/build/rootfs///bin/base64 -> busybox /home/home/code/build/rootfs///bin/cat -> busybox /home/home/code/build/rootfs///bin/chattr -> busybox /home/home/code/build/rootfs///bin/chgrp -> busybox /home/home/code/build/rootfs///bin/chmod -> busybox /home/home/code/build/rootfs///bin/chown -> busybox# 后面略该步骤相当于往文件系统中安装了很多的工具。
查看之前创建的目录:
work@HOME:~/code/build/rootfs$ lsbin dev lib lib64 linuxrc proc sbin sys tmp usr可以看到多了很多新的内容。
work@HOME:~/code/build/rootfs$ sudo mknod dev/console c 5 1work@HOME:~/code/build/rootfs$ sudo mknod dev/null c 1 3创建init文件:
work@HOME:~/code/build/rootfs$ cat > init << EOF#!/bin/shmount -t proc none /procmount -t sysfs none /sysmount -t tmpfs none /tmpmount -t devtmpfs none /devexec /bin/shEOFwork@HOME:~/code/build/rootfs$ chmod +x ./initinit文件很重要,它是内核启动后在用户空间创建的第一个进程,它的核心作用是初始化系统运行环境、启动后续的系统服务,这里的init就是一个脚本程序,它调用的/bin/sh就是busybox安装的,开启了一个shell。
work@HOME:~/code/build/rootfs$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz如下所示:
work@HOME:~/code/build$ lsinitramfs.cpio.gz rootfsinitramfs.cpio.gz就是最终需要的。
这里使用Windows下的QEMU,执行如下的命令:
PS E:\Linux> qemu-system-x86_64.exe -kernel .\bzImage -initrd .\initramfs.cpio.gz -nographic -append "console=ttyS0 root=/dev/ram rw init=/init"参数说明如下:
-kernel:指定编译好的内核镜像文件。-initrd:指定根文件系统镜像文件。-nographic:无图形界面,使用终端输出。-append:内核启动参数:console=ttyS0:将控制台输出重定向到串口(适配 QEMU)。root=/dev/ram:根文件系统在内存中。rw:根文件系统可读写。init=/init:指定启动的第一个程序。最终能够正常启动:

前面的示例是直接使用内核和文件系统来进行启动,这里将内核和文件系统放到虚拟硬盘中进行启动,这更像是一般的桌面系统所做的事情。
root@HOME:/home/work/code/build# dd if=/dev/zero of=disk.img bs=1M count=6464+0 records in64+0 records out67108864 bytes (67 MB, 64 MiB) copied, 0.0244948 s, 2.7 GB/sroot@HOME:/home/work/code/build# mkfs.vfat disk.imgmkfs.fat 4.2 (2021-01-31)注意这里进行了简化,不在进行分区。
root@HOME:/home/work/code/build# mkdir tmproot@HOME:/home/work/code/build# mount -o loop disk.img ./tmp/root@HOME:/home/work/code/build# mkdir tmp/efiroot@HOME:/home/work/code/build# mkdir tmp/efi/bootroot@HOME:/home/work/code/build# cp grub.cfg bootx64.efi ./tmp/efi/boot/root@HOME:/home/work/code/build# cp bzImage initramfs.cpio.gz ./tmp/最终硬盘中的文件:
root@HOME:/home/work/code/build# tree ./tmp/./tmp/├── bzImage├── efi│ └── boot│ ├── bootx64.efi│ └── grub.cfg└── initramfs.cpio.gz3 directories, 4 filesbzImage和initramfs.cpio.gz已经在前面介绍过,bootx64.efi和grub.cfg将在下面进一步介绍。
root@HOME:/home/work/code/build# umount ./tmp/work@HOME:~/code$ wget https://ftp.gnu.org/gnu/grub/grub-2.06.tar.xz--2026-01-07 22:32:30-- https://ftp.gnu.org/gnu/grub/grub-2.06.tar.xzResolving ftp.gnu.org (ftp.gnu.org)... 209.51.188.20, 2001:470:142:3::bConnecting to ftp.gnu.org (ftp.gnu.org)|209.51.188.20|:443... connected.HTTP request sent, awaiting response... 200 OKLength: 6581924 (6.3M) [application/x-xz]Saving to: ‘grub-2.06.tar.xz’grub-2.06.tar.xz 100%[=================================================>] 6.28M 6.33KB/s in 23m 45s2026-01-07 22:54:31 (4.51 KB/s) - ‘grub-2.06.tar.xz’ saved [6581924/6581924]然后解压:
work@HOME:~/code$ tar -xvf grub-2.06.tar.xzwork@HOME:~/code/grub-2.06$ ./configure --target=x86_64 --with-platform=efi --disable-werror这里使用了几个参数:
--target参数用来指定对应的平台,这里指定了x86的64位平台。--with-platform用来指定用于UEFI。--disable-werror用来防止告警变成错误导致编译异常。work@HOME:~/code/grub-2.06$ make -j$(nproc)work@HOME:~/code/grub-2.06$ ./grub-mkimage -p /efi/boot -d ./grub-core/ -O x86_64-efi -o bootx64.efi boot linux part_msdos part_gpt fat normal serial efi_gop minicmd configfile参数的说明如下:
-p指定了grub.cfg、独立模块等文件所在的目录,这里指定与bootx64.efi在同一个目录/efi/boot。注意这个参数不能错,否则会找不到grub.cfg文件。-d指定了我们刚才编译出来的image和module的位置,因为它们才是我们需要的,而不是系统中已经存在的。-O指定了二进制的格式,我们需要x86平台下UEFI的格式。-o用来指定输出的文件名。work@HOME:~/code/grub-2.06$ ls ./grub-core/ -altotal 30692drwxr-xr-x 30 work work 36864 Jan 7 23:02 .drwxr-xr-x 15 work work 4096 Jan 7 23:02 ..drwxr-xr-x 2 work work 4096 Jan 7 23:02 .deps-coredrwxr-xr-x 2 work work 4096 Jan 7 23:02 .deps-util-rw-r--r-- 1 work work 0 Jan 7 23:02 .dirstamp-rw-r--r-- 1 work work 3265263 Jan 7 23:02 Makefile-rw-r--r-- 1 work work 20471 Mar 2 2021 Makefile.am-rw-r--r-- 1 work work 1135139 Jun 8 2021 Makefile.core.am-rw-r--r-- 1 work work 50130 Mar 2 2021 Makefile.core.def-rw-r--r-- 1 work work 3403 Jun 8 2021 Makefile.gcry.def-rw-r--r-- 1 work work 4431360 Jun 8 2021 Makefile.in-rw-r--r-- 1 work work 1151 Jan 7 23:02 acpi.marker-rw-r--r-- 1 work work 15872 Jan 7 23:02 acpi.mod-rw-r--r-- 1 work work 81600 Jan 7 23:02 acpi.module-rw-r--r-- 1 work work 0 Jan 7 23:02 adler32.marker-rw-r--r-- 1 work work 1952 Jan 7 23:02 adler32.mod# 后面略这里并没有包含所有模块,具体需要哪些文件可以根据实际情况选择。
set default=0 set timeout=0menuentry 'UEFI Linux' {set root=(hd0) linux /bzImage root=/dev/sda rw console=tty0 init=/init initrd /initramfs.cpio.gz}注意这里指定的set root=后的内容需要根据实际情况来设置,如果包含分区(比如GPT分区),则可能是写成(hd0,gpt1)这样的格式。可以在GRUB的Shell下通过ls命令查看到底有哪些硬盘。
这里使用Windows下的QEMU,执行的命令来自https://gitee.com/jiangwei0512/edk2-beni:
E:\Gitee\edk2-beni>boot.cmd start disk实际上执行的命令如下:
qemu-system-x86_64 -display gtk,show-menubar=off -m 1024 -machine q35,smm=on -drive if=pflash,format=raw,unit=0,file=OVMF.fd -drive format=raw,file=disk.img得到的结果:

这里创建一个简单的应用,代码如下:
#include <stdio.h>int main(void){printf("Hello world!\n");return 0;}通过gcc进行编译:
work@HOME:~/code/test$ gcc ./helloworld.c -static -o hellowork@HOME:~/code/test$ lltotal 780drwxr-xr-x 2 work work 4096 Jan 7 23:10 ./drwxr-xr-x 7 work work 4096 Jan 7 23:09 ../-rwxr-xr-x 1 work work 785368 Jan 7 23:10 hello*-rw-r--r-- 1 work work 84 Jan 7 23:10 helloworld.c注意参数-static,这样应用就不会依赖于外部的系统。
将这个应用放到前面创建的虚拟硬盘disk.img中并重新启动(示例中修改了内核的输出,变成从串口输出了,通过修改grub.cfg中的内核的console参数来完成):
~ # mount /dev/sda tmp[ 10.734360] mount (84) used greatest stack depth: 14632 bytes left~ # cd tmp/tmp # lsappdp.efi event.efi helloworld_signed.efiboot example.json memtest.efiefi hello startup.nsh[ 13.919596] ls (85) used greatest stack depth: 14064 bytes left/tmp # ./helloHello world!实际上前面的代码还有点问题,所以并没有真正进入硬盘的根文件系统(暂时不管),所以还需要挂载硬盘文件,然后执行其中的hello程序,得到上述的结果。