2. 开发环境搭建:交叉编译工具链配置、内核源码获取与分支管理、QEMU模拟环境搭建、文件系统制作

嵌入式Linux驱动开发与原生PC开发最大的区别在于交叉编译与目标板模拟。本章将系统搭建一套完整的开发环境,涵盖工具链配置、内核源码管理、QEMU虚拟化平台构建以及根文件系统制作。这套环境将贯穿后续所有驱动移植实验。

环境版本对齐:本课程所有实验基于 Ubuntu 20.04 LTS 主机,目标架构为 ARM Cortex-A9(vexpress-a9)。内核源码从 v3.16 逐步升级至 v5.10,工具链使用 gcc-arm-10.2-2020.11 系列。

2.1 交叉编译工具链配置

交叉编译工具链是嵌入式开发的基石。它运行在x86_64主机上,生成ARM架构的可执行代码。工具链的配置直接影响内核编译、驱动模块编译以及应用程序的二进制兼容性。

2.1.1 工具链选择与下载

推荐使用ARM官方维护的GNU Arm Embedded Toolchain,它经过严格测试,与主线内核兼容性最好。避免使用发行版仓库中过时的版本(如gcc-arm-linux-gnueabihf 4.8),否则可能遇到内核编译宏或内联汇编的兼容性问题。

工具链版本 适用内核范围 GCC版本 glibc版本 推荐度
gcc-arm-10.2-2020.11 v4.19 ~ v5.10+ 10.2.1 2.31 强烈推荐
gcc-arm-8.3-2019.03 v4.14 ~ v5.4 8.3.1 2.28 推荐
gcc-arm-7.2-2017.11 v3.16 ~ v4.9 7.2.1 2.26 兼容旧内核
apt安装(gcc-arm-linux-gnueabihf) v4.9 ~ v5.4 7.5.0 (Ubuntu 20.04) 2.27 仅用于快速验证

2.1.2 安装与路径配置

以下步骤以 gcc-arm-10.2-2020.11-x86_64-arm-linux-gnueabihf 为例,将其安装到 /opt/toolchains 目录。

# 1. 创建工具链目录
sudo mkdir -p /opt/toolchains
cd /opt/toolchains

# 2. 下载并解压(若网络受限,可先下载到本地再拷贝)
sudo wget https://developer.arm.com/-/media/Files/downloads/gnu-a/10.2-2020.11/binrel/gcc-arm-10.2-2020.11-x86_64-arm-linux-gnueabihf.tar.xz
sudo tar -xf gcc-arm-10.2-2020.11-x86_64-arm-linux-gnueabihf.tar.xz

# 3. 添加至系统PATH(写入 ~/.bashrc 或 /etc/profile)
export PATH=/opt/toolchains/gcc-arm-10.2-2020.11-x86_64-arm-linux-gnueabihf/bin:$PATH

# 4. 验证安装
arm-linux-gnueabihf-gcc --version
# 输出应包含 "arm-linux-gnueabihf-gcc (GNU Toolchain for the A-profile Architecture 10.2-2020.11) 10.2.1 20201103"
常见陷阱:不要将工具链路径追加在 $PATH 末尾,而应放在最前面,避免系统自带的x86_64 gcc被错误调用。建议在 ~/.bashrc 中显式 export,并在每个终端会话开始时 source ~/.bashrc。

2.1.3 工具链组成与测试

一个完整的交叉工具链包含编译器、汇编器、链接器、库以及调试工具。下表列出核心组件及其在内核编译中的角色:

可执行文件 作用 内核编译中的使用
arm-linux-gnueabihf-gcc C编译器 编译内核C代码、驱动模块
arm-linux-gnueabihf-ld 链接器 链接vmlinux、模块.ko
arm-linux-gnueabihf-objcopy 二进制格式转换 生成zImage、uImage
arm-linux-gnueabihf-objdump 反汇编器 调试内核崩溃、分析驱动代码
arm-linux-gnueabihf-gdb 调试器 通过QEMU的gdbstub调试内核

编写一个简单的测试程序,验证工具链能否正确生成ARM架构的可执行文件:

// test.c
#include <stdio.h>

int main(void)
{
    printf("Cross toolchain works!\n");
    return 0;
}
arm-linux-gnueabihf-gcc -static -o test test.c
file test
# 输出应包含 "ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked"
关键点:使用 -static 选项生成静态链接的可执行文件,避免在目标板上缺少动态库时运行失败。后续制作文件系统时,我们会将必要的动态库拷贝到根文件系统中。
交叉编译工具链工作流程 x86_64 主机 交叉工具链 编译 / 链接 / 转换 ARM 目标板 zImage / 模块.ko 说明: 主机侧工具链生成ARM指令集代码,通过scp/NFS传输到目标板或QEMU模拟器执行。 内核编译时通过 CROSS_COMPILE 变量指定前缀,例如:CROSS_COMPILE=arm-linux-gnueabihf-

2.2 内核源码获取与分支管理

驱动移植的核心工作是对比不同内核版本的API差异。因此,我们需要同时管理多个内核版本的源码树,并能够快速切换分支进行编译和测试。

2.2.1 获取主线内核源码

从 kernel.org 获取官方主线内核源码,使用git克隆整个仓库,以便后续切换标签和分支。

# 创建内核源码工作目录
mkdir -p ~/linux-kernel
cd ~/linux-kernel

# 克隆主线内核(完整历史,约2.5GB)
git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux

# 查看所有标签(按版本排序)
git tag -l 'v3.*' | sort -V | tail -5
git tag -l 'v5.*' | sort -V | tail -5

# 检出我们需要的版本
git checkout v3.16    # 3.x 基线版本
git checkout v4.4     # 4.x 过渡版本
git checkout v5.10    # 5.x 目标版本
节省空间技巧:如果磁盘空间有限,可以使用 git clone --depth=1 --branch v5.10 只克隆单个标签的浅层副本。但后续无法使用 git log 查看历史提交,建议至少保留 --depth=1000。

2.2.2 分支管理策略

为每个实验创建独立的工作分支,避免污染主线标签。推荐的分支命名规范:

分支名称 基版本 用途
exp/gpio/v3.16 v3.16 GPIO驱动移植实验(3.x基线)
exp/gpio/v5.10 v5.10 GPIO驱动移植实验(5.x目标)
exp/i2c/v3.16 v3.16 I2C驱动移植实验
exp/i2c/v5.10 v5.10 I2C驱动移植实验
fix/xxx 当前实验分支 修复编译错误或Bug
# 从v3.16创建实验分支
git checkout v3.16
git checkout -b exp/gpio/v3.16

# 从v5.10创建实验分支
git checkout v5.10
git checkout -b exp/gpio/v5.10

# 查看当前分支列表
git branch -a

2.2.3 内核配置与编译

针对QEMU模拟的 vexpress-a9 平台,使用默认配置并启用必要的驱动选项。

# 设置环境变量(每次编译前执行)
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-

# 使用vexpress默认配置
make vexpress_defconfig

# 进入菜单配置,添加调试符号和驱动选项
make menuconfig
# 建议开启:
#   Kernel hacking  --->  [*] Kernel debugging
#   Kernel hacking  --->  [*] Compile the kernel with debug info
#   Device Drivers  --->  [*] GPIO Support
#   Device Drivers  --->  [*] I2C Support

# 编译内核(使用-jN并行编译,N为CPU核心数)
make -j$(nproc) zImage
make -j$(nproc) modules
make -j$(nproc) dtbs

# 编译完成后,检查生成的文件
ls -lh arch/arm/boot/zImage
ls -lh arch/arm/boot/dts/vexpress-v2p-ca9.dtb
编译错误排查:如果遇到 undefined reference to `__aeabi_unwind_cpp_pr0' 等链接错误,通常是因为工具链版本与内核版本不匹配。对于v3.16内核,建议使用gcc-arm-7.2或更低版本;对于v5.10内核,使用gcc-arm-10.2。如果必须使用高版本工具链编译旧内核,可以在 arch/arm/Makefile 中添加 KBUILD_CFLAGS += -fno-unwind-tables 临时规避。
内核源码分支管理策略 v3.16 (主线标签) v5.10 (主线标签) exp/gpio/v3.16 exp/gpio/v5.10 exp/i2c/v3.16 exp/i2c/v5.10 fix/gpio-build 分支管理原则: 1. 每个实验从对应主线标签创建独立分支,避免交叉影响。 2. 修复分支基于实验分支创建,修复后合并回实验分支,不直接修改主线标签。

2.3 QEMU模拟环境搭建

QEMU(Quick EMUlator)是嵌入式Linux开发中不可或缺的虚拟化工具。它能够模拟完整的ARM开发板,使我们无需硬件即可验证内核启动、驱动加载和应用程序运行。

2.3.1 QEMU安装与版本选择

Ubuntu 20.04 默认仓库中的 QEMU 版本为 4.2.1,足以支持 vexpress-a9 模拟。如果需要更精确的模拟特性(如 GICv3、PCIe RC),建议从源码编译 QEMU 5.2+。

# 安装QEMU及相关工具
sudo apt-get update
sudo apt-get install qemu-system-arm qemu-efi-arm qemu-utils

# 验证安装
qemu-system-arm --version
# 输出示例: QEMU emulator version 4.2.1 (Debian 1:4.2-3ubuntu6.24)

2.3.2 启动内核与设备树

使用 QEMU 启动我们刚刚编译的内核,验证内核能否正常初始化并挂载根文件系统。

# 基本启动命令(不挂载文件系统,仅验证内核启动)
qemu-system-arm \
    -M vexpress-a9 \
    -m 512M \
    -kernel ~/linux-kernel/linux/arch/arm/boot/zImage \
    -dtb ~/linux-kernel/linux/arch/arm/boot/dts/vexpress-v2p-ca9.dtb \
    -nographic \
    -append "console=ttyAMA0"

# 如果看到内核启动日志,最后停在 "Kernel panic - not syncing: VFS: Unable to mount root fs"
# 说明内核启动成功,只是缺少根文件系统。
参数说明:
  • -M vexpress-a9:指定模拟的开发板型号。
  • -m 512M:分配512MB内存。
  • -kernel:指定内核镜像路径。
  • -dtb:指定设备树二进制文件。
  • -nographic:不使用图形界面,串口输出到终端。
  • -append "console=ttyAMA0":内核命令行参数,指定控制台为串口0。

2.3.3 网络与存储设备配置

为了后续驱动实验,需要为QEMU添加网络后端和SD卡存储模拟。

# 创建虚拟SD卡镜像(用于存储文件系统和驱动模块)
qemu-img create -f raw sd.img 512M

# 启动QEMU并挂载SD卡、启用网络
qemu-system-arm \
    -M vexpress-a9 \
    -m 512M \
    -kernel ~/linux-kernel/linux/arch/arm/boot/zImage \
    -dtb ~/linux-kernel/linux/arch/arm/boot/dts/vexpress-v2p-ca9.dtb \
    -drive file=sd.img,format=raw,if=sd \
    -net nic -net user,hostfwd=tcp::2222-:22 \
    -nographic \
    -append "console=ttyAMA0 root=/dev/mmcblk0 rw"
QEMU参数 作用 驱动实验中的用途
-drive file=sd.img,if=sd 模拟SD卡控制器 块设备驱动、文件系统挂载
-net nic -net user 模拟网卡(用户模式网络) 网络驱动、NFS根文件系统
hostfwd=tcp::2222-:22 端口转发(主机2222→客户机22) SSH登录、scp传输驱动模块
-s 启动gdbstub(默认端口1234) 内核调试、驱动单步执行
QEMU vexpress-a9 模拟环境架构 x86_64 主机 QEMU 进程 QEMU 虚拟硬件 Cortex-A9 GIC PL011 UART MMCI SD SMSC 911x SP804 Timer ARM Linux 内核 驱动模块 .ko 交互方式: 主机通过串口(-nographic)与客户机交互,通过端口转发(hostfwd)实现SSH/scp,通过gdbstub(-s)进行内核调试。

2.4 文件系统制作

根文件系统是Linux启动后挂载的第一个文件系统,它包含init程序、库文件、设备节点和必要的工具。对于驱动实验,我们需要一个轻量但功能完整的文件系统,支持模块加载、设备节点创建和网络功能。

2.4.1 使用BusyBox构建最小根文件系统

BusyBox是一个集成了数百个常用Unix命令的单一可执行文件,是嵌入式Linux文件系统的标准选择。

# 1. 下载BusyBox源码
cd ~
wget https://busybox.net/downloads/busybox-1.33.2.tar.bz2
tar -xf busybox-1.33.2.tar.bz2
cd busybox-1.33.2

# 2. 配置BusyBox(使用默认配置,启用静态编译)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 在 Settings 中启用:
#   [*] Build BusyBox as a static binary (no shared libs)

# 3. 编译并安装到临时目录
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- install CONFIG_PREFIX=~/rootfs

# 4. 查看安装结果
ls -lh ~/rootfs/bin/busybox
~/rootfs/bin/busybox --help

2.4.2 完善文件系统目录结构

一个可启动的根文件系统需要包含以下目录和关键文件:

cd ~/rootfs
# 创建必要目录
mkdir -p dev etc init.d lib proc sys tmp var/log mnt
# 创建设备节点(静态方式,后续可使用devtmpfs自动创建)
sudo mknod dev/console c 5 1
sudo mknod dev/null c 1 3
sudo mknod dev/ttyAMA0 c 204 64
# 创建初始化脚本 etc/inittab
cat > etc/inittab << 'EOF'
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
EOF
# 创建系统初始化脚本 etc/init.d/rcS
cat > etc/init.d/rcS << 'EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
mkdir -p /dev/pts
mount -t devpts