news 2026/8/26 1:38:34

从零手动构建RISC-V嵌入式Linux:QEMU实战与工具链深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零手动构建RISC-V嵌入式Linux:QEMU实战与工具链深度解析

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而非物理开发板,有三大不可替代的优势:

  1. 零成本与高可复现性:无需购买特定的RISC-V硬件,任何x86或ARM架构的宿主机都能运行。实验环境可以轻易保存、复制和分享,确保教程中的每一步结果对所有读者都是一致的。
  2. 极佳的调试支持:QEMU提供了GDB调试接口,我们可以单步跟踪内核的启动代码,观察寄存器和内存的变化。这种能力在物理板上往往需要昂贵的硬件调试器(如JTAG)才能实现。
  3. 灵活的硬件模拟:我们可以为虚拟机配置特定的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是基础编译套件;flexbison用于语法分析;libncurses-devmake menuconfig界面所必需的;libssl-devlibelf-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、内核的编译,都依赖于这套我们亲手打造的、可靠的交叉工具链。

整个构建过程可以概括为四个主要阶段:

  1. 基础工具链打造:解决循环依赖,生成riscv64-unknown-linux-gnu-为前缀的系列工具。
  2. 内核与核心用户态编译:用自制的工具链编译Linux内核和BusyBox。
  3. 系统镜像组装:创建根文件系统,将内核、BusyBox、必要的配置文件和库文件整合到一起,形成可启动的镜像。
  4. 启动与调试:配置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.gz

GCC编译依赖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/mpc

4.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:定义目标系统三元组。格式为架构-厂商-系统-ABIriscv64指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-asriscv64-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_unwindlibc_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-Vdynamically 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

在配置界面中,我们需要关注几个核心设置:

  1. General setup->Cross-compiler prefix:确认已自动设置为$(CROSS_COMPILE)
  2. Platform type->RISCV platform type:选择(X) Virtio MMIO。QEMU的virt机器默认使用Virtio设备,这是一个在虚拟化环境中高效、标准化的I/O框架。
  3. Kernel Features:确保(X) 64-bit kernel被选中。
  4. 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机器默认的串口,是内核启动早期和用户交互的唯一通道。
  5. 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

关键配置:

  1. Settings->Build Options->Build static binary (no shared libs)强烈建议勾选。这将BusyBox编译成静态链接,不依赖动态库。这能确保它在任何环境下都能独立运行,极大简化了根文件系统的制作。缺点是二进制文件体积会稍大。
  2. 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 3

console节点是系统控制台,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 EOF

6.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_8250CONFIG_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) c
    然后观察卡在哪里。或者在内核命令行添加panic=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_INITRDCONFIG_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 创建并格式化磁盘镜像

使用ddmkfs.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 mnt

8.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
  • -appendroot=/dev/vda指定根设备为第一个Virtio磁盘。rootfstype=ext4指定文件系统类型。rw以读写模式挂载。

启动后,系统将从磁盘镜像加载根文件系统。你可以在其中创建文件,关机后再启动,文件依然存在。这为我们后续安装更多软件包、配置网络等操作奠定了基础。

走到这一步,你已经完成了一个从源码开始,涵盖工具链、内核、根文件系统的完整嵌入式Linux构建循环。这个过程虽然曲折,但每一个错误和解决过程,都让你对系统各组件间的联系有了更深刻的理解。这不仅仅是构建了一个系统,更是获得了一把理解复杂系统的钥匙。在后续的章节中,我们可以在此基础上,继续探索内核模块的编译、设备树的深入解析、网络功能的添加,甚至移植一个轻量级的图形界面。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/26 1:38:28

28岁转行网络安全:零基础学习路径与求职策略

1. 转型背景与行业认知28岁转行网络安全听起来像是个冒险的决定&#xff0c;但作为过来人&#xff0c;我可以明确告诉你&#xff1a;这个行业对"半路出家"的从业者异常友好。去年某安全厂商的行业报告显示&#xff0c;32%的从业者都是25岁后转行进入的&#xff0c;其…

作者头像 李华
网站建设 2026/8/26 1:27:03

STM32开发环境搭建:Keil MDK从安装到调试完整指南

1. 项目概述&#xff1a;为什么Keil是STM32开发的“瑞士军刀”&#xff1f;如果你刚拿到一块STM32开发板&#xff0c;准备大展拳脚&#xff0c;第一道坎往往不是写代码&#xff0c;而是搭建开发环境。在众多选择中&#xff0c;Keil MDK&#xff08;Microcontroller Development…

作者头像 李华
网站建设 2026/8/26 1:24:34

学习产品生命周期管理系统 —— 产品生命周期管理系统之十一

经理发现项目的推进速度在下降&#xff0c;他的记事本里几乎每个工作项目都是延迟的状态。整个进度受到各种问题的影响。公司的会议室里&#xff0c;针对项目近期遇到的问题&#xff0c;各级主管在开会。会议的结论是&#xff0c;目前项目的进展不够顺利&#xff0c;原因是多方…

作者头像 李华
网站建设 2026/8/26 1:23:39

Vibe Coding 开发工作流:从自然语言到可运行软件的实践指南

这里写自定义目录标题欢迎使用Markdown编辑器一、什么是 Vibe Coding&#xff1a;一场编程范式的变革二、Vibe Coding 的核心工作循环三、需求描述&#xff1a;Vibe Coding 的"第一生产力"四、代码审查&#xff1a;Vibe Coding 的"安全底线"五、实战案例&a…

作者头像 李华
网站建设 2026/8/26 1:21:04

齐纳二极管原理与选型:从反向击穿到钳位应用,避开稳压坑

我做硬件这几年&#xff0c;有个特别深的感受&#xff1a;很多入门时觉得“理所当然”的器件&#xff0c;恰恰是后来项目里最容易翻车的地方。齐纳二极管就是典型。教科书上叫它“稳压二极管”&#xff0c;老师傅嘴里叫它“Zener”&#xff0c;几乎所有电源电路入门教程都会提到…

作者头像 李华