Ubuntu虚拟机中使用QEMU搭建ARM64环境
通过本实验学习如何编译一个 ARM64 版本的内核 image,并且在QEMU 上运行起来。
文章目录
- Ubuntu虚拟机中使用QEMU搭建ARM64环境
- 一、安装aarch64交叉编译工具
- 二、安装QEMU
- 三、制作根文件系统
- 1、根文件系统简介
- 2、BusyBox构建根文件系统
- 1)下载BusyBox源码
- 2)制定编译工具链
- 3)配置编译BusyBox
- 4)创建需要的目录
- (a) etc目录更新
- (b) dev目录下的必要文件
- (c) lib目录下的必要文件
- 四、编译内核源码
- 1、下载源码
- 2、指定编译工具
- 3、将根文件系统放到源码根目录
- 4、配置生成.config
- 5、编译
- 五、启动QEMU
- 1、创建共享文件目录
- 2、运行QEMU模拟器
- 4、编译一个简单的内核模块并在QEMU上运行
- 六、实验过程中遇到的一些问题及解决办法
- 1、本地下载压缩包如何上传Ubuntu中?
- 2、make编译BusyBox出现的报错
- 3、根文件系统放到源码根目录时报错
- 4、关于内核模块在QEMU中运行出现的问题
- 参考链接
一、安装aarch64交叉编译工具
搭建QEMU的模拟环境首先需要下载安装对应架构的交叉编译工具链(这里是arm64架构):
sudo apt-get install gcc-aarch64-linux-gnu
sudo apt-get install libncurses5-dev build-essential git bison flex libssl-dev
安装完成之后查看版本说明安装完成:
$ aarch64-linux-gnu-gcc -v
二、安装QEMU
在终端中输入 sudo apt install qemu-system-arm
安装
安装完成后,输入 qemu-system-aarch64 --version
来查看 qemu版本
三、制作根文件系统
1、根文件系统简介
Linux的根文件系统一般也叫做 rootfs,Linux的根文件系统更像是一个文件夹或者叫做目录,在这个目录里面会有很多的子目录。根目录下和子目录中会有很多的文件,这些文件是Linux运行所必须的,比如库、常用的软件和命令、设备文件、配置文件等等。
根文件系统的“根”字就说明了这个文件系统的重要性,它是其他文件系统的根,没有这个“根” ,其他的文件系统或者软件就别想工作。比如我们常用的 ls、mv、ifconfig 等命令其实就是一个个小软件,只是这些软件没有图形界面,而且需要输入命令来运行。这些小软件就保存在根文件系统中。
在构建根文件系统之前,先来看一下根文件系统里面都有些什么内容,根文件系统的目录名字为‘/’ ,就是一个斜杠。下面以Ubuntu为例,一些常用的子目录介绍如下表示:
2、BusyBox构建根文件系统
1)下载BusyBox源码
BusyBox是一个集成了大量的Linux命令(如ls、mv、ifconfig 等命令)和工具的软件。借助BusyBox,进行配置和编译,就可以方便的构建一个嵌入Linux平台所需要的根文件系统。
可在BusyBox官网 https://busybox.net/ 下载源码。
将压缩包拖拽到Ubuntu虚拟机中的桌面或文件夹中,并解压
tar -vxjf busybox-1.37.0.tar.bz2
解压后的文件如下:
2)制定编译工具链
cd busybox-1.36.1
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
//检测是否配置成功
echo $ ARCH
echo $CROSS_COMPILE
3)配置编译BusyBox
busybox中文字符支持:若直接编译busybox,使用串口工具时是不支持中文显示的,会显示为“?” ,可修改源码,取消 busybox对中文显示的限制
打开文件/libbb/printable_string.c,将函数printable_string()中的部分程序注释掉,修改后的函数内容如下:
/********** printable_string.c代码段 **********/
const char* FAST_FUNC printable_string(uni_stat_t *stats, const char *str)
{
char *dst;
const char *s;
s = str;
while (1) {
......
if (c < ' ')
break;
/* 注释掉下面这个两行代码,禁止字符大于0X7F以后 break */
/* if (c >= 0x7f)
break; */
s++;
}
#if ENABLE_UNICODE_SUPPORT
dst = unicode_conv_to_printable(stats, str);
#else
{
char *d = dst = xstrdup(str);
while (1) {
unsigned char c = *d;
if (c == '\0')
break;
/* 修改下面代码,禁止字符大于0X7F以后输出‘?’ */
/* if (c < ' ' || c >= 0x7f) */
if( c < ' ')
*d = '?';
d++;
}
......
#endif
return auto_string(dst);
}
接着打开文件/libbb/unicode.c,修改如下内容:
/********** unicode.c代码段 **********/
static char* FAST_FUNC unicode_conv_to_printable2(uni_stat_t *stats, const char *src, unsigned width, int flags)
{
char *dst;
unsigned dst_len;
unsigned uni_count;
unsigned uni_width;
if (unicode_status != UNICODE_ON) {
char *d;
if (flags & UNI_FLAG_PAD) {
d = dst = xmalloc(width + 1);
......
/* 修改下面一行代码 */
/* *d++ = (c >= ' ' && c < 0x7f) ? c : '?'; */
*d++ = (c >= ' ') ? c : '?';
src++;
}
*d = '\0';
} else {
d = dst = xstrndup(src, width);
while (*d) {
unsigned char c = *d;
/* 修改下面一行代码 */
/* if (c < ' ' || c >= 0x7f) */
if(c < ' ')
*d = '?';
d++;
}
}
......
return dst;
}
......
return dst;
}
- 配置busybox:有以下几种配置选项,一般使用默认配置即可
– defconfig:缺省配置,也就是默认配置选项
– allyesconfi:全选配置,即选中 busybox 的所有功能
– allnoconfig:最小配置
make defconfig #使用默认配置
make menuconfig #打开图形化配置界面
设置Settings -> Build static binary (no shared libs)
设置Settings -> Support Unicode,使能busybox的unicode编码以支持中文
- 编译busybox:配置好busybox以后就可以编译了,输入如下命令
make
make install CONFIG_PREFIX=/home/xlq/linux/rootfs
#CONFIG_PREFIX指定编译结果的存放目录
编译完成以后,busybox的所有工具和文件就会被安装到rootfs目录中,如下图;rootfs目录下有bin、sbin和usr三个目录,以及linuxrc文件。
4)创建需要的目录
cd ~/linux/rootfs
mkdir dev etc lib sys proc tmp var home root mnt
(a) etc目录更新
- 创建 profile 文件,添加下面内容
#!/bin/sh
export HOSTNAME=user
export USER=root
export HOME=/home
export PS1="[$USER@$HOSTNAME \W]\# "
PATH=/bin:/sbin:/usr/bin:/usr/sbin
LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
export PATH LD_LIBRARY_PATH
用nano profile打开新建的文件后,Ctrl+O打开写入后直接回车,再点击Ctrl+x退出终端
回到文件夹下会出现新建的一个profile空白文件,将上述内容复制粘贴到该空白文件下即可。
后续创建操作皆可如此
- 创建 inittab 文件,添加下面内容
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r
- 创建 fstab 文件,添加下面内容,指定挂载的文件系统
#device mount-point type options dump fsck order
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
debugfs /sys/kernel/debug debugfs defaults 0 0
kmod_mount /mnt 9p trans=virtio 0 0
- 创建init.d目录
mkdir init.d
- 在init.d下创建 rcS文件,添加下面内容
cd init.d
mkdir -p /sys
mkdir -p /tmp
mkdir -p /proc
mkdir -p /mnt
/bin/mount -a
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
- 添加权限
chmod 777 rcS
- 利用tree命令查看etc下目录结构如下所示
(b) dev目录下的必要文件
cd dev
sudo mknod console c 5 1
输出下面命令检查是否创建了console的设备文件:
ls -l /dev/console
© lib目录下的必要文件
为了支持动态编译的应用程序的执行,根文件系统需要支持动态库,所以我们添加arm64相关的动态库文件到lib下
cd lib
cp /usr/aarch64-linux-gnu/lib/*.so* -a .
检查当前目录是否包含了.so文件:
四、编译内核源码
1、下载源码
下载Linux的内核源码点击下载
很多教程是直接使用命令行下载,但我最初直接采用命令行方式下载Ubuntu内存爆红并且下载失败了,后面改用直接从网站下载比较稳妥。
同BusyBox放到Ubuntu合适位置进行解压
tar xvf linux-6.13.5.tar.xz
2、指定编译工具
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
3、将根文件系统放到源码根目录
cd linux-6.13.5
sudo cp ~/linux/rootfs rootfs_arm64 -a
4、配置生成.config
make defconfig
make menuconfig
添加hotplug支持
Device Drivers
-> Generic Driver Options
-> Support for uevent helper
(/sbin/hotplug) path to uevent helper
添加initramfs支持
General setup --->
[*]Initial RAM filesystem and RAM disk(initramfs/initrd) support
(_install_arm64) Initramfs souce file(s)
Virtual address space配置
Kernel Features --->
Page size(4KB) --->
Virtual address space size(48-bit)--->
5、编译
make all -j8
五、启动QEMU
1、创建共享文件目录
在内核源码目录下创建目录
mkdir kmodules
2、运行QEMU模拟器
在内核源码目录下执行下面命令
qemu-system-aarch64 -machine virt -cpu cortex-a57 -machine type=virt -m 1024 -smp 4 -kernel arch/arm64/boot/Image --append "rdinit=/linuxrc root=/dev/vda rw console=ttyAMA0 loglevel=8" -nographic --fsdev local,id=kmod_dev,path=$PWD/kmodules,security_model=none -device virtio-9p-device,fsdev=kmod_dev,mount_tag=kmod_mount
— machine virt:使用virt机器类型。
— cpu cortex-a57:使用Cortex-A57 CPU模型。
— -m 1024:设置虚拟机内存大小为1024MB。
— -smp 4:设置虚拟机使用4个CPU核心。
— -kernel arch/arm64/boot/Image:指定Linux内核镜像的路径。
— --append “rdinit=/linuxrc root=/dev/vda rw console=ttyAMA0 loglevel=8”:指定内核启动参数,其中rdinit
指定init程序的路径,root
指定根文件系统的设备,rw
表示以读写模式挂载根文件系统,console
指定控制台设备,loglevel
指定内核日志级别。
— -nographic:禁用图形界面,使用纯文本控制台。
— --fsdev local,id=kmod_dev,path=$PWD/kmodules,security_model=none:创建一个本地文件系统设备,其中id
指定设备ID,path
指定设备挂载的本地路径,security_model
指定安全模型。
— -device virtio-9p-device,fsdev=kmod_dev,mount_tag=kmod_mount:将本地文件系统设备挂载到虚拟机中,其中fsdev
指定设备ID,mount_tag
指定设备挂载的标签。
启动成功后会打印下面内容,点击Enter进入控制台
退出QEMU模拟器
poweroff
4、编译一个简单的内核模块并在QEMU上运行
-
在根目录下创建一个文件夹module_test,并编写一个简单的hello.c代码
// 包含内核模块编程所需的头文件 #include <linux/init.h> // 包含模块初始化和退出函数的宏 #include <linux/module.h> // 包含模块相关的宏和函数 #include <linux/kernel.h> // 包含内核打印函数 printk 的头文件 // 模块初始化函数 // 当模块被加载到内核时,此函数会被调用 static int __init test_init(void) { // 在内核日志中打印 "hello world!" printk("hello world!\n"); // 返回 0 表示初始化成功 return 0; } // 模块退出函数 // 当模块从内核中卸载时,此函数会被调用 static void __exit test_exit(void) { // 在内核日志中打印 "hello exit!" printk("hello exit!\n"); } // 注册模块的初始化函数 // 当模块被加载时,test_init 函数会被调用 module_init(test_init); // 注册模块的退出函数 // 当模块被卸载时,test_exit 函数会被调用 module_exit(test_exit); // 声明模块的许可证 // GPL 是 GNU 通用公共许可证,表示这是一个开源模块 MODULE_LICENSE("GPL");
-
再编写Makefile文件
# 设置目标架构为 ARM64
export ARCH=arm64
# 设置交叉编译工具链前缀为 aarch64-linux-gnu-
export CROSS_COMPILE=aarch64-linux-gnu-
# 定义内核源码目录
KERNEL_DIR ?= /home/xlq/linux-6.13.5
# 定义要编译的内核模块目标文件
# obj-m 表示将 hello.c 编译为内核模块 hello.ko
obj-m := hello.o
# 默认目标:编译内核模块
modules:
# 调用内核源码目录的 Makefile,编译当前目录下的模块
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
# 清理目标:删除编译生成的文件
clean:
# 调用内核源码目录的 Makefile,清理当前目录下的生成文件
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
# 安装目标:将编译好的内核模块复制到指定目录
install:
# 将当前目录下所有的 .ko 文件复制到内核源码目录的 kmodules 子目录中
cp *.ko $(KERNEL_DIR)/kmodules
- 编译module,拷贝到共享目录
make modules
make install
-
启动QEMU
启动命令:
qemu-system-aarch64 -machine virt -cpu cortex-a57 -machine type=virt -m 1024 -smp 4 -kernel arch/arm64/boot/Image --append "rdinit=/linuxrc root=/dev/vda rw console=ttyAMA0 loglevel=8" -nographic --fsdev local,id=kmod_dev,path=$PWD/kmodules,security_model=none -device virtio-9p-device,fsdev=kmod_dev,mount_tag=kmod_mount
回车后进入控制台,以防万一可以检查一下挂载点是否成功
ls /mnt
可以成功看到.ko文件。
- 在QEUM执行module的插入与卸载,可以看到成功执行并打印log
[xlq@xlq-virtual-machine ]$ cd mnt
[xlq@xlq-virtual-machine mnt]$ ls /mnt
hello.ko kmod_mount
[xlq@xlq-virtual-machine mnt]$ insmod hello.ko
[ 3679.306752] hello: loading out-of-tree module taints kerne
[ 3679.322242] hello world!
[xlq@xlq-virtual-machine mnt]$ rmmod hello.ko
3732.515746] hello exit!
六、实验过程中遇到的一些问题及解决办法
1、本地下载压缩包如何上传Ubuntu中?
1、本地电脑上下载的busybox和Linux内核源码在移动到Ubuntu虚拟机上时拖到到主文件夹即可
2、make编译BusyBox出现的报错
2、出现问题:在使用命令make编译BusyBox时,出现报错信息如下
libbb/hash_md5_sha.c:1316:35: error: 'sha1_process_block64_shaNI' undeclared (first use in this function);did you mean 'sha1 process block64'?
解决办法:
nano libbb/hash_md5_sha.c
进入函数中,查找sha1_process_block64_shaNI
,将所有的sha1_process_block64_shaNI
修改成sha1_process_block64
。保存后退出,再使用make编译即可通过。
3、根文件系统放到源码根目录时报错
3、出现问题:在将根文件系统放到源码根目录的时候,参考命令为:
cp ~linux/rootfts rootfs_arm64 -a
,但出现报错“‘无法创建特殊文件’”,原因是因为普通用户没有足够的权限来创建,设备文件和特殊文件通常需要root
权限来创建或修改。解决办法:使用
sudo
来提升权限
sudo cp ~/linux/rootfs rootfs_arm64 -a
4、关于内核模块在QEMU中运行出现的问题
4、当我在编译一个简单内核模块hello.c在QEMU运行测试的过程中,出现了很大问题:
出现问题:通过编译生成的内核模块hello.ko文件在成功拷贝到共享目录kmodules下时,启动QEMU,在mnt文件夹下无法看到hello.ko文件。
问题排查:合理利用大模型提出问题,主要从下面几个点来排查问题
- 共享目录未正确挂载
//检查是否执行了挂载命令 mount -t 9p -o trans=virtio kmod_mount /mnt //如果没有挂载,需要先挂载 mkdir -p /mnt/kmod_mount mount -t 9p -o trans=virtio kmod_mount /mnt/kmod_mount cd /mnt/kmod_mount ls //检测挂载是否成功 mount | grep kmod_mount //如果没有输出,说明挂载失败
- 挂载点错误(共享目录挂载到了其他目录,而不是/mnt)
//检查挂载点 mount | grep kmod_mount //输出示例 kmod_mount on /mnt type 9p (trans=virtio)
共享目录路径出错
本人觉得这个问题可能性不大,在上面执行make install命令后,可以回到内核源码目录下的kmodules文件下查看是否存在.ko文件
如果路径错误,修改QEMU启动命令中的path参数。
如果上述问题都没有可以再文件系统权限问题和内核未启用9p文件系统支持问题
解决办法:本人最初遇到的问题是挂载失败,重新挂载后还是无法看到挂载点下的.ko文件,进入kmodules文件下,正确的挂载应该出现一个kmod_mount的挂载标签,和一个.ko文件,如下(不要把.ko文件放到kmod_mount文件夹中)
回到QEMU中检查目录内容:
参考链接
用BusyBox构建根文件系统
QEMU搭建ARM64+Linux系统
qemu搭建ARM Linux环境