相关文章推荐
朝气蓬勃的火腿肠  ·  python - What is the ...·  12 月前    · 
没读研的小蝌蚪  ·  Spark ...·  1 年前    · 
魁梧的四季豆  ·  xmlhttprequest - how ...·  1 年前    · 

嵌入式开发离不开硬件设备:开发板、外设等,但如果只是想研究Linux内核的架构/工作模式,修改一些代码然后烧写到开发板中验证,这样未必有些复杂。然而qemu可以避免频繁在开发板上烧写版本,如果仅仅是内核方面的调试,qemu完全可以完美地胜任。仿真能解决以下痛点:

  • 真实单板难以获取时,可以快速上板,无需轮候
  • 源码级的GDB(这真是一个超级强大的功能,有了它,开发效率会直线上升)
  • 快速单元测试、开发者测试
  • 业务代码无需打桩(桩还是会有条件存在的,但是转移到了qemu侧)
  • 使用qemu运行自己编译的Linux系统,并能够进行简单调试。本文不对qemu做过多分析,着重于如何快速搭建环境。

    qemu可运行在多个平台上,如Linux、windows、mac等。通常嵌入式开发是基于开源Linux的,因此我们也基于Linux环境开展实验。

  • windows下安装VMware,VMware创建Ubuntu 20.04 LTS版本的虚拟机。Ubuntu版本下载: https://ubuntu.com/download/desktop
  • 在Ubuntu安装qemu软件
  • 用qemu模拟运行arm64 Linux系统
  • qemu安装

    qemu安装方式有两种:Linux软件包安装、源码编译安装。

    软件包安装

    Ubuntu的软件包安装:qemu软件包越来越大,因此被拆分为了多个软件包。不同软件包有不同的功能,比如qemu-system-ARCH提供全系统模拟(ARCH替换为arm/mips等架构名),qemu-utils提供了一些工具。

    sudo apt install qemu-system-arm

    查看版本号,如果版本号太老,考虑使用源码编译

    lv@ubuntu:~$ qemu-system-aarch64 --version
    qemu emulator version 4.2.1 (Debian 1:4.2-3ubuntu6.16)

    qemu官网给出了安装步骤: https://www.qemu.org/download/
    qemu版本号会持续更新。make之后,编译的可执行二进制文件在 ./build目录下

    wget https://download.qemu.org/qemu-7.0.0-rc0.tar.xz
    tar xvJf qemu-7.0.0-rc0.tar.xz
    cd qemu-7.0.0-rc0
    ./configure
    

    内核编译步骤

  • 下载并解压kernel源码
  • 下载最新的kernel源码,kernel官网:https://www.kernel.org

    tar -xf linux-5.12xx.tar

  • 安装编译工具
  • gcc交叉编译工具链安装
  • 我们要在x86_64 Ubuntu系统下编译arm64镜像,因此需要交叉编译工具链。

    sudo apt install gcc-aarch64-linux-gnu

    初装的Ubuntu缺少很多编译工具,可不急于全部安装以下工具,如果make menuconfig在哪出错了,再安装对应软件包即可。

    sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc flex libelf-dev bison

  • 配置环境变量
  • 要在x86_64的宿主机编译arm64的镜像,需要arm64的gcc工具。每次重新打开shell都需要配置。

    export ARCH=arm64
    export CROSS_COMPILE=<path>/aarch64-linux-gnu-
    

    查看环境变量是否设置成功:export

  • 生成.config文件
  • 在顶层目录生成arm64的默认配置.config,其实是把arch/arm64/configs/defconfig复制到kernel源码顶层目录。

    make defconfig

  • 在.config的基础上配置其他特性开关
  • make menuconfig

  • 编译镜像,并且10个job运行,加快编译速度。生成的内核镜像在:arch/arm64/boot/Image
  • make -j 10

    启动裸内核

    qemu相关使用

  • 可执行程序
  • 在./build目录可以找到下qemu-system-aarch64可执行文件,同时目录下面还有其他cpu架构的可执行程序,命名都是qemu-system-作为前缀。

  • 查看qemu帮助信息
  • qemu-system-aarch64 -h  // 查看全部帮助信息
    qemu-system-aarch64 -machine help //查看支持的machine
    qemu-system-aarch64 -cpu help //查看machine支持的cpu类型
    During emulation, press 'ctrl-a h' to get some help
    

    启动裸内核

    执行如下命令尝试启动内核,如果一切顺利,可看到Linux的启动log,但是大概率会运行到根文件系统初始化时挂死。哈哈,不过到这里证明我们成功一半了。

    qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1024 -nographic -kernel path-to-Image-file
    

    -M 指定开发板
    -m 指定内存ram大小,单位MB
    -smp 指定core num
    -nographic 指定不需要图形界面
    -kernel 指定内核文件
    -dtb 指定dtb文件
    -hda 硬盘0,Use file as hard disk
    -hdb 硬盘1
    -append 指定启动参数command line

    $ ./build/qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1024 -nographic -kernel /home/xxxx/linux/linux-5.12.4/arch/arm64/boot/Image
    [ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x411fd070]
    [ 0.000000] Linux version 5.12.4 (xxxx) (aarch64-linux-gnu-gcc (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0, GNU ld (GNU Binutils for Ubuntu) 2.30) #1 SMP PREEMPT Mon May 17 14:36:21 CST 2021
    [ 0.000000] Machine model: linux,dummy-virt
    [ 0.000000] efi: UEFI not found.

    [ 1.080779] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
    [ 1.081468] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 5.12.4 #1
    [ 1.081886] Hardware name: linux,dummy-virt (DT)

    上文提到裸内核在初始化文件时出错而停止运行,接下来制作一个根文件系统并传递给内核,启动一个完整的内核程序。

  • 文件系统和内核是完全独立的两个部分,文件是用户与内核交互的主要工具。
  • 根文件系统是内核启动时所mount的第一个文件系统,是加载其它文件系统的”根“。
  • 一套linux体系,只有内核本身是不能工作的,必须要rootfs(etc目录下的配置文件、/bin /sbin等目录下的shell命令,还有/lib目录下的库文件等···)相配合才能工作。
  • 文件系统配置

    打开ext4支持
    不一定非得是ext4,这里勾选ext4仅仅是因为我们要制作的文件系统是ext4格式

    File systems
     ----> {*} The Extended 4 (ext4) filesystem
    

    制作根文件系统

    制作一个简易的根文件系统,该文件系统包含的功能极其简陋,仅为了验证qemu启动Linux内核后挂载根文件系统的过程。以后会进一步完善该文件系统。

    下载编译busybox

  • 从官网下载最新的busybox源码,https://busybox.net/downloads/
  • tar -xf busybox-1.29.3.tar.bz2

  • busybox的编译和Linux kernel类似都是通过kconfig管理
  • export ARCH=arm64
    export CROSS_COMPILE=<path>/aarch64-linux-gnu-
    make menuconfig
    
  • 进入menuconfig后,配置为静态编译
  • setting
     ----> Build Options
      ----> [*] Build static binary (no shared libs)
    编译完成之后,在busybox根目录下会有_install目录,该目录是编译好的一些命令集合。
    make install
    

    创建文件系统EXT4格式

  • 创建空文件
  • if= / of=,分别指定输入输出文件。/dev/zero是输出一直为零的设备。下面命令创建内容为空的文件rootfs.ext4。

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

  • 将文件格式转为ext4文件系统
  • mkfs.ext4 rootfs.ext4

    将busybox编译生成的_install目录下的文件全部拷贝到initrd。至此,简易版根文件系统就制作完成,该根文件系统只含有最基本的功能,一些其他功能在以后的操作中会进行添加。

    mkdir mnt
    sudo mount rootfs.ext4 mnt/
    cd mnt
    sudo cp -rf busybox-xxx/_install/* .
    cd ../
    sudo umount mnt
    rm -rf mnt
    

    非嵌入式启动

    qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1G -nographic -kernel path-to-Image-file -append "root=/dev/vda" -hda ~/rootfs.ext4

    嵌入式启动方式

    文件系统配置

  • 内核打开initramfs支持
    ramfs是将文件系统直接编译进内核镜像中。
  • General setup
     ----> [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support
    
  • 内核打开ramdisk支持
    ramdisk是在内存中模拟磁盘,使用ramdisk比Initramfs灵活一些,不需要每次都去编译内核。不过在qemu中,都支持通过-initrd指定文件镜像。注意ramdisk大小一定要足够大
  •  Device Drivers
      ----> [*] Block devices
       ----> <*> RAM block device support
        ----> (65535) Default RAM disk size (kbytes)
    

    制作initramfs

    cp busybox/_install/  rootfs
    cd rootfs
    find . | cpio -o -H newc > rootfs.cpio
    gzip -c rootfs1.cpio > rootfs.cpio.gz
    
  • initramfs启动
  • qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1G -nographic -kernel path-to-Image-file -initrd ~/rootfs.cpio.gz -append "rdinit=/linuxrc"
    
  • ramdisk启动
  • qemu-system-aarch64 -M virt -cpu cortex-a57 -m 1G -nographic -kernel path-to-Image-file -append "root=/dev/ram0 ramdisk_size=65535" -initrd ~/rootfs.ext4
    

    initramfs与initrd区别

    Linux内核只认cpio格式的initramfs文件(因为unpack_to_rootfs只能解析cpio格式文件),非cpio格式的initramfs文件包将被系统抛弃,而initrd可以是cpio包也可以是传统的镜像文件,实际使用中initrd都是传统镜像文件如ext4格式。
    使用initramfs,命令行参数将不需要"root="命令。
    如下,kernel会首先尝试解析initramfs,然后尝试initrd(ramdisk)。

    [ 0.548161] Trying to unpack rootfs image as initramfs...
    [ 0.550507] rootfs image is not initramfs (invalid magic at start of compressed archive); looks like an initrd

    ramdisk大小

    如果RAM disk size这个大小和你做的ramdisk不匹配,则启动时仍然会出现 kernel panic内核恐慌,提示ramdisk格式不正确,挂载不上ramdisk。也可通过设置启动参数修改ramdisk大小。“ramdisk_size=65536”

     Device Drivers
      ----> [*] Block devices
       ----> <*> RAM block device support
        ----> (65535) Default RAM disk size (kbytes)
    

    挂载失败打印:

    RAMDISK: ext2 filesystem found at block 0
    RAMDISK: image too big! (32768KiB/4096KiB)
    VFS: Cannot open root device "(null)" or unknown-block(0,0): error -6

    串口打印, 不能打开tty文件

    文件系统添加dev目录

    sudo mkdir dev
    

    找不到rcS文件

    can't run '/etc/init.d/rcS': No such file or directory,
    /etc/init.d/rcS allows you to run additional programs at boot time,
    在文件系统中新建rcs解决此问题

    sudo mkdir -p etc/init.d/
    cd etc/init.d/
    sudo chmod 777 rcS
    

    kernel启动参数

    不知怎的,最新版本的Linux没有这个文档了,看看之前版本的吧

    https://elixir.bootlin.com/linux/v2.6.37.5/source/Documentation/kernel-parameters.txt

    常用的主要有

  • initrd [BOOT] Specify the location of the initial ramdisk
  • loglevel
  • ramdisk_size
  • gdb调试

  • arm不能使用普通的gdb,要用gdb-multiarch
  • sudo apt install gdb-multiarch

  • 确保编译的内核包含调试信息
  • Kernel hacking
     ----> Compile-time checks and compiler options
      ----> [*] compile the kernel with debug info
      ----> [*] Provide GDB scripts for kernel debugging
    
  • qemu命令需要添加以下选项
  • qemu-system-aarch64  -S -s
    

    -S:表示qemu虚拟机会冻结CPU,直到远程的gdb输入相应控制命令
    -s:表示在1234端口接受gdb的调试连接

  • 在另一终端输入命令
  • gdb-multiarch --tui
    (gdb) file vmlinux //加载kernel符号表
    (gdb) target remote localhost:1234 //通过1234端口连接qemu平台
    (gdb) b start_kernel //在内核start_kernel设置断点
    (gdb) c //continue 运行

    Linux kernel使用了大量的static函数,配合-O2编译选项,这些static函数被自动inline,这样可以达到优异的速度优化效果,但是也让gdb的时候带来一些小困扰。

    这个问题似乎没有更好的解决办法,因为-O2是不能关闭的,因此一般都是在函数名前加上这样一个定义来临时规避这个问题:

    __attribute__((optimize("O0")))
    

    gdb调试,在start_kernel断点停不住

    关闭kaslr

    qemu-system-aarch64 -append nokaslr
    

    启动打印时间戳

    Kernel hacking
     ----> printk and dmesg options
     ----> [*] Show timing information on printk
    

    earlyprintk

    因为printk依赖于console,而console驱动此时还未加载,这个阶段printk的内存都被存储到log_buf中,待console驱动加载后才会一次性地打印出来。在console被初始化之前,earlyprintk将printk的所有内容直接输出到串口,console初始化完毕后,由console驱动接管printk的内容。打开earlyprintk有以下两种方式:
    1、打开config 宏
    2、增加启动参数:bootargs还要增加earlyprintk

    buildroot

    可借助buildroot,生成kernel、rootfs等

    riscv linux启动

    qemu启动命令和arm64一样,只不过需要编译出riscv的linux和busybox镜像。
    https://risc-v-getting-started-guide.readthedocs.io/en/latest/linux-qemu.html

    ~/workspace/qemu/qemu-7.0.0-rc0/build/qemu-system-riscv64 \
      -M virt \
      -smp 1 \
      -m 1024 \
      -nographic \
      -kernel ~/workspace/buildroot/buildroot-2022.02.3/output/images/Image \
      -initrd ~/workspace/buildroot/buildroot-2022.02.3/output/images/rootfs.cpio \
      -append "root=/dev/ram0 ramdisk_size=65535 nokaslr"