可恶,这次我一定要编译出Linux内核

前言

博客很久没有更新了,其实也写了很多 IOT 研究的文章,但因为保密不能公开。而本文写的是最近几天在搭建 QEMUARM Linux 运行环境的过程,这方面网上的文章比较多,没有什么保密性可言。

本文记录了交叉编译 Linux 内核、BusyBox 以及制作虚拟存储介质的过程。旨在根据不同设备的内核版本,搭建出一套匹配内核版本的 QEMU 仿真环境。之前研究几款路由器设备都是 2.6x 版本的 ARM 内核。使用 18.04 上安装的交叉编译工具链去编译的程序,在这种 2.6x 老内核中运行会报错 kernel too old。如果想编译一套相应的研究工具(如 gdbserverptraceltrace、小型反弹木马等等)会很麻烦,包括一些测试场景,可能会因为内核版本差异较大,产生一些异常。

因为网上有很多相关文章(大部分都是搞嵌入式的老哥写的),本文只是对自己学习这部分做一个记录和总结,帮助自己之后可以根据不同的研究需求去搭建特定环境。之前搞 IOT 仿真的时候,用的都是这个 编译好的配套环境,看完本文后,应该就能自己搞一套 QEMU 环境了😋。

本文记录了下面部分内容。

  1. 配置交叉编译环境
  2. 交叉编译 ARM Linux 4.1x 内核
  3. 交叉编译 busybox
  4. 构建磁盘文件镜像
  5. 将 01-03 的产物成功用 QEMU 运行起虚拟机
  6. 实现 QEMU 虚拟机的网络通信

补充:虽然在摸索的过程中遇到了一些报错并解决。但为了不影响文章的逻辑和连贯性,在正文只顺序记录所有正确的操作步骤,在文末会单独记录踩的坑。

软件版本

工具(虚拟机) 版本
qemu-system-arm 7.2.0
Ubuntu 18.04
arm-linux-gnueabihf-gcc 4.9.4
busybox 1.30.1

编译QEMU 2.6x内核环境-失败

其实最开始搭建的是这个 2.6.36.4 ARM 内核环境,最后算失败的。尽管 QEMU 可以运行起来内核,但问题是无法保存文件系统中的任何修改。具体原因应该是磁盘镜像文件没有被内核识别到,为了避免看完文章,发现到头来搭建的环境用不了。我打算先记录搭建成功的 4.1.15ARM 内核环境,而 2.6 上磁盘文件挂载不上的问题,至少在写完这篇文章时,依然没有解决。

补充:

  1. 最开始实验的时候,内核启动经常崩溃。当时是关掉 shell 或者把进程 kill 掉,这样比较麻烦。可以用 ctrl+a 再按 x 退出 QEMU 比较方便。
  2. 编译 2.6.36.4 内核,交叉编译工具链版本最好使用 4.3.2 版本的 arm-none-linux-gnueabi-gcc。并且 busybox 使用的版本最好为 1.20.0,编译内核时,我选择的开发板是 versatile。

配置交叉编译环境

我的机器上原本有一套交叉编译环境,是 7.5.0 的 arm-linux-gnueabi-gcc。如果内核版本和交叉编译工具链的版本差太多的话,会出问题。因此我重新下载了一套 4.9.4 的 arm-linux-gnueabihf-gcc 交叉编译工具。

交叉编译工具链下载 地址

这个解压后就能直接使用,不需要把路径添加到环境变量里面。后面设置交叉编译工具链时指定具体路径即可,并不会和原本的 arm-linux-gnueabi-gcc 冲突。

image-20241010190803682

交叉编译内核

下载 linux-4.1.15 内核源码。

wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.1.15.tar.xz

解压源码。

tar xvf linux-4.1.15.tar.xz

生成默认内核配置文件。

make ARCH=arm vexpress_defconfig

image-20241010191358797

指定编译 ARM 架构内核,并设置交叉编译工具链前缀。然后大概编译五分钟左右就结束了。

make ARCH=arm CROSS_COMPILE=/home/zikh/Desktop/gcc-linaro-4.9-2016.02-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- all

正常编译后,产物应该有 zImage 和 vexpress-v2p-ca9.dtb。

image-20241010192134786

交叉编译busybox

下载 1.30.1 版本的 busybox,交叉编译出文件系统的一些必要文件。

wget https://busybox.net/downloads/busybox-1.30.1.tar.bz2

解压文件

tar -xjf busybox-1.30.1.tar.bz2

通过图形化界面配置 busybox,这些环境变量可以直接用 export 设置在本次 shell 中生效,也可以每次运行命令的时候,作用在本次命令。

make ARCH=arm menuconfig

使用所有的默认配置即可,不需要做任何修改。不要勾选 Build static binary (no shared libs) 和 Build Shared libbusybox选项。退出时保存默认配置即可。

image-20241010192851869

然后用同样的命令编译 busybox,大概两分钟就结束了。

make ARCH=arm CROSS_COMPILE=/home/zikh/Desktop/gcc-linaro-4.9-2016.02-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- all

image-20241010193516024

再执行下面命令进行安装,如果不指定安装目录的话,默认会将 busybox 安装到当前的 _install 目录。

make ARCH=arm CROSS_COMPILE=/home/zikh/Desktop/gcc-linaro-4.9-2016.02-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- install
image-20241010193736185

创建文件系统

现在的文件系统 _install 并不完整,缺少了很多目录。下面补充一些缺少的必要目录和文件。

注意:下面要将交叉编译工具链里面的 lib 目录拷贝到当前的 _install 目录,而交叉编译工具链下面有好几个 lib 目录。如果不能确定的话,就用命令 find xxx -name "ld-linux-armhf.so.3" 搜索,有这个 ld 的文件就是要拷贝的 lib 目录。

mkdir lib
cp -a ~/Desktop/gcc-linaro-4.9-2016.02-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib/* ./lib

image-20241010194118469

创建 etc/inittab 文件,写入下面内容。

# 系统启动时
::sysinit:/etc/init.d/rcS

# 系统启动按下Enter键时
::askfirst:-/bin/sh

# 按下Ctrl+Alt+Del键时
::ctrlaltdel:/sbin/reboot

# 系统关机时
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r

# 系统重启时
::restart:/sbin/init

创建 etc/init.d/rcS 文件,写入下面内容。并且给 etc/init.d/rcS 文件 777 的权限

#!/bin/sh

# 挂载 /etc/fstab 中定义的所有文件系统
/bin/mount -a

# 挂载虚拟的devpts文件系统用于用于伪终端设备
/bin/mkdir -p /dev/pts
/bin/mount -t devpts devpts /dev/pts

# 使用mdev动态管理u盘和鼠标等热插拔设备
/bin/echo /sbin/mdev > /proc/sys/kernel/hotplug

# 扫描并创建节点
/sbin/mdev -s

创建 etc/fstab 文件,写入下面内容。

# <file system>    <mount point>    <type>    <options>    <dump>    <pass>     
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0

创建 etc/profile 文件,写入下面内容。

export HOSTNAME=zy

# 用户名
export USER=root

# 用户目录
export HOME=/root

# 终端默认提示符
export PS1="[$USER@$HOSTNAME:\$PWD]\# "

# 环境变量
export PATH=/bin:/sbin:/usr/bin:/usr/sbin

# 动态库路径
export LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH

创建 dev 目录

mkdir dev

创建终端文件

sudo mknod dev/console c 5 1
sudo mknod dev/null c 1 3

创建其他目录

mkdir mnt proc tmp sys root

现在的 _install 目录已经像是一个正常的文件系统了。

image-20241010200240848

构建磁盘文件镜像

dd 创建一个 512M 的空白镜像,再用 mkfs.ext3 命令在 rootfs.ext3.img 文件中生成一个 ext3 格式的文件系统。通过挂载的方式,访问 rootfs.ext3.img 文件,将之前的 _install 目录下文件拷贝至挂载点 mnt_tmpfs,最后取消挂载。至此一个装有文件系统的磁盘镜像制作完毕。

mkdir mnt_tmpfs
dd if=/dev/zero of=rootfs.ext3.img bs=1M count=512
sudo mkfs.ext3 rootfs.ext3.img
sudo mount -t ext3 rootfs.ext3.img ./mnt_tmpfs -o loop
sudo cp -r _install/* mnt_tmpfs
sudo umount mnt_tmpfs

image-20241010200923987

启动 QEMU

将之前准备的产物拷贝到一个新的目录。

cp /home/zikh/Desktop/busybox-1.30.1/rootfs.ext3.img ./
cp /home/zikh/Desktop/linux-4.1.15/arch/arm/boot/zImage ./
cp /home/zikh/Desktop/linux-4.1.15/arch/arm/boot/dts/vexpress-v2p-ca9.dtb ./

QEMU 启动脚本

qemu-system-arm -M vexpress-a9 \
-m 512M \
-dtb ./vexpress-v2p-ca9.dtb \
-kernel ./zImage \
-nographic \
-append "root=/dev/mmcblk0 rw console=ttyAMA0" \
-drive file=./rootfs.ext3.img,format=raw,if=sd \
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no \
-device virtio-net-device,netdev=net0

网络脚本

#!/bin/bash
sudo ip tuntap add dev tap0 mode tap
sudo ip link set tap0 up
sudo brctl addif virbr0 tap0
sudo chmod 666 /dev/net/tun

执行完网络脚本后,执行 QEMU 启动脚本。

image-20241010201405606

目前 QEMU 可以正常启动,但里面的机器还没有配置 IP。

image-20241010202037424

给 eth0 网卡分一个 IP(取决于宿主机中的 virbr0 在哪个网段),再配置一下网关。

ip add add 192.168.122.130/24 dev eth0
ip link set eth0 up
ip route add default via 192.168.122.1

现在 IP 分配出来了,并且可以 ping 通网关。ping 不通百度说明 DNS 解析还没有配置。

image-20241010202256317

创建一个 /etc/resolv.conf 文件,里面写入 nameserver 8.8.8.8,至此虚拟机网络通信正常。

image-20241010202518573

磁盘也是正常的(2.6x 的内核这里识别不到磁盘设备),如果有时候创建完文件,重启后文件消失了,可能是 QEMU 还没有将数据同步到磁盘上,就退出了系统。可以在退出 QEMU 前执行一下 sync 命令,将所有缓存数据写入磁盘。

image-20241010202651202

报错与解决

由于我没有在每一次遇到问题时,都记录了相关的日志。所以有很多操作的一些小错误,我并没有记录,只记录了几个印象深刻的错误。并且下面的报错记录,并不是按照遇到顺序记录的。只希望可以帮助遇到同样错误的朋友做一个参考。

报错1

新增了网络配置时, QEMU 启动脚本是这样。

qemu-system-arm -M vexpress-a9 \
-m 512M \
-dtb ./vexpress-v2p-ca9.dtb \
-kernel ./zImage \
-nographic \
-append "root=/dev/mmcblk0 rw console=ttyAMA0" \
-drive file=./rootfs.ext3.img,format=raw,if=sd \
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no \
-device virtio-net-pci,netdev=net0

有如下报错。因为vexpress-a9 并不支持 PCI 总线。将替换 virtio-net-pcivirtio-net-device,可以避免使用 PCI 总线并解决这个错误。

qemu-system-arm: -device virtio-net-pci,netdev=net0: No 'PCI' bus found for device 'virtio-net-pci'

修改后脚本如下

qemu-system-arm -M vexpress-a9 \
-m 512M \
-dtb ./vexpress-v2p-ca9.dtb \
-kernel ./zImage \
-nographic \
-append "root=/dev/mmcblk0 rw console=ttyAMA0" \
-drive file=./rootfs.ext3.img,format=raw,if=sd \
-netdev tap,id=net0,ifname=tap0,script=no,downscript=no \
-device virtio-net-device,netdev=net0

报错2

编译 busybox 时报错日志如下

Output of:
/home/zikh/Desktop/gcc-linaro-4.9-2016.02-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc -Wall -Wshadow -Wwrite-strings -Wundef -Wstrict-prototypes -Wunused -Wunused-parameter -Wunused-function -Wunused-value -Wmissing-prototypes -Wmissing-declarations -Wno-format-security -Wdeclaration-after-statement -Wold-style-definition -fno-builtin-strlen -finline-limit=0 -fomit-frame-pointer -ffunction-sections -fdata-sections -fno-guess-branch-probability -funsigned-char -static-libgcc -falign-functions=1 -falign-jumps=1 -falign-labels=1 -falign-loops=1 -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-builtin-printf -Os -fpic -fvisibility=hidden 0_lib/applet.c -o 0_lib/nologin -Wl,--sort-common -Wl,--sort-section,alignment -Wl,--gc-sections -L0_lib -lbusybox -Wl,--warn-common
==========
/tmp/cctzZ541.o: In function main':
applet.c:(.text.main+0x16): undefined reference to scripted_main'
collect2: error: ld returned 1 exit status
Makefile:718: recipe for target 'busybox_unstripped' failed
make: *** [busybox_unstripped] Error 1

因为在执行 make ARCH=arm menuconfig 配置 busybox 时开启了 Build Shared libbusybox 选项,关闭即可。

报错3

在 QEMU 启动内核时,有如下报错。

Starting init: /sbin/init exists but couldn't execute it (error -8)
...
Starting init: /bin/sh exists but couldn't execute it (error -8)
Kernel panic - not syncing: No working init found.

这里是因为当时 busybox 没有设置好架构,编译成了 x64 从而导致架构不匹配报错 error -8。指定好环境变量 ARCH 为 arm 即可。

报错4

依旧是 QEMU 启动内核时报错,这个意思是指文件系统的 /sbin/init 文件不存在或无法运行。

Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/init.txt for guidance.

我将磁盘文件挂载后查看确实存在 /sbin/init 文件,但是发现其依赖 ld-linux-armhf.so.3 文件。我查看了 /lib 目录,发现并没有该文件。因此认为是动态库出了问题,使 /sbin/init 执行失败。

后面检查发现,错把 gcc-linaro-4.9-2016.02-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/lib目录当成了文件系统的 /lib目录。实际要拷贝的目录应该是 gcc-linaro-4.9-2016.02-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib 目录。

参考文章

搭建基于qemu的linux开发环境 - 大奥特曼打小怪兽 - 博客园 (cnblogs.com)

Mini2440之linux根文件系统yaffs2移植 - 大奥特曼打小怪兽 - 博客园 (cnblogs.com)