搭建基于qemu的linux开发环境

前言

因为疫情的原因,迟迟没有开学,而我的开发板还在学校,为了不影响linux的学习计划,决定使用qemu来模拟开发板。我的目标是:

  1. 在qemu上运行u-boot
  2. 使用u-boot通过TFTP的方式加载内核到虚拟开发板的内存
  3. 内核启动后,通过网络的方式挂载文件系统(NFS)

要想实现上述目标,需要配置一些环境,整个配置的过程比较细碎,一些配置方法不常用,容易忘记,因此通过一篇博客详细记录下来。过程中参考了一些网络资料,会在参考文献中列出。

1 安装并配置qemu

1.1 安装

ubuntu环境下,可以直接使用如下命令安装qemu:

apt install qemu

我的ubuntu版本是18.04.2,相应的qemu版本是2.11,因此直接安装无法安装到qemu的最新版本,喜欢尝鲜的同学可以去下载最新版的qemu源码然后采用编译安装的方式。

1.2 配置qemu的网络

由于我计划在qemu上运行u-boot,然后由u-boot使用tftp的方式将kernel镜像载入到qemu虚拟出的开发板的内存中,最后启动内核并通过网络的方式挂载根文件系统(NFS),因此需要qemu能够和ubuntu网络连通,换句话说,需要qemu里运行的u-boot以及kernel能够ping通ubuntu。

qemu其实也是虚拟机,qemu上运行的u-boot及kernel的关系类似于ubuntu和windows之间的关系。如果需要ubuntu和windows之间以ping通(就好像局域网中的两台计算机),那么需要将虚拟机(VMWare)的网络配置为桥接方式。同样的,qemu的网络也可以配置为桥接

将qemu的网络配置为桥接方式,有以下几件事情要做:

  1. 安装必要的工具包
    qemu在使用网络时,需要用到brctl等命令,这些命令ubuntu安装后可能没有带,需要另外安装,运行如下命令即可:
    apt install uml-utilities bridge-utils
    
  2. 主机内核的tun/tap模块
    qemu配置为桥接方式需要主机内核的tun/tap模块的支持,在我的ubuntu版本下,查看相应模块
    ls -l /dev/net/tun
    crw-rw-rw- 1 root root 10, 200 Mar 21 23:37 /dev/net/tun
    
    发现相应的模块已经存在,说明主机内核已经支持了。可能有一些老版本的ubuntu是没有 支持的需要自己安装相应的模块并创建设备文件。
  3. 修改主机的网络配置
    在ubuntu的/etc/network/interfaces文件中添加如下配置:
    # 2020.03.21 for qemu
    auto ens33
    auto br0
    iface br0 inet dhcp
    bridge_ports ens33
    
  4. 添加/etc/qemu-ifup和/etc/qemu-ifdown脚本
    经查看,这两个脚本文件在我的ubuntu中已经存在了,可能某些老版本的ubuntu是需要自己创建的。如果需要自己创建的话,脚本的内容网上很容易找到,不再赘述。

完成以上步骤后,重启ubuntu,以使配置(主要针对主机网络配置的修改)生效。重启后,运行ifconfig,如果出现br0则说明配置成功了,如下:

br0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.0.100  netmask 255.255.255.0  broadcast 192.168.0.255
        inet6 fe80::20c:29ff:fe31:287b  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:31:28:7b  txqueuelen 1000  (Ethernet)
        RX packets 162  bytes 102405 (102.4 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 201  bytes 19805 (19.8 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

需要注意的是,用qemu运行u-boot或kernel时,如果要使用qemu的网络功能,需要加上参数:-net nic,vlan=0 -net tap,vlan=0,ifname=tap0

2 搭建TFTP环境

u-boot支持通过TFTP的方式加载内核,主机(ubuntu)上运行TFTP服务器,u-boot向服务器请求其参数指定的文件,主机收到请求后将TFTP传输目录下的相应文件通过网络传输给u-boot。这需要主机具有TFTP环境。搭建过程分一下几个步骤:

  1. 安装相应软件
    apt install tftp-hpa tftpd-hpa
    
  2. 建立TFTP传输目录
    mkdir tftpboot
    chmod 0777 tftpboot
    
  3. 修改TFTP的配置文件/etc/default/tftpd-hpa
    修改
    TFTP_USERNAME="tftp"
    # TFTP传输目录
    TFTP_DIRECTORY=""/mnt/hgfs/share/tftpboot""
    TFTP_ADDRESS="0.0.0.0:69"
    # -c表示可以上传文件,-s表示指定TFTP传输目录,上面已经指定
    TFTP_OPTIONS="-l -c -s" 
    
  4. 重启TFTP服务
    service tftpd-hpa restart
    

为了确保环境搭建成功,可以做一些测试。在上文设置的TFTP传输目录下创建文件test。然后进入某个目录,假设是A目录,运行如下命令:

#localhost 表示本机
tftp localhost

此时进入TFTP的命令行,输入如下命令:

get test
put test
q

A目录下出现文件test,表示TFTP服务器安装成功。

3 搭建NFS环境

NFS是网络文件系统的缩写,可以用来实现在网络中跨主机访问文件(只能访问目标主机开放出来的目录,不是任何目录)。这样,在开发阶段,直接在主机上利用开发工具开发,只要把开发好的程序拷贝到NFS共享目录(向其它主机开放的目录)下,开发板就可以访问到了,非常方便。

搭建NFS环境有以下几步:

  1. 安装软件
    # 安装服务器即可(客户端有需要再装)
    apt install nfs-kernel-server
    
  2. 设置NFS共享目录
    编辑/etc/exports,添加一行:
    # 这里我设置的NFS共享目录是/root/work/nfsroot
    # 记得创建好这个目录
    /root/work/nfsroot *(rw,sync,no_root_squash,no_subtree_check)
    
    rw:表示权限为可读可写
    sync:表示将数据同步到磁盘(保证数据的一致性)
    no_root_squash:表示来访的root用户保持root帐号权限
    no_subtree_check:表示不检查父目录权限(提高效率)
  3. 重启NFS服务
    运行如下命令:
    /etc/init.d/nfs-kernel-server restart
    

为了确保NFS环境搭建成功,不妨做一下测试。挂接NFS共享目录,即执行如下命令:

mount -t nfs localhost:/root/work/nfsroot /mnt/rootfs

然后访问/mnt/rootfs,可见/root/work/nfsroot目录下的内容出现在/mnt/rootfs目录下,说明环境搭建成功。

4 编译u-boot

u-boot的源码可以在这里下载到,我下载的版本是u-boot-2019.10,下载之后先解压再编译。当然,编译之前需要先配置,非常幸运的是这个版本的u-boot直接支持了vexpress-a9,因此直接使用相应的默认配置就可以了,省去了移植的过程:

make vexpress_ca9x4_defconfig ARCH=arm

为了后续操做的方便,我们需要配置一下u-boot的几个环境变量。执行:

make menuconfig ARCH=arm

打开图形化配置界面后,将BOOTARGS配置为:root=/dev/nfs rw nfsroot=192.168.0.100:/root/work/nfsroot init=/linuxrc ip=192.168.0.106 console=ttyAMA0;并将BOOTCOMMAND配置为:tftp 0x60003000 uImage;tftp 0x60500000 vexpress-v2p-ca9.dtb;bootm 0x60003000 - 0x60500000;

以上环境变量是告诉u-boot给内核传什么参数(挂载网络文件系统作为根文件系统、init进程、内核IP、控制台),以及启动(boot)内核时u-boot执行的启动命令(从TFTP加载内核及设备树)。

此外,还需要设置一下u-boot的关于IP地址的环境变量,主要是服务端IP和开发板IP,这关系到开发板与服务器能否通过网络通信。但这些环境变量尚未在u-boot的Kconfig体系(也就是图形化配置)中,因此,不妨在源码中直接修改。具体的,在include/configs/vexpress_common.h中添加如下宏定义:

/* 服务端IP根据主机IP确定,开发板IP与主机IP在一个网段 */
#define CONFIG_IPADDR 192.168.0.107
#define CONFIG_NETMASK 255.255.255.0
#define CONFIG_SERVERIP 192.168.0.100

然后编译u-boot:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-

编译完成后成功生成了u-bootu-boot.bin等文件。为了测试配置和编译没有问题,运行如下命令,使用qemu运行u-boot(注意不是u-boot.bin,运行u-boot.bin会出错):

qemu-system-arm -M vexpress-a9\
                -m 512M\
                -kernel u-boot\
                -nographic\
                -net nic,vlan=0 -net tap,vlan=0,ifname=tap0

以上命令中,需要注意的是u-boot的路径需要根据自己的实际情况填写,这里是当前目录的情况。命令执行后,u-boot成功运行,输出了如下信息:


U-Boot 2019.10 (Mar 21 2020 - 22:14:46 +0800)

DRAM:  512 MiB
WARNING: Caches not enabled
Flash: 128 MiB
MMC:   MMC: 0
*** Warning - bad CRC, using default environment

In:    serial
Out:   serial
Err:   serial
Net:   smc911x-0
Hit any key to stop autoboot:  0 
=> 

此时,可以查看一下我们之前设置的IP地址是否生效:

=> print ipaddr
ipaddr=192.168.0.107

然后,在测试下第1节中对qemu的网络配置是否真的能让开发板和ubuntu通过网络进行通信。测试的方法很简单,直接ping主机(ubuntu)即可:

=> ping 192.168.0.100
smc911x: MAC 52:54:00:12:34:56
smc911x: detected LAN9118 controller
smc911x: phy initialized
smc911x: MAC 52:54:00:12:34:56
Using smc911x-0 device
smc911x: MAC 52:54:00:12:34:56
host 192.168.0.100 is alive

结果显示,qemu网络配置成功,u-boot可以ping通ubuntu。

5 编译linux kernel

可以在linux内核的官网下载内核源码,我选择的版本是比较新的长期支持版本5.4.26。下载完源码,并解压,然后进入源码的目录。执行vexpress开发板的默认配置:

make vexpress_defconfig ARCH=arm

此外,由于我们需要挂载网络文件系统,所以要配置内核的NFS客户端功能。具体的,执行:

make menuconfig ARCH=arm

打开图形化配置界面后,在File systems ⇒ Network File Systems配置项下,选中NFS客户端相关的项。然后,保存并退出配置界面。

接下来需要编译内核镜像、内核模块以及设备树。编译内核镜像时需要注意,因为我们是使用u-boot启动内核,因此要编译内核镜像为uImage,同时根据上文对u-boot环境变量的设置(根据开发板内存的实际地址),内核的加载地址需要设定为0x60003000

制作uImage时需要mkimage工具,没有安装的话需要安装:

apt install u-boot-tools

编译内核镜像时,执行:

make uImage LOADADDR=0x60003000 ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-

编译内核模块和设备树文件时,执行:

make modules ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-
make dtbs ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-

编译得到的设备树文件位于:arch/arm/boot/dts/vexpress-v2p-ca9.dtb。kernel的测试则放到下文,与busybox制作的根文件系统一起测试。

6 使用busybox制作根文件系统

6.1 制作过程

先到busybox的官网下载源码,然后下载解压。进入源码目录后进行配置。这里我直接采用默认配置:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- defconfig

然后,通过图形化的配置界面修改部分配置:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig

主要修改的是busybox的编译方式,设置为静态编译:Settings ⇒ Build static binary (no shared libs)

之后,就可以编译安装了:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- install

busybox默认被编译安装在源码根目录的_install目录下,进入这个目录后发现,仅有一下几个目录文件:

bin  linuxrc  sbin  usr

接下来手动为其创建几个linux kernel要用到的目录:

mkdir lib proc sys dev etc etc/init.d

为了让kernel在启动时自动挂载一些文件系统(proc、sysfs)以及创建设备节点,我们需要rcS脚本,这个脚本在kernel启动后会自动被kernel执行:

touch etc/init.d/rcS

然后再脚本中添加如下内容:

#!bin/sh

# 挂载proc文件系统
mount -t proc none /proc

# 挂载sysfs文件系统
mount -t sysfs none /sys

# mdev相当于嵌入式版本的udev
/sbin/mdev -s

同时不要忘了修改rcS的权限为可执行:

chmod a+x etc/init.d/rcS

6.2 测试

下面我们需要测试制作好的文件系统有没有问题,不妨先使用SD卡镜像的方式测试(后面搭建好了NFS环境就直接挂载网络文件系统了)。开始之前,为了方便,不妨建立一个rootfs目录,并把_install目录下的文件拷贝到rootfs

cp -rf busybox-1.31.1/_install/* rootfs/

然后,制作SD卡镜像:

dd if=/dev/zero of=rootfs.ext4.img bs=1M count=32

显然,rootfs.ext4.img并不是真正的SD卡设备,它只是我们在磁盘上创建的一个文件(为qemu创建的虚拟SD卡)。接下来我们会假装它是一张SD卡,并正儿八经的对其进行格式化、挂载等操作。先将它按照ext4文件系统格式进行格式化:

mkfs.ext4 rootfs.ext4.img

把这个镜像文件按照ext4文件系统类型进行挂载,从而方便往里面拷贝内容。注意,rootfs.ext4.img不是真正的SD卡,实质是一个文件,一个包含有文件系统格式的文件,也就是一个loop设备。所以挂载的时候要添加loop选项:

mount -t ext4 -o loop rootfs.ext4.img /mnt/rootfs

最后,将我们通过busybox创建的根文件系统拷贝到挂载点/mnt/rootfs上,并卸载rootfs.ext4.img:

cp -rf rootfs/* /mnt/rootfs/
umount /mnt/rootfs

制作好根文件系统镜像之后,就可以使用qemu运行kernel了,看看能不能成功挂载根文件系统。运行如下命令(命令中各种文件的路径要根据自己的实际情况写):

qemu-system-arm -M vexpress-a9\
                -m 512M\
	       	    -kernel ./linux-5.4.26/arch/arm/boot/zImage\
	       	    -dtb ./linux-5.4.26/arch/arm/boot/dts/vexpress-v2p-ca9.dtb\
		        -nographic\
		        -sd rootfs.ext4.img\
	        	-append "root=/dev/mmcblk0 rw console=ttyAMA0"

很幸运,kernel启动成功:

/ # ls
bin      etc      linuxrc  sbin     usr
dev      lib      proc     sys

设备文件也被自动创建了:

/ # ls /dev/
console          ptype            tty33            tty7
cpu_dma_latency  ptypf            tty34            tty8
full             random           tty35            tty9
gpiochip0        root             tty36            ttyAMA0
......(不再列出)

7 在qemu上利用u-boot启动kernel

以上准备工作都完成后,我们就可以进行我们的最终目标了。回顾一下我们的目标:

  1. 在qemu上运行u-boot
  2. 使用u-boot通过TFTP的方式加载内核到虚拟开发板的内存
  3. 内核启动后,通过网络的方式挂载文件系统(NFS)

首先把kernel镜像以及编译后的设备树文件拷贝到TFTP的传输目录:

cp linux-5.4.26/arch/arm/boot/uImage /mnt/hgfs/share/tftpboot/
cp linux-5.4.26/arch/arm/boot/dts/vexpress-v2p-ca9.dtb /mnt/hgfs/share/tftpboot/

然后输入如下命令,使用qemu运行u-boot:

qemu-system-arm -M vexpress-a9\
                -m 512M\
                -kernel u-boot\
                -nographic\
                -net nic,vlan=0 -net tap,vlan=0,ifname=tap0

u-boot运行后,可以通过TFTP加载内核,但是内核无法挂载网络根文件系统,输出的错误信息如下:

VFS: Unable to mount root fs via NFS, trying floppy.
List of all partitions:
1f00          131072 mtdblock0 
 (driver?)
1f01           32768 mtdblock1 
 (driver?)
No filesystem could mount root, tried: 
 nfs

Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(2,0)

经过一番搜索,发现是启动参数的问题,原先的启动参数如下:

root=/dev/nfs rw nfsroot=192.168.0.100:/root/work/nfsroot init=/linuxrc ip=192.168.0.106 console=ttyAMA0

修改如下:

rootfstype=nfs root=/dev/nfs rw nfsroot=192.168.0.100:/root/work/nfsroot,v3 init=/linuxrc ip=192.168.0.106:192.168.0.100::255.255.255.0 console=ttyAMA0

其中一部分修改是没有效的,最关键的修改是v3。这个参数表明,kernel在挂载网络根文件系统时,作为NFS客户端,需要按照NFS v3规定的格式向服务端运行于Ubuntu)发送相应的数据包,进而完成挂载。

为什么原先没有指定版本的参数不行呢?因为没有指定版本的话kernel会按照NFS v2向服务端发送数据(至于是u-boot默认传参v2,还是u-boot就传了没有版本的参数但kernel默认按照NFS v2,这我没有细究)。而自Ubuntu 17.10之后NFS默认支持NFS v3NFS v4,这样一来,kernel就无法挂载网络根文件系统了(更多内容可以参看参考文献[5][6])。解决这个问题可以从两个方面出发:

  1. u-boot传参
    修改u-boot传递的参数,指明NFS的版本为v3v4(当然,kernel关于NFS v3NFS v4的配置要选上);
  2. 修改主机配置
    Ubuntu默认支持NFS v3NFS v4,但我们可以通过修改主机上NFS的配置使得对NFS v2的支持也开启。

具体选用哪种解决方法,大家各凭喜好。修改完u-boot传给kernel的参数后,再次使用qemu启动u-boot,这次u-boot启动kernel后,kernel成功挂载网络根文件系统:

VFS: Mounted root (nfs filesystem) on device 0:14.
Freeing unused kernel memory: 1024K
Run /linuxrc as init process
random: fast init done

Please press Enter to activate this console. random: crng init done

/ # 

8 对开发环境的一些完善工作

到了这里,最初的目标已经达成,开发环境初步建立了,不过还有一些有待完善的内容。主要是我们构建的根文件系统太过简陋,比如缺少配置文件/etc/inittab等。

这里简单介绍一下kernel的启动流程:
在这里插入图片描述
不难发现,当前建立的根文件系统中还没有/etc/inittab/etc/profile等文件,虽然不至于让kernel无法启动,但这会导致一些应该在启动时完成的工作没有完成,比如一些文件系统没有得到挂载。

那么,怎么制作上述文件呢?这些网上有很多资料提及,不难找到,这里就不再赘述了。

参考文献

[1] 用Qemu模拟vexpress-a9 — 配置 qemu 的网络功能
[2] Ubuntu 16.04中安装tftp
[3] ubuntu——nfs服务搭建
[4] Building a Root File System using BusyBox
[5] uboot 无法通过 nfs 启动 Ubuntu 18.04 内的根文件目录
[6] How can I make the nfs server support protocol version 2 in Ubuntu 17.10?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章