核心流程

首先,本文主要讲述如何编译Linux内核并在qemu虚拟机上运行。这里针对的架构是aarch64

本文的实验平台是Ubuntu 16.04

为了达成目标,我们需要有qemubuildrootlinux安装包或源码。

首先确保qemu-system-aarch64命令可用,可以通过在命令行执行qemu-system-aarch64 --version判断。

下载buildroot源码,链接见下文平台工具。假设其绝对路径保存在变量BUILD_ROOT_PATH中。执行以下命令

cd $BUILD_ROOT_PATH
make menuconfig

在弹出的配置界面中,设置Target option ---> Target ArchitectureAArch64 (little endian);设置Toolchain ---> Toolchain typeExternal toolchain,这时我们可以看到Toolchain ---> Toolchain的值为linaro AArch64 xxxx.xx;设置System configuration ---> Enable root login with password开启,并设置System configuration ---> Root passwordxxxx(任意的你喜欢的密码);设置System configuration ---> Run a getty (login prompt) after boot ---> TTY port的值为ttyAMA0(这一条非常重要,不然虚拟机可能启动不了);设置Target packages ---> Show packages that are also provided by busybox开启;设置Target packages ---> Debugging, profiling and benchmark ---> strace开启;设置Filesystem images ---> cpio the root filesystem开启。

在配置完成之后,执行

make

注意:这里可能需要配置wget代理。

生成的rootfs.cpio在目录$BUILD_ROOT_PATH/output/images下面。

下载Linux内核源码,链接见下文平台工具。假设其绝对路径保存在变量LINUX_KERNEL_PATH中。执行以下命令

cd $LINUX_KERNEL_PATH
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make defconfig
vim .config

在打开的文件中,找到CONFIG_CMDLINE这个配置选项,设置其值为console=ttyAMA0;找到CONFIG_INITRAMFS_SOURCE这个配置选项,设置其值为$BUILD_ROOT_PATH/output/images/rootfs.cpio(注意,这里要自己展开变量BUILD_ROOT_PATH);设置CONFIG_DEBUG_INFO配置项为y

配置结束后,执行以下命令

ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make -j16

执行结束后,我们可以看到生成了一些文件,其中包括$LINUX_KERNEL_PATH/vmlinux$LINUX_KERNEL_PATH/arch/arm64/boot/Image

为了能在 QEMU 里面使用网络功能,这里使用 tap 作为后端网络设备。在主机中,创建tap_linux设备,命令如下:

$ sudo ip tuntap add dev tap_linux mod tap
$ sudo ifconfig tap_linux up
$ sudo ifconfig tap_linux 192.168.10.10 netmask 255.255.255.0

在当前目录创建一个shell脚本,避免重复输入指令。其文件名为start.sh,其内容为

qemu-system-aarch64 -machine virt -cpu cortex-a57 \
	-machine type=virt -nographic -smp 1 \
	-m 2048 \
	-kernel ./arch/arm64/boot/Image \
	--apend "console=ttyAMA0" \
        -netdev tap,id=mynet1,script=no,downscript=no,ifname=tap_linux \
        -device virtio-net-device,netdev=mynet1,mrg_rxbuf=off,csum=off,guest_csum=off,gso=off,guest_tso4=off,guest_tso6=off,guest_ecn=off,guest_ufo=off \
	$1 $2

执行./startup.sh,这是可以用qemu启动linux内核。在进入命令行之前,需要输入buildroot login: 的值,其值为root,然后需要输入Password: ,这是前文构建rootfs.cpio的时候,配置项System configuration ---> Root password的值。然后就可以进入命令行执行以下常用命令了。(注意,需要先cd /)。使用网络功能需要先给网卡配置 ip,例如ifconfig eth0 192.168.10.11

如果要退出qemu,可以先按Ctrl + A,然后按X

为了在主机和qemu虚拟机之间共享文件,我们可以创建一个目录,其绝对路径为SHARED_FILE_PATH。然后执行以下命令

cd $BUILD_ROOT_PATH
vim .config

修改BR2_ROOTFS_OVERLAY配置项的值为$SHARED_FILE_PATH(注意,自行展开变量)。

保存后执行以下命令重新创建 rootfs.cpio

rm $BUILD_ROOT_PATH/output/images/rootfs.*
make

然后需要重新编译内核,即

cd $LINUX_KERNEL_PATH
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make -j16

依然通过./start.sh启动虚拟机跑linux内核,进入命令行后,执行cd /; ls,我们可以看到$SHARED_FILE_PATH目录下的文件。

重新执行./start.sh -s -S进入qemu的调试状态,然后开一个新的shell,输入命令

cd $LINUX_KERNEL_PATH
aarch64-linux-gnu-gdb ./vmlinux -ex "target remote :1234"

现在,可以像以往一样使用gdb进行调试了……

下面介绍如何编译一个linux内核模块。

cd $SHARED_FILE_PATH
vim hello.c

hello.c的内容为

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/seq_file.h>
#include <linux/proc_fs.h>
#include <linux/sched.h>

int helloinit(void)
{
	printk(KERN_DEBUG "hello world !!\n");
	return 0;
}

void helloexit(void)
{
	printk(KERN_DEBUG "goodbye world !!\n");
}

module_init(helloinit);
module_exit(helloexit);

MODULE_LICENSE("GPL");

然后在当前目录创建Makefile内容如下

ifneq (${KERNELRELEASE},)
obj-m := hello.o
else
KERNEL_SOURCE := $LINUX_KERNEL_PATH # 注意自行展开变量LINUX_KERNEL_PATH
PWD := $(shell pwd)

default:
	${MAKE} -C ${KERNEL_SOURCE} M=${PWD} modules

clean:
	${MAKE} -C ${KERNEL_SOURCE} M=${PWD} clean
endif

然后执行交叉编译命令

ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make

OK,重新编译rootfs.cpiolinux内核,然后运行qemu。在进入linux命令行后,执行以下命令

cd /
insmod hello.ko
dmesg | tail      # 可以看到打印信息 hello world !!
rmmod hello.ko
dmesg | tail      # 可以看到打印信息 goodbye world !!

相关经验

Linux Kernel

  • 使用qemu并开启gdb server功能之后,在gdb窗口输入b start_kernel,进入最初的内核初始化函数。

平台工具

参考