1. 项目缘起与目标定位
几年前,当我第一次尝试将Linux系统移植到一块全新的RISC-V开发板上时,那段经历至今记忆犹新。从交叉编译工具链的版本冲突,到内核启动参数的反复调试,再到根文件系统里一个个缺失的动态库,整个过程就像在黑暗中摸索拼图,每一步都可能踩坑。市面上关于嵌入式Linux的教程不少,但要么是基于成熟的ARM平台,步骤简化到近乎“一键完成”,掩盖了底层细节;要么是过于理论化,和实际动手操作之间隔着一道鸿沟。对于RISC-V这个正在蓬勃发展的开源指令集架构,一份从零开始、手把手、不回避任何“脏活累活”的实战指南,显得尤为稀缺。
这就是我启动“基于qemu-riscv从0开始构建嵌入式linux系统”这个系列专栏的初衷。我的目标非常明确:不使用任何现成的、封装好的构建系统或发行版工具(如Buildroot、Yocto),而是像二十年前的先驱们那样,亲手组合每一个部件。我们将从最底层的模拟器环境搭建开始,一步步编译出专属的交叉工具链,配置、编译Linux内核,制作最小的根文件系统,最终在QEMU模拟的RISC-V虚拟硬件上,引导出一个完全由自己掌控的、极简的Linux系统。这个过程,你会清晰地看到bootloader如何把控制权交给内核,内核又如何挂载根文件系统并启动第一个用户态进程。这不仅仅是完成一个“Hello World”式的演示,而是深入理解嵌入式Linux系统骨架的绝佳机会。
这个系列适合谁?如果你是一名对操作系统原理感兴趣的学生,希望将书本上的概念落到实处;如果你是一名嵌入式软件工程师,习惯了在SDK基础上开发,想揭开底层的神秘面纱;或者你是一位RISC-V技术的爱好者,想找一个有挑战性的动手项目——那么,这个系列就是为你准备的。我们不需要昂贵的物理开发板,一台普通的电脑和足够的耐心就是全部所需。最终,你获得的将不仅仅是一个能跑起来的系统镜像,更是一套能够举一反三、应对各种定制化需求的底层构建能力。
2. 核心工具链与平台选型解析
工欲善其事,必先利其器。在开始漫长的构建之旅前,我们必须对核心工具做出明确的选择,并理解其背后的原因。这直接决定了后续所有步骤的顺畅程度。
2.1 为什么是QEMU与RISC-V?
QEMU是我们的核心实验平台。它是一个功能强大且开源的全系统模拟器。对于本项目而言,选择QEMU而非物理开发板,有三大不可替代的优势:
- 零成本与高可复现性:无需购买特定的RISC-V硬件,任何x86或ARM架构的宿主机都能运行。实验环境可以轻易保存、复制和分享,确保教程中的每一步结果对所有读者都是一致的。
- 极佳的调试支持:QEMU提供了GDB调试接口,我们可以单步跟踪内核的启动代码,观察寄存器和内存的变化。这种能力在物理板上往往需要昂贵的硬件调试器(如JTAG)才能实现。
- 灵活的硬件模拟:我们可以为虚拟机配置特定的CPU核心数、内存大小、外设类型(如UART、virtio磁盘、网络设备),这有助于我们理解硬件描述(Device Tree)如何与驱动交互。
RISC-V是我们构建的目标架构。选择它,不仅仅因为其开源和开放的特性,更因为它在嵌入式与学术领域的独特地位。与ARM架构相比,RISC-V的指令集手册完全公开,没有授权壁垒。这意味着我们在学习过程中,可以毫无阻碍地查阅指令集细节,甚至去阅读相关工具链(如GCC)的源码来理解ABI(应用程序二进制接口)。从最简单的rv32ima到支持多核与虚拟化的rv64gc,我们可以从基础子集开始,逐步深入。本次构建,我们将选择主流的rv64gc作为目标,即64位,支持整数(I)、乘法(M)、原子操作(A)、单精度浮点(F)、双精度浮点(D)以及压缩指令(C)扩展,这是一个功能相对完整的通用配置。
2.2 交叉编译工具链:自制还是使用预编译?
这是第一个关键决策点。交叉编译工具链(Cross Compilation Toolchain)是在宿主机(比如你的x86_64电脑)上运行,但生成目标机(RISC-V)可执行代码的一套程序,主要包括编译器(gcc)、链接器(ld)、库(glibc等)和二进制工具(objdump, strip等)。
方案一:使用预编译工具链。例如,SiFive或Bootlin提供的预构建版本。优点是快,下载解压即可用。缺点是版本固定,可能不包含你需要的特定功能或库,且无法定制C库(比如你想用musl libc替代glibc以追求更小的体积)。
方案二:使用Crosstool-NG等工具自动化构建。这是一个折中方案,它通过配置菜单引导你选择架构、C库、GCC版本等,然后自动下载源码并编译。它比方案一更灵活,但抽象掉了很多构建细节。
方案三:完全手动构建。这正是本系列要走的“硬核”路线。我们将从GNU官网下载binutils、gcc、glibc等组件的源码,按照严格的依赖顺序和配置参数,手动执行configure,make,make install。这个过程极其繁琐,极易出错,但价值巨大。你会彻底明白工具链各组件间的依赖关系(例如,编译gcc需要目标系统的头文件,而头文件来自glibc,但编译glibc又需要目标系统的编译器,这就产生了“鸡生蛋蛋生鸡”的循环依赖,需要分阶段构建),理解--target、--prefix、--with-sysroot这些关键参数的意义,并拥有从源码构建任何架构工具链的能力。
实操心得:对于第一次尝试的读者,我强烈建议在跟随本系列手动构建一次之后,为日常开发保存一份预编译或Crosstool-NG生成的工具链。手动构建是“修炼内功”,而高效的日常工具是“提高生产力”,两者并不矛盾。
2.3 宿主操作系统与基础环境准备
宿主机操作系统推荐使用Linux发行版,如Ubuntu 22.04 LTS或Fedora。在Windows或macOS上,可以通过WSL2或虚拟机安装一个Linux环境。选择Linux宿主的根本原因在于,我们构建的整个工具链和流程,其脚本、命令和环境都与Linux生态无缝契合,可以避免在Windows上因路径、符号链接、脚本解释器(如bash)差异带来的无数隐性问题。
在开始之前,我们需要安装一些基础的开发包。以下以Ubuntu/Debian为例:
sudo apt update sudo apt install build-essential git wget flex bison libncurses-dev libssl-dev \ libelf-dev python3-dev texinfo automake autoconf pkg-config \ gperf libtool ninja-build meson这些包涵盖了从源码编译所需的编译器、库、配置工具和文档生成器。build-essential是基础编译套件;flex和bison用于语法分析;libncurses-dev是make menuconfig界面所必需的;libssl-dev和libelf-dev是编译新版内核和某些工具所必需的。
3. 项目整体构建路线图
构建一个完整的、可启动的嵌入式Linux系统,其核心组件和流程是一个经典的依赖链条。下图清晰地展示了从零到一的完整路径,以及各关键组件之间的依赖关系:
flowchart TD A[宿主机环境准备<br>安装基础编译工具] --> B[手动构建RISC-V交叉编译工具链] subgraph B [工具链构建(解决循环依赖)] B1[1. 编译Binutils] --> B2[2. 编译GCC第一阶段<br>(仅编译器,无C库)] B2 --> B3[3. 编译目标系统Linux内核头文件] B3 --> B4[4. 编译Glibc C库] B4 --> B5[5. 编译完整的GCC<br>(含编译器与标准库)] end B --> C[编译BusyBox<br>提供用户态核心命令] B --> D[编译Linux内核<br>生成内核镜像vmlinux与设备树] C --> E[制作初始内存盘initramfs] D --> F[制作根文件系统rootfs<br>整合BusyBox与运行时库] E --> G[整合最终系统镜像<br>内核+initramfs或内核+磁盘rootfs] F --> G G --> H[使用QEMU加载系统镜像<br>完成从零启动]这个流程图是本系列的行动总纲。它揭示了一个关键挑战:工具链的构建本身就是一个“先有鸡还是先有蛋”的循环依赖问题。我们需要一个编译器来编译C库,但C库的头文件又是编译器运行时所必需的。因此,我们必须采用分阶段构建的策略,如图中步骤B1至B5所示。这是整个项目第一个,也是最重要的技术难点。后续所有步骤,包括BusyBox、内核的编译,都依赖于这套我们亲手打造的、可靠的交叉工具链。
整个构建过程可以概括为四个主要阶段:
- 基础工具链打造:解决循环依赖,生成
riscv64-unknown-linux-gnu-为前缀的系列工具。 - 内核与核心用户态编译:用自制的工具链编译Linux内核和BusyBox。
- 系统镜像组装:创建根文件系统,将内核、BusyBox、必要的配置文件和库文件整合到一起,形成可启动的镜像。
- 启动与调试:配置QEMU,加载镜像,观察启动日志,使用GDB进行内核调试。
每一步我们都将深入细节,解释每个配置选项的作用,并记录可能遇到的错误及其解决方法。
4. 第一阶段实操:手动构建RISC-V交叉工具链
现在,让我们开始最富挑战性也最有价值的一步:手动构建交叉工具链。我们将按照binutils->gcc (bootstrap)->linux headers->glibc->gcc (full)的顺序进行。
4.1 准备工作目录与源码下载
首先,创建一个清晰的工作目录结构,这有助于管理源码、构建文件和安装路径。
export TOPDIR=$(pwd) export RISCV=$TOPDIR/toolchain-install # 工具链安装路径 export PATH=$RISCV/bin:$PATH mkdir -p $TOPDIR/src $TOPDIR/build $RISCV cd $TOPDIR/src我们将所有源码下载到src目录,在build目录中进行编译(保持源码目录纯净),最终安装到toolchain-install目录。
接下来,下载所需版本的源码包。版本的选择需要兼顾兼容性,这里给出一个经过验证的组合:
wget https://ftp.gnu.org/gnu/binutils/binutils-2.42.tar.xz wget https://ftp.gnu.org/gnu/gcc/gcc-13.2.0/gcc-13.2.0.tar.xz wget https://ftp.gnu.org/gnu/glibc/glibc-2.38.tar.xz wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz wget https://ftp.gnu.org/gnu/gmp/gmp-6.3.0.tar.xz wget https://ftp.gnu.org/gnu/mpfr/mpfr-4.2.1.tar.xz https://ftp.gnu.org/gnu/mpc/mpc-1.3.1.tar.gzGCC编译依赖GMP、MPFR、MPC这三个数学库。我们可以将它们解压到GCC源码目录内,GCC构建脚本会自动识别并使用它们。
tar -xf gmp-6.3.0.tar.xz && mv gmp-6.3.0 gcc-13.2.0/gmp tar -xf mpfr-4.2.1.tar.xz && mv mpfr-4.2.1 gcc-13.2.0/mpfr tar -xf mpc-1.3.1.tar.gz && mv mpc-1.3.1 gcc-13.2.0/mpc4.2 第一步:编译Binutils
Binutils是二进制工具的集合,包括汇编器(as)、链接器(ld)、静态库生成器(ar)等,是编译其他一切的基础。
cd $TOPDIR/build mkdir binutils && cd binutils ../../src/binutils-2.42/configure \ --prefix=$RISCV \ --target=riscv64-unknown-linux-gnu \ --with-sysroot=$RISCV/sysroot \ --disable-multilib \ --disable-nls make -j$(nproc) make install关键参数解析:
--target=riscv64-unknown-linux-gnu:定义目标系统三元组。格式为架构-厂商-系统-ABI。riscv64指64位RISC-V,unknown表示厂商未知或通用,linux指目标系统,gnu指使用GNU的C库(glibc)及ABI。--with-sysroot=$RISCV/sysroot:指定目标系统的根目录。工具链将在该目录下寻找目标系统的头文件和库。目前这个目录是空的,我们后续会填充。--disable-multilib:禁用多库支持。因为我们只构建rv64gc一种配置,不需要同时生成32位库,可以简化构建。--disable-nls:禁用本地化支持,可以加快编译速度并减少依赖。
安装成功后,$RISCV/bin目录下会出现riscv64-unknown-linux-gnu-as、riscv64-unknown-linux-gnu-ld等工具。可以执行riscv64-unknown-linux-gnu-as --version验证。
4.3 第二步:编译GCC第一阶段(Bootstrap Compiler)
此时我们还没有目标系统的C库(glibc),因此只能先编译一个“裸”的GCC,它支持C语言编译,但无法链接标准库(因为库还不存在)。这个编译器仅用于编译后续的glibc。
cd $TOPDIR/build mkdir gcc-bootstrap && cd gcc-bootstrap ../../src/gcc-13.2.0/configure \ --prefix=$RISCV \ --target=riscv64-unknown-linux-gnu \ --with-sysroot=$RISCV/sysroot \ --enable-languages=c \ --without-headers \ --disable-shared \ --disable-threads \ --disable-libatomic \ --disable-libgomp \ --disable-libquadmath \ --disable-libssp \ --disable-libvtv \ --disable-libstdcxx \ --disable-multilib \ --disable-nls make -j$(nproc) all-gcc make install-gcc关键参数解析:
--enable-languages=c:只编译C语言前端,足矣。--without-headers:明确告诉GCC,目标系统还没有C库头文件。这正是bootstrap阶段的核心标志。- 一系列
--disable-*:禁用所有依赖C库或线程等复杂运行时的组件,确保这个初始编译器能独立构建出来。
4.4 第三步:安装Linux内核头文件
C库(glibc)需要知道目标系统内核提供了哪些系统调用和数据结构定义,这些信息通过内核头文件暴露。我们需要从Linux内核源码中提取出纯净的、与架构无关的头文件。
cd $TOPDIR/src/linux-6.6 make ARCH=riscv INSTALL_HDR_PATH=$RISCV/sysroot/usr headers_install这个命令会将内核的通用头文件(如asm-generic/)和RISC-V架构特定的头文件(asm/)安装到$RISCV/sysroot/usr/include目录下。sysroot目录开始有内容了。
4.5 第四步:编译与安装Glibc
现在,我们有了bootstrap编译器、binutils和内核头文件,终于可以编译完整的C库了。Glibc的构建需要在一个独立的目录中进行。
cd $TOPDIR/build mkdir glibc && cd glibc ../../src/glibc-2.38/configure \ --prefix=/usr \ --host=riscv64-unknown-linux-gnu \ --with-headers=$RISCV/sysroot/usr/include \ --disable-werror \ libc_cv_forced_unwind=yes \ libc_cv_c_cleanup=yes make -j$(nproc) make install DESTDIR=$RISCV/sysroot关键参数解析:
--host=riscv64-unknown-linux-gnu:指定运行编译结果的主机(即目标机)。配置脚本会根据这个值寻找我们之前安装的bootstrap交叉编译器。--prefix=/usr:指定glibc在目标根文件系统中的安装路径。注意,这里不是$RISCV,因为glibc是安装在目标系统的/usr目录下。--with-headers:指定内核头文件的位置。DESTDIR=$RISCV/sysroot:这是make install时的参数,而非configure的。它指定一个“临时根目录”,所有文件将被安装到$RISCV/sysroot/usr下,而不是真正的系统/usr。这是交叉编译中的标准做法。
注意事项:Glibc的编译是对工具链稳定性的第一次重大考验。你可能会遇到关于
libc_cv_forced_unwind和libc_cv_c_cleanup的配置错误。这是因为bootstrap编译器功能不全,无法运行某些测试程序。通过环境变量直接设置yes可以绕过这些测试。如果遇到其他测试失败,需要根据具体错误信息,在搜索引擎或社区寻找对应的解决方案,这通常是手动构建过程中最耗时的部分。
4.6 第五步:编译完整的GCC
Glibc就位后,我们可以重新编译一个功能完整的GCC,它支持C、C++等语言,并能链接到我们刚编译好的glibc。
cd $TOPDIR/build mkdir gcc-full && cd gcc-full ../../src/gcc-13.2.0/configure \ --prefix=$RISCV \ --target=riscv64-unknown-linux-gnu \ --with-sysroot=$RISCV/sysroot \ --enable-languages=c,c++ \ --disable-multilib \ --disable-nls make -j$(nproc) make install这次配置去掉了--without-headers等限制性选项。编译安装完成后,我们的交叉工具链就彻底准备好了。使用riscv64-unknown-linux-gnu-gcc -v查看版本,并使用一个简单的Hello World程序测试:
// hello.c #include <stdio.h> int main() { printf("Hello, RISC-V!\n"); return 0; }编译并检查文件类型:
riscv64-unknown-linux-gnu-gcc hello.c -o hello.rv64 file hello.rv64 # 应输出:hello.rv64: ELF 64-bit LSB executable, UCB RISC-V, ... for GNU/Linux ...如果看到RISC-V和dynamically linked的字样,恭喜你,最艰难的一关已经通过。
5. 第二阶段实操:编译Linux内核与BusyBox
拥有了得心应手的工具链,接下来的工作会顺畅许多。我们将编译系统的“大脑”(内核)和“手脚”(用户态基础命令集)。
5.1 配置与编译Linux内核
Linux内核的构建系统(Kbuild)非常成熟。我们主要使用make menuconfig进行图形化配置。
cd $TOPDIR/src/linux-6.6 make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- menuconfig在配置界面中,我们需要关注几个核心设置:
- General setup->Cross-compiler prefix:确认已自动设置为
$(CROSS_COMPILE)。 - Platform type->RISCV platform type:选择
(X) Virtio MMIO。QEMU的virt机器默认使用Virtio设备,这是一个在虚拟化环境中高效、标准化的I/O框架。 - Kernel Features:确保
(X) 64-bit kernel被选中。 - Device Drivers->Character devices->Serial drivers:确保
(X) 8250/16550 and compatible serial support和(X) Support for console on 8250/16550 and compatible serial port被启用。这是QEMUvirt机器默认的串口,是内核启动早期和用户交互的唯一通道。 - File systems-> 为了支持后续的根文件系统,需要启用
(X) ext4等文件系统支持。对于初期的initramfs,需要启用(X) Initial RAM filesystem and RAM disk (initramfs/initrd) support。
配置完成后,保存退出,开始编译:
make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- -j$(nproc)编译成功后,主要生成两个文件:
arch/riscv/boot/Image:压缩的内核镜像。arch/riscv/boot/dts/riscv/virt.dtb:设备树二进制文件(Device Tree Blob)。它以一种硬件描述语言(DTB)的形式,告诉内核QEMUvirt机器上有什么硬件(CPU、内存、中断控制器、串口、磁盘等)以及它们的地址。
5.2 编译BusyBox:用户态的瑞士军刀
BusyBox将上百个常用的Unix命令(如ls,cp,sh,mount)集成进一个单一的可执行文件,通过创建符号链接来调用不同的功能,是嵌入式系统根文件系统的绝对核心。
cd $TOPDIR/src wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 tar -xf busybox-1.36.1.tar.bz2 cd busybox-1.36.1配置BusyBox,同样使用菜单界面:
make CROSS_COMPILE=riscv64-unknown-linux-gnu- menuconfig关键配置:
- Settings->Build Options->Build static binary (no shared libs):强烈建议勾选。这将BusyBox编译成静态链接,不依赖动态库。这能确保它在任何环境下都能独立运行,极大简化了根文件系统的制作。缺点是二进制文件体积会稍大。
- Settings->Installation Options->Destination path for ‘make install’:可以设置为
$TOPDIR/rootfs,方便后续操作。
配置完成后,编译并安装:
make CROSS_COMPILE=riscv64-unknown-linux-gnu- -j$(nproc) make CONFIG_PREFIX=$TOPDIR/rootfs install安装后,$TOPDIR/rootfs目录下会出现bin,sbin,usr/bin等目录,里面是BusyBox二进制文件和指向它的各种符号链接(如ls -> busybox)。
6. 第三阶段实操:制作根文件系统与整合镜像
系统启动的最后一个环节,是为内核提供一个包含初始程序和必要文件的“家园”——根文件系统。
6.1 构建最小根文件系统
我们首先创建一个基本的根文件系统目录结构,并将BusyBox安装进去。
cd $TOPDIR mkdir -p rootfs/{bin,sbin,etc,proc,sys,dev,lib,usr/{lib,bin,sbin},tmp} cp -a src/busybox-1.36.1/_install/* rootfs/由于我们编译的是静态版BusyBox,lib目录暂时是空的。但为了系统的完整性,我们需要创建几个必要的设备节点(在真实的Linux系统中,/dev下的文件是特殊的设备文件)。
sudo mknod rootfs/dev/console c 5 1 sudo mknod rootfs/dev/null c 1 3console节点是系统控制台,null是空设备。c表示字符设备,后面的数字是主设备号和次设备号。
接下来,创建最简化的初始化脚本。内核启动后,会尝试执行根文件系统中的/init或/sbin/init程序。我们用BusyBox提供的init,并配置一个简单的inittab。
# 创建初始化脚本 cat > rootfs/init << 'EOF' #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs none /dev echo -e "\nWelcome to My RISC-V Linux!\n" exec /bin/sh EOF chmod +x rootfs/init # 创建inittab (BusyBox init会读取) mkdir -p rootfs/etc/init.d cat > rootfs/etc/inittab << 'EOF' ::sysinit:/etc/init.d/rcS ::askfirst:-/bin/sh ::ctrlaltdel:/sbin/reboot ::shutdown:/sbin/swapoff -a ::shutdown:/bin/umount -a -r ::restart:/sbin/init EOF cat > rootfs/etc/init.d/rcS << 'EOF' #!/bin/sh mount -a EOF chmod +x rootfs/etc/init.d/rcS cat > rootfs/etc/fstab << 'EOF' proc /proc proc defaults 0 0 sysfs /sys sysfs defaults 0 0 devtmpfs /dev devtmpfs defaults 0 0 EOF6.2 制作initramfs镜像
Initramfs是一个被压缩的cpio归档文件,内核在启动早期会将其解压到内存中作为一个临时的根文件系统。这是最简单快速的启动方式。
cd $TOPDIR/rootfs find . | cpio -H newc -o | gzip -9 > ../initramfs.cpio.gz现在,我们拥有了启动所需的所有部件:内核镜像(Image)、设备树(virt.dtb)和初始内存盘(initramfs.cpio.gz)。
7. 第四阶段实操:QEMU启动与问题排查
激动人心的时刻到了,让我们启动这个亲手打造的系统。
7.1 基础启动命令与参数解析
使用QEMU的virt机器进行模拟:
cd $TOPDIR qemu-system-riscv64 \ -machine virt \ -cpu rv64 \ -m 256M \ -kernel src/linux-6.6/arch/riscv/boot/Image \ -dtb src/linux-6.6/arch/riscv/boot/dts/riscv/virt.dtb \ -initrd initramfs.cpio.gz \ -append "console=ttyS0 earlycon=sbi root=/dev/ram rdinit=/init" \ -nographic \ -serial mon:stdio关键参数解析:
-machine virt:指定模拟的机器类型为QEMU的通用RISC-V虚拟平台。-cpu rv64:指定CPU为通用的64位RISC-V。-m 256M:分配256MB内存。-kernel,-dtb,-initrd:分别指定内核、设备树和initramfs镜像。-append:传递给内核的命令行参数。console=ttyS0:指定控制台设备为第一个串口。earlycon=sbi:启用早期控制台输出(通过SBI,即RISC-V Supervisor Binary Interface)。root=/dev/ram:告诉内核根文件系统在RAM磁盘上(即我们的initramfs)。rdinit=/init:指定initramfs中的初始化程序为/init。
-nographic:禁用图形界面,完全使用命令行。-serial mon:stdio:将虚拟串口重定向到标准输入输出,这样我们就可以在终端里与虚拟机交互。
如果一切顺利,你将看到内核解压、启动、挂载文件系统,最后打印出Welcome to My RISC-V Linux!并进入BusyBox的shell提示符。你可以运行ls,mount,ps等命令进行探索。
7.2 常见启动问题与排查实录
第一次启动很少能一帆风顺。以下是几个我踩过的坑及排查思路:
问题一:内核在Starting kernel ...后卡住或无输出。
- 排查:这通常意味着内核崩溃在了非常早的阶段。首先检查串口配置。确保内核配置中
CONFIG_SERIAL_8250和CONFIG_SERIAL_8250_CONSOLE已启用。其次,检查QEMU命令行中的-append参数是否正确指定了console=ttyS0。可以尝试添加更详细的早期打印参数,如earlyprintk(但新内核可能已移除)。 - 进阶调试:使用QEMU的
-S -s参数启动,它会暂停CPU并开启一个GDB调试服务(默认端口1234)。在另一个终端用GDB连接:
然后观察卡在哪里。或者在内核命令行添加riscv64-unknown-linux-gnu-gdb src/linux-6.6/vmlinux (gdb) target remote localhost:1234 (gdb) cpanic=5,让内核在panic时等待更久,方便查看最后的信息。
问题二:内核提示VFS: Cannot open root device ...或Failed to execute /init。
- 排查:这是根文件系统相关的问题。首先确认
-initrd指定的路径和文件名正确。其次,检查initramfs.cpio.gz文件是否制作正确,可以用gunzip -c initramfs.cpio.gz | cpio -t列出其中文件,看/init是否存在且可执行。最后,检查内核配置是否启用了CONFIG_BLK_DEV_INITRD和CONFIG_DEVTMPFS。
问题三:进入shell后,命令无法执行或提示-sh: xxx: not found。
- 排查:如果你编译的是动态链接的BusyBox,而根文件系统里缺少相应的动态库(
/lib/ld-linux-riscv64-*.so.1和/lib/libc.so.6等),就会出现此问题。解决方案是:1)重新编译BusyBox为静态版本(推荐)。2)或者,将工具链sysroot里的相关库文件拷贝到根文件系统的/lib目录下。可以使用riscv64-unknown-linux-gnu-readelf -d busybox查看动态依赖。
问题四:系统启动后,键盘输入无反应。
- 排查:QEMU在
-nographic模式下,为了退出虚拟机,默认使用了Ctrl+A C等组合键。这有时会干扰输入。可以尝试在QEMU命令中添加-echr 2来改变转义字符。更简单的方法是,要退出QEMU,先按Ctrl+A,松开后再按X。
8. 从Initramfs到磁盘镜像:系统的持久化
基于initramfs的系统运行在内存中,所有改动在关机后都会丢失。为了构建一个更接近真实场景的、可持久化的系统,我们需要创建磁盘镜像,并将根文件系统安装到其中。
8.1 创建并格式化磁盘镜像
使用dd和mkfs.ext4创建一个空的磁盘镜像文件并格式化为ext4文件系统。
cd $TOPDIR dd if=/dev/zero of=rootfs.img bs=1M count=64 mkfs.ext4 rootfs.img这个命令创建了一个64MB的镜像文件。你可以将其挂载到宿主机的一个目录,以便向其中拷贝文件。
mkdir -p mnt sudo mount -o loop rootfs.img mnt8.2 向磁盘镜像中部署根文件系统
将我们之前制作的rootfs目录全部拷贝到挂载点,并创建必要的设备节点。
sudo cp -a rootfs/* mnt/ sudo mknod mnt/dev/console c 5 1 sudo mknod mnt/dev/null c 1 3 sudo umount mnt现在,rootfs.img就是一个包含了完整根文件系统的ext4格式磁盘镜像。
8.3 修改启动参数以从磁盘启动
我们需要修改内核命令行参数,告诉内核从磁盘的特定分区寻找根文件系统,并指定根文件系统的类型。
qemu-system-riscv64 \ -machine virt \ -cpu rv64 \ -m 256M \ -kernel src/linux-6.6/arch/riscv/boot/Image \ -dtb src/linux-6.6/arch/riscv/boot/dts/riscv/virt.dtb \ -drive file=rootfs.img,format=raw,id=hd0 \ -device virtio-blk-device,drive=hd0 \ -append "console=ttyS0 earlycon=sbi root=/dev/vda rootfstype=ext4 rw" \ -nographic \ -serial mon:stdio关键变更:
-drive:指定磁盘镜像文件。-device virtio-blk-device:为虚拟机添加一个Virtio块设备,并将其关联到上面的驱动器。在QEMUvirt机器中,第一个Virtio块设备在系统内通常显示为/dev/vda。-append:root=/dev/vda指定根设备为第一个Virtio磁盘。rootfstype=ext4指定文件系统类型。rw以读写模式挂载。
启动后,系统将从磁盘镜像加载根文件系统。你可以在其中创建文件,关机后再启动,文件依然存在。这为我们后续安装更多软件包、配置网络等操作奠定了基础。
走到这一步,你已经完成了一个从源码开始,涵盖工具链、内核、根文件系统的完整嵌入式Linux构建循环。这个过程虽然曲折,但每一个错误和解决过程,都让你对系统各组件间的联系有了更深刻的理解。这不仅仅是构建了一个系统,更是获得了一把理解复杂系统的钥匙。在后续的章节中,我们可以在此基础上,继续探索内核模块的编译、设备树的深入解析、网络功能的添加,甚至移植一个轻量级的图形界面。