1. Running RISC-V Linux on QEMU

QEMU是纯软件实现的虚拟化模拟器,几乎可以模拟任何硬件设备,我们最熟悉的就是能够模拟一台能够独立运行操作系统的虚拟机,虚拟机认为自己和硬件打交道,但其实是和 Qemu 模拟出来的硬件打交道,QEMU将这些指令转译给真正的硬件。

Ps.所有图片都是以64bit为例的,跟32bit区别不大。

1.1. 准备

安装依赖项:

sudo apt install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev gawk \
build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev git

然后创建主工作文件夹并进入目录:

  • 32bit RISC-V
mkdir ~/riscv32-linux
cd ~/riscv32-linux
  • 64bit RISC-V
mkdir ~/riscv64-linux
cd ~/riscv64-linux

1.2. 下载/拉取相关资源并初步测试

下载所需的源文件:

  • QEMU
  • Linux
  • Busybox

通过git命令即可:

git clone https://github.com/qemu/qemu
git clone https://github.com/torvalds/linux
git clone https://git.busybox.net/busybox

由于某些神秘力量,Github经常寄,所以考虑换镜像站,QEMU在./configure的过程中也需要连接Github服务器所以,直接wget源码:

  • QEMU
wget https://download.qemu.org/qemu-8.0.3.tar.xz
tar xvJf qemu-8.0.3.tar.xz
  • Linux
git clone https://mirrors.tuna.tsinghua.edu.cn/git/linux.git
  • Busybox
git clone https://gitee.com/mirrors/busyboxsource.git

整个编译过程还需要一个RISC-V交叉编译工具链,通过网站下载:

https://toolchains.bootlin.com

在这里插入图片描述

新建存放工具链的文件夹toolchain,在toolchain文件夹解压,查看一下目录下的文件:

ls toolchain/riscv64-lp64d--glibc--bleeding-edge-2022.08-1/

在这里插入图片描述

通过添加环境变量安装这个工具链:

  • 32bit RISC-V
sudo vim ~/.bashrc
# 在最后一行添加(此处为riscv32)
export PATH="/home/$USER/riscv32-linux/toolchain/riscv32-ilp32d--glibc--bleeding-edge-2022.08-1/bin":$PATH
  • 64bit RISC-V
sudo vim ~/.bashrc
# 在最后一行添加(此处为riscv64)
export PATH="/home/$USER/riscv64-linux/toolchain/riscv64-lp64d--glibc--bleeding-edge-2022.08-1/bin":$PATH

测试一下交叉编译工具,随便写一个main.c(随便某处):

#include <stdio.h>

int main(int argc, char *argv[]) {
    int i = 1;
    for (i = 1; i < 16; i++)  {
        printf("hello %d\n", i);
    }
    return 0;
}

输入命令编译这个文件

  • 32bit RISC-V
riscv64-buildroot-linux-gnu-gcc main.c -o main.out
  • 64bit RISC-V
riscv64-buildroot-linux-gnu-gcc main.c -o main.out

查看一下现在路径下的文件:

在这里插入图片描述

反汇编一下:

  • 32bit RISC-V
riscv32-buildroot-linux-gnu-objdump -h -D main.out > main.asm
vim main.asm
  • 64bit RISC-V
riscv64-buildroot-linux-gnu-objdump -h -D main.out > main.asm
vim main.asm

可以在下面看到,调用了printf来完成打印。可以证明该交叉编译器正常工作。

在这里插入图片描述

1.3. QEMU编译

  • 32bit RISC-V
cd qemu-8.0.3
./configure --target-list=riscv32-softmmu
make -j $(nproc)
sudo make install
  • 64bit RISC-V
cd qemu-8.0.3
./configure --target-list=riscv64-softmmu
make -j $(nproc)
sudo make install
  • 32bit & 64bit RISC-V
cd qemu-8.0.3
./configure --target-list=riscv32-softmmu,riscv64-softmmu
make -j $(nproc)
sudo make install

1.4. Linux内核编译

接下来通过交叉编译器来为RISC-V编译Linux系统内核,并在QEMU上进行模拟。

编译linux内核:

  • 32bit RISC-V
cd ~/riscv32-linux/linux
make ARCH=riscv CROSS_COMPILE=riscv32-buildroot-linux-gnu- defconfig
make ARCH=riscv CROSS_COMPILE=riscv32-buildroot-linux-gnu- -j $(nproc)
  • 64bit RISC-V
cd ~/riscv64-linux/linux
make ARCH=riscv CROSS_COMPILE=riscv64-buildroot-linux-gnu- defconfig
make ARCH=riscv CROSS_COMPILE=riscv64-buildroot-linux-gnu- -j $(nproc)

编译完成。

1.5. 添加自己的C程序

进入busybox目录:

  • 32bit RISC-V
cd ~/riscv32-linux/busyboxsource
  • 64bit RISC-V
cd ~/riscv64-linux/busyboxsource

创建一个目录,比如叫mine:

mkdir ./mine
cd ./mine

创建并编辑一个文件,hello_busybox.c:

vim ./hello_busybox.c

比如这样,比如参考初步测试中:

#include "libbb.h"

int hello_busybox_main(int argc, char *argv[]) {
    int i = 1;
    for (i = 1; i < 16; i++)  {
        printf("hello %d\n", i);
    }
    return 0;
}

注意此处主函数名字,不再是main,而是xxx_main。因为BusyBox从busybox.c文件中的main函数开始执行,int main(int argc, char *argv[])argc为参数数量,argv[0]为applet_name,后续执行过程如下:

  • 调用 applets/applets.c 文件的 return lbb_main(argv);
  • 在 include/applets.h 中 填充内容为xxx_main函数;
  • 自此跳转到 applet 中执行。

因此新增的applet函数为int xxx_main(int argc, char*argv[])格式。

下一步在,在*.c文件路径下添加文件Config.in:

vim Config.in

Config.in:

menu "Mine"
config HELLO_BUSYBOX
        bool "say hello to busybox"
        default y
        help
            say hello to busybox
endmenu

修改这里主要是使得后面执行“make menuconfig”的时候,配置界面可以出现我们新增的命令,让用户对该命令可以配置,解释如下:

  • 第一行是设定最外层菜单级别并设置出现在菜单界面上的文字;
  • 第二行是表示该命令的一个环境变量;
  • 第三行是出现在配置界面上的文字,是一个布尔量,取值为“Y”或者“N”;
  • 第四行是这个选项的默认值,这里默认是选中;
  • 后面两行是在配置界面的帮助信息。

与Config.in相同,接下来添加Kbuild文件并编辑:

vim Kbuild

Kbuild:

lib-y:=
    lib-$(CONFIG_HELLO_BUSYBOX)    += hello_busybox.o

简单地说,这里是添加需要链接的库。

下一步,修改include/applets.src.h文件:

cd ../include/
vim ./applets.src.h

在文件中添加一行,注意,添加的新行一定要在INSERT字段之后、#endif之前进行添加,否则在后面make的时候会被自动删掉:

IF_HELLO_BUSYBOX(APPLET(hello_busybox, BB_DIR_SBIN, BB_SUID_DROP))

如图:

在这里插入图片描述

解释:第一个参数:命令的名字;第二个参数:存放的路径:第三个参数:权限

最后一步,为命令添加帮助信息,以便使用–help的时候查阅(不做这步会报错):

vim ./usage.src.h

同样地,一定要在INSERT字段之后、#endif之前进行添加:

#define hello_busybox_trivial_usage "None"
#define hello_busybox_full_usage "None"

如图:

在这里插入图片描述

继续修改一些文件,返回上一层,修改总的Config.in

cd ..
vim ./Config.in

在最后一行添加一句:

source mine/Config.in

如图:

在这里插入图片描述

还有Makefile,在libs-y下面添加一行,记得按照字母顺序:

mine/ \

如图:

在这里插入图片描述

至此,定制自己的C程序完成,接下来通过busybox生成根文件系统。

1.6. 利用busybox制作rootfs

进入busybox目录:

  • 32bit RISC-V
cd ~/riscv32-linux/busyboxsource
  • 64bit RISC-V
cd ~/riscv64-linux/busyboxsource

然后配置busybox:

  • 32bit RISC-V
CROSS_COMPILE=riscv32-buildroot-linux-gnu- make menuconfig
  • 64bit RISC-V
CROSS_COMPILE=riscv64-buildroot-linux-gnu- make menuconfig

在弹出的菜单里找到Settings -> Build static binary (no shared libs),勾选:

在这里插入图片描述

Exit返回上一步,并向下,可以看到自己的程序的相关选项:

在这里插入图片描述

在这里插入图片描述

然后Exit -> Exit -> … -> Yes:

接着执行make及make install:

  • 32bit RISC-V
CROSS_COMPILE=riscv32-buildroot-linux-gnu- make -j $(nproc)
CROSS_COMPILE=riscv32-buildroot-linux-gnu- make install
  • 64bit RISC-V
CROSS_COMPILE=riscv64-buildroot-linux-gnu- make -j $(nproc)
CROSS_COMPILE=riscv64-buildroot-linux-gnu- make install

稍加等待,完成后生成_install文件夹,这个文件夹下的东西就是根文件系统要用的。

制作一个最小的文件系统,先回到主工作目录,然后使用qemu-img生成一个img文件:

  • 32bit RISC-V
cd ~/riscv32-linux
qemu-img create rootfs.img 1g
mkfs.ext4 rootfs.img
  • 64bit RISC-V
cd ~/riscv64-linux
qemu-img create rootfs.img 1g
mkfs.ext4 rootfs.img

rootfs.img是文件系统的镜像文件名,1g是磁盘文件大小,可以根据需要修改。并将磁盘文件格式化为ext4文件格式。

挂载这个img,将_install目录下的东西拷贝到这个文件系统中,除此之外再创建一些必要的文件和目录。

mkdir rootfs
sudo mount -o loop rootfs.img  rootfs
cd rootfs
sudo cp -r ../busyboxsource/_install/* .
sudo mkdir proc sys dev etc etc/init.d

然后另外再新建一个最简单的init的RC文件:

cd etc/init.d/
sudo vim rcS

该文件如下:

#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
/sbin/mdev -s

然后修改rcS文件权限,加上可执行权限,这样当busybox的init运行起来后,就能运行这个/etc/init.d/rcS脚本:

sudo chmod +x rcS

最后退出 rootfs 目录并卸载文件系统:

sudo umount rootfs

至此,文件系统就制作完成了。

1.7. 利用QEMU运行系统

回到主工作路径,并运行命令启动QEMU:

  • RISC-V 32bit
#!/bin/sh
cd ~/riscv32-linux
qemu-system-riscv32 -M virt -m 256M -nographic \
        -kernel linux/arch/riscv/boot/Image \
        -drive file=rootfs.img,format=raw,id=hd0 \
        -device virtio-blk-device,drive=hd0 \
        -append "root=/dev/vda rw console=ttyS0"
  • RISC-V 64bit
#!/bin/sh
cd ~/riscv64-linux
qemu-system-riscv64 -M virt -m 256M -nographic \
        -kernel linux/arch/riscv/boot/Image \
        -drive file=rootfs.img,format=raw,id=hd0 \
        -device virtio-blk-device,drive=hd0 \
        -append "root=/dev/vda rw console=ttyS0"

命令过长不方便,也可以编辑一个.sh文件,例如:

  • RISC-V 32bit
vim ./yuanshenqidong32.sh
  • RISC-V 64bit
vim ./yuanshenqidong64.sh

将上面那段命令复制粘贴到.sh文件并保存,然后运行下面的命令即可:

  • RISC-V 32bit
sh ./yuanshenqidong32.sh
  • RISC-V 64bit
sh ./yuanshenqidong64.sh

系统成功在QEMU上启动:

在这里插入图片描述

再看一下自定义测试程序的情况,直接输入命令:

hello_busybox

窗口打印出与代码逻辑相符的结果:

在这里插入图片描述

要退出当前QEMU模拟器终端,只需先按下Ctrl+A,松开后按下X即可。

至此,基础的QEMU上运行64bit RISC-V Linux完成。

1.8. Build static binary (no shared libs) 问题

之前勾选了Build static binary (no shared libs),所以相关的库都是静态链接的,这会导致静态编译的用户程序直接给到文件系统能运行,但动态编译的用户程序无法直接运行,而去掉这个选项在QEMU启动时又会因为根文件系统中相关的库不完备而报错。我们再做一些操作,来解决这个问题。

Ps. 对于上一节做进系统的程序命令hello_busybox其实也是静态编译的,只不过上文说的动态静态编译指的是人工地在Ubuntu上用交叉编译器进行编译(静态:riscv-buildroot-linux-gnu-gcc -static xxx.c -o xxx,不加-static则是动态),这个做进系统的命令给其实是到busybox中静态编译(因为那个选项),所以做进系统的命令与外部人工-static编译并无本质区别。

上一章中“利用busybox制作rootfs”,当执行到menuconfig时,我们不勾选Build static binary (no shared libs),勾选其子选项Build position independent executable(我觉得应该勾选这个,build到独立位置)

在这里插入图片描述

然后继续原来的步骤,直到给rcS文件添加好权限后,先不要umount取消挂载,交叉编译工具链文件夹中提供了一个sysroot,参考如下:

  • RISC-V 32bit
cd home/$USER/riscv32linux/toolchain/riscv32-ilp32d--glibc--bleeding-edge-2022.08-1/riscv32-buildroot-linux-gnu/
ls
cd ./sysroot
ls
  • RISC-V 64bit
cd /home/$USER/riscv64-linux/toolchain/riscv64-lp64d--glibc--bleeding-edge-2022.08-1/riscv64-buildroot-linux-gnu/
ls
cd ./sysroot
ls

看看情况:

在这里插入图片描述

交叉编译工具链提供了一个lib完备的根文件系统,里边有相应的lib,可以直接给到给到rootfs使用:

  • RISC-V 32bit
cd home/$USER/riscv32-linux/
sudo mount -o loop rootfs.img  rootfs
cd rootfs
sudo cp -r ../toolchain/riscv32-ilp32d--glibc--bleeding-edge-2022.08-1/riscv32-buildroot-linux-gnu/sysroot/* .
  • RISC-V 64bit
cd home/$USER/riscv64-linux/
sudo mount -o loop rootfs.img  rootfs
cd rootfs
sudo cp -r ../toolchain/riscv64-lp64d--glibc--bleeding-edge-2022.08-1/riscv64-buildroot-linux-gnu/sysroot/* .

还需要搞一下环境变量,不然正常进入系统,什么命令都没得用,甚至ls都不行:

cd ./etc
sudo vim profile

第一行原来的不要了,直接把改成这样:

export PATH="/bin:/sbin:/usr/bin:usr/sbin:/var/bin:/var/sbin":$PATH

如图:

在这里插入图片描述

还要把程序编译一下拷给根文件系统,这次的程序是这样的:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int i = 1;
    for (i = 1; i < 16; i++) {
     printf("i = %d\n", i);
    }
    printf("Hello World!!!\n");
    return 0;
}

编译(动态/静态编译对比一下):

  • RISC-V 32bit
riscv32-buildroot-linux-gnu-gcc main.c -o main
riscv32-buildroot-linux-gnu-gcc -static main.c -o main_static
  • RISC-V 64bit
riscv64-buildroot-linux-gnu-gcc main.c -o main
riscv64-buildroot-linux-gnu-gcc -static main.c -o main_static

如图,注意大小:

在这里插入图片描述

复制可执行程序

  • RISC-V 32bit
cd home/$USER/riscv32-linux/rootfs/opt
cp ~/Desktop/test/main ./    #前一个路径是编译好的程序存放的地方
  • RISC-V 64bit
cd home/$USER/riscv64-linux/rootfs/opt
cp ~/Desktop/test/main ./    #前一个路径是编译好的程序存放的地方

最后取消挂载:

sudo umount rootfs

然后同样地:

  • RISC-V 32bit
sh ./yuanshenqidong32.sh
  • RISC-V 64bit
sh ./yuanshenqidong64.sh

在QEMU模拟器窗口Terminal运行程序:

/opt/main

程序顺利运行:

在这里插入图片描述

针对动/静态编译/链接的说明:

1、静态链接与动态链接的概念

静态链接:编译系统在链接阶段将程序的输出与所欲需要的库函数链接在一起,这样生成的可执行文件可以在没有函数库的情况下运行。

动态链接:与静态链接相反,编译系统在链接阶段不将程序的输出与所欲需要的库函数链接在一起,这样生成的可执行文件不能单独运行,其所在的文件系统中必须要有所需的库。

2、静态链接与动态链接的优缺点

  • 静态链接

    • 优:运行效率高
    • 缺:生成的可执行文件较大
  • 动态链接

    • 优:生成的可执行文件较小
    • ​缺:运行效率低

3、在配置Busybox的选项时,选择静态链接与动态链接的区别:

在使用静态链接方式(勾选Build static binary (no shared libs)选项)进行编译时,所需的库已经与程序静态地链接在一起,这些程序不需要额外的库就可以单独运行,也就是只需要拷贝_install文件夹下的东西就够了(其实这个文件夹下只有busybox是一个程序,其他全都是指向该程序的软连接)。但是自己编写的程序在文件系统上运行必须采用静态编译,否则会报类似“-bin/sh: hello: not found”的错误。

如果想要使用动态链接方式,那就需要把riscv编译器里面的提供的库文件加进去,环境变量之类的也要改一下,然后busybox选择动态编译的。在选了动态编译之后,命令和工具集需要加载lib里面的库,所以在制作根文件系统的时候必须要添加库文件,否则根文件系统无法启动。

一般动态链接方式:

准备三个文件:

main.c

#include "hello.h"

int main(int argc, char const *argv[]) {
  hello();
  return 0;
}

hello.h

#ifndef HELLO_H
#define HELLO_H
void hello();
#endif

hello.c

#include <stdio.h>
#include "hello.h"

void hello() {
  printf("Hello\n");
}

动态链接库命名规则是固定的,在动态库名增加前缀lib,文件扩展名为.so,如libhello.so.

执行命令:

# 生成hello.o
gcc -c hello.c
# 生成动态链接库libhello.so
gcc -shared -fPIC -o libhello.so hello.o
# 使用动态链接库编译main.c
gcc -o main main.c -L. -lhello

-shared:该选项指定生成动态连接库;

-fPIC:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的;

-L.:表示要连接的库在当前目录中;

-lhello:编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.so来确定库的名称。

拷贝动态库:

sudo cp libhello.so /usr/lib

然后运行:

./main

不拷贝直接运行的话会报错:

./main: error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐