32bit RISC-V Linuxを作りQEMUで実行する

皆様こんにちは.18のhiraです.
僕はコンピュータ工学をやっているつもりで,最近は作成したプロセッサでLinuxを動かそうとしているらしいです.

そのため対称実験用に正しく動くものが欲しいと思い,作っているプロセッサと同じ32bit RISC-VでLinuxを動かす環境を探していました.そこでプロセッサエミュレータであるQEMUでLinuxを動かしました.

その際,64bit向けはあっても意外に32bit向けの構築方法のまとめは少なかったため,32bit環境でLinuxをソースコードからRISC-V向けにビルドしたり,それをQEMUで実行する流れをまとめました.

やっている内容はほぼリンクのブログ(1)リンクのブログ(2)の丸パクリなのですが,一部変える点があったり,補足したい点があったため記録します.

QEMU上で32bit RISC-V Linuxを動かした際のブート画面

注: もし何も表示されないなどのエラーが出た場合,面倒ですが一度toolchainなどを全部アンインストールして,下記の方法に従って1からやり直した場合が良いかもしれません(実は設定が1つ抜けていたとかが多い).

1. riscv-gnu-toolchain のインストール

riscv-gnu-toolchainはC/C++のソースコードをRISC-Vのバイナリにコンパイルするためのコンパイラです.これを用いてLinuxのソースコードをRISC-V向けにビルドします.
特に今回は32bit用コンパイラをインストールします.

以下の手順でriscv-gnu-toolchainをGithubから取ってきてビルドします.

git clone --depth 1 https://github.com/riscv-collab/riscv-gnu-toolchain.git
cd riscv-gnu-toolchain/
./configure --prefix=/opt/riscv32ima --with-arch=rv32ima --with-abi=ilp32
sudo make -j $(nproc) linux
sudo make install

この時ビルド時の設定を「./configure」で指定します.今回は以下の点を変更します:

  • prefix: コンパイラの実行ファイルの生成先.一般的には「/opt/riscv」とか「/opt/riscv32」,「/opt/riscv32ima」みたいな場所に置く人が多いようですが任意です.後で.bashrcにここのパスを記録します.
  • with-arch: コンパイルで生成されるアーキテクチャの指定.今回はC++コードを32bit RISC-Vの基本命令(I: 整数, M: 乗算, A: アトミック)だけの「riscv32ima」にコンパイルします.もし浮動小数を使いたいなどの場合は「f」などを追加しますが,今回は使いません(むしろ余計な圧縮「c」などがあると失敗するかも).
  • with-abi: データ型のサイズなどを指定します.今回は「ilp32」という形式を使います.double型まで定義した「ilp32d」とかもあるのですが,ilp32dを指定したらtoolchainのビルドに失敗したためilp32にしました(今回はfloatもdoubleも使わないので問題ありません).

ビルドが成功すると,prefixで指定したフォルダ内に「bin」や「include」フォルダなどが作成され,binフォルダ内に「riscv32-unknown-linux-gnu-gcc」などの実行ファイルができているはずです.

/opt/riscv32ima/bin の中身(-linux~以外のものもあります)

これらが生成されていればtoolchainのインストール成功です.
「~/.bashrc」を開いて次の行を追加します.

export PATH=$PATH:/opt/riscv32ima/bin

「opt/riscv32ima」はprefixで指定したフォルダのパスです.
これにより以降のLinuxなどのビルド時に,PCが生成されたRISC-Vコンパイラを呼び出すことができます.

2. QEMUのインストール

プロセッサエミュレータであるQEMUをインストールします.今回は32bit RISC-Vを対象とします.

GithubからQEMUを取ってきてビルドします.toolchain同様に,prefixで指定した任意の「/your_qemu_path」フォルダにQEMUのソースと生成された実行ファイルを格納します.

cd /yor_qemu_path/
git clone --depth 1 --branch v5.2.0 https://github.com/qemu/qemu
cd qemu
./configure --target-list=riscv32-softmmu,riscv64-softmmu --prefix=/yor_qemu_path/
make -j $(nproc)
sudo make install

「target-list」で指定した「riscv32-softmmu」は,OSを搭載するために必要なメモリ管理機構(MMU)を使用するという意味です.MMUの解説はぜひ僕の以前記事をご覧ください.

僕の環境ではyour_qemu_pathに適当に作成した「/…/QEMU」フォルダをしていしました.ビルドに成功するとQEMUフォルダに「bin」フォルダなどが作製され,そのbinフォルダには実行ファイルである「qemu-system-riscv32」などが生成されているはずです.

/your_qemu_pat/bin/にできたQEMUの実行ファイル

3. Linuxカーネルのビルド

いよいよLinux本体を32bit RISC-V向けにビルドします.LinuxにはUbuntuやFedoraといったディストリビューションがありますが,今回は素のLinuxのソースコードをGithubから取ってきて自分でビルドします.

この時,32bit RISC-V向けにビルドするため,コンパイラとして先程インストールしたriscv-gnu-toolchainを指定します.exportでコンパイラのパス(/opt/riscv32imaなど)を指定します.

例によってwordpressでのソースコードの貼り方が分からないため,一部の半角「$」が「半$」になっています.実行時は半角に直して下さい.

cd /your_linux_path(任意パス)
git clone --depth 1 --branch v5.10 https://github.com/torvalds/linux.git linux-v5.10
export RISCV=/opt/riscv32ima
export PATH=$PATH:半$RISCV/bin
export CCPREFIX=riscv32-unknown-linux-gnu-
make ARCH=riscv CROSS_COMPILE=$CCPREFIX defconfig
make ARCH=riscv CROSS_COMPILE=$CCPREFIX menuconfig

menuconfigのコマンドを実行すると下の図のようなエディタが表示されます.

最初に「Boot options」に移動して「UEFI runtime support」のチェックを外して無効化します.

続いて「Platform type」に移動して以下の3項目を変更します:

  1. 「Base ISA」を「RV32I」にする
  2. 「Emit compressed~」のチェックを外して無効化する(先程のUEFI~を先に無効化する必要あり)
  3. 「FPU support」のチェックを外して無効化する

2のEmit~が有効になっていると,作成されるLinuxの実行ファイルがRISC-Vの16bit圧縮命令(C=compress)で構成されてしまいます.
圧縮命令の動作は圧縮前の命令と等価です.プログラムのサイズを減らせ,さらに一度に多くの命令をキャッシュに入れられキャッシュミスが減るという利点があります.
しかし圧縮された命令を展開する機構が必要なため,今回の環境では使えません.

上記の変更が済んだらSaveより保存して下記のコマンドで実際にビルドします.

make -j $(nproc) ARCH=riscv CROSS_COMPILE=半$CCPREFIX

ビルドが成功すると/your_linux_path/linux-v5.10/arch/riscv/boot/フォルダに「Image」というファイルが作製されます.

Linuxの実行ファイル:Image

Linuxを動かす上で使用するのがUNIXコマンドですが,そのUNIXコマンドを提供するコマンドのセットがBusyBoxです.

4. BusyBoxのビルド

cd /your_busybox_path(任意パス)
git clone --depth 1 --branch 1_32_0 https://github.com/mirror/busybox.git
cd busybox
export CCPREFIX=riscv32-unknown-linux-gnu-
CROSS_COMPILE=$CCPREFIX make defconfig
CROSS_COMPILE=$CCPREFIX make menuconfig

menuconfigウィンドウが開くので,最初の「Settings」の中に入り「Build static binary」にチェックを入れます.

「Build static binary」にチェックする

変更したらExitからメニューを終了します.その際に変更を保存します.
その後下記のコマンドで実際にビルドします.

CROSS_COMPILE=$CCPREFIX make -j 半$(nproc)

ビルドが終わったら,同じディレクトリで以下のコマンドを続行します.これによりBusyBoxを組み込んだファイルシステムが作製されます.
(こちらはvirtual hard driveを使う方法で,initramfsを使う方法は本ブログ末尾に記載)

dd if=/dev/zero of=busybox.bin bs=1M count=32
mkfs.ext2 -F busybox.bin
mkdir mnt
sudo mount -o loop busybox.bin mnt
cd mnt
sudo mkdir -p bin etc dev lib proc sbin tmp usr usr/bin usr/lib usr/sbin
sudo cp /your_busybox_path/busybox bin
sudo ln -s ../bin/busybox sbin/init
sudo ln -s ../bin/busybox bin/sh
cd ..
sudo umount mnt

やっていることとしては,

  1. ddコマンドでゼロの羅列をコピーして1MBサイズのファイルbusybox.binを作製する
  2. mkfs.ext2コマンドでbusybox.binをext2形式のファイル構造にフォーマットする
  3. mountコマンドでmntディレクトリにbusybox.binのファイル構造を展開する
  4. cpやlnでmntの中身を操作することでbusybox.binファイル構造の中身を操作する

というかんじです.

これが終了したら32bit RISC-V LinuxのQEMUでの実行に必要な全てのファイルの完成です!

5. 32bit RISC-V LinuxのQEMUでの実行

いよいよQEMUを実行します.QEMUの実行ファイルのフォルダに移動して,これまで作成したLinux,ファイルシステムを指定してQEMUを実行します.

cd /your_qemu_path/bin
./qemu-system-riscv32 -nographic \
    -machine virt \
    -kernel /your_linux_path/linux-v5.10/arch/riscv/boot/Image \
    -append "root=/dev/vda rw console=ttyS0 earlycon=sbi keep_bootcon bootmem_debug" \
    -drive file=/your_busybox_path/busybox.bin,format=raw,id=hd0 \
    -device virtio-blk-device,drive=hd0

実行に成功すると,冒頭のようなOpenSBIマークから続くブート画面が表示されます.

以下のように続き,試しにlsコマンドである「/bin/busybox ls -a」と入力すると,正しくフォルダ内が表示されています.

QEMUでの32bit RISC-V Linuxの実行結果

この状態ではまだLinuxにBusyBoxがインストールされてはいないため,QEMU上で下記のコマンドを入力してインストールします.

/bin/busybox --install -s
mount -t proc proc /proc

これを行うことで,いちいち/bin/busyboxなど指定しなくても,unameコマンドやlsコマンドが使えるようになります.

BusyBoxのインストールとCPU情報などの表示

補足: initramfsを使う方法

補足-1: initramfsの作製

busybox.binを使うのではなく,initramfsを使う方法は下記となります.ここでもmakeで作成した実行ファイルbusyboxは使うため,4の途中まで進めておきます(your_busybox_path/busyboxにいるとします).

mkdir initramfs
cd initramfs
mkdir -p {bin,sbin,dev,etc,home,mnt,proc,sys,usr,tmp}
mkdir -p usr/{bin,sbin}
mkdir -p proc/sys/kernel
cd dev
sudo mknod sda b 8 0
sudo mknod console c 5 1
cd ..
cp ../busybox ./bin/

ここまででLinux実行時に展開されるフォルダ/binなどが作製されます.ここで,mknodコマンドは特殊ファイルの作製に使われ,/devの中の特殊ファイルは外部デバイス(記憶装置,プリンタなど)との情報のやりとりに使われます.
ここでsdaは,「ブロック単位(「b」)で読み書きするメジャー番号8番(SCSI disk: SCSIプロトコルに準拠した記憶装置系)に属する装置の0個目」を意味します.

続いて,このフォルダの中に4章のmakeコマンドで作成しておいたbusyboxをコピーします.

cp ../busybox ./bin/

そして,ブート時に実行される動作を決めるため,initramfsフォルダの下に「init」という名前のファイルを作製し,以下の内容を記述します.

.../initramfs$ vim init
#!/bin/busybox sh
# Make symlinks
/bin/busybox –install -s
# Mount system
mount -t devtmpfs devtmpfs /dev
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t tmpfs tmpfs /tmp
# Busybox TTY fix
setsid cttyhack sh
# https://git.busybox.net/busybox/tree/docs/mdev.txt?h=1_32_stable
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
sh

以上の内容を書いたらセーブし,chmodコマンドで実行権限を与えます.

chmod +x init

最後に,initramfsを含む「initramfs.cpio.gz」ファイルを作製します.Linuxはこの圧縮ファイルを使ってブートします.

find . -print0 | cpio --null -ov --format=newc | gzip -9 > initramfs.cpio.gz

補足-2: Linuxへのinitramfsの登録

再度3章と同様に,Linuxの設定を開きます.
General setupを開き,以下の2つを設定して作成したinitramfsを登録します.

  • 「Initial RAM filesystem ~」にチェックして有効にする
  • 直後の「Initramfs source file(s)」に作成したinitramfs.cpio.gzのパスを追加する

以上の再設定が終わったら,3章同様にカーネルをビルドし直します.

補足-3: QEMUでのinitramfs番Linuxの実行

以下のコマンドでQEMUを実行します.

cd /your_qemu_path/bin
./qemu-system-riscv32 -nographic \
    -machine virt \
    -kernel /your_linux_path/linux-v5.10/arch/riscv/boot/Image \
    -append "console=ttyS0 earlycon=sbi keep_bootcon bootmem_debug"

成功すると,OpenSBIのマークや色々の文章が出てきて,最終的にコマンドラインが表示されます.

文字列としては以下のようになります.

OpenSBI v0.8
   ____                    _____ ____ _____
  / __ \                  / ____|  _ \_   _|
 | |  | |_ __   ___ _ __ | (___ | |_) || |
 | |  | | '_ \ / _ \ '_ \ \___ \|  _ < | |
 | |__| | |_) |  __/ | | |____) | |_) || |_
  \____/| .__/ \___|_| |_|_____/|____/_____|
        | |
        |_|

Platform Name       : riscv-virtio,qemu
Platform Features   : timer,mfdeleg
Platform HART Count : 1
Boot HART ID        : 0
Boot HART ISA       : rv32imafdcsu
BOOT HART Features  : pmp,scounteren,mcounteren,time
BOOT HART PMP Count : 16
Firmware Base       : 0x80000000
Firmware Size       : 80 KB
Runtime SBI Version : 0.2

MIDELEG : 0x00000222
MEDELEG : 0x0000b109
PMP0    : 0x80000000-0x8001ffff (A)
PMP1    : 0x00000000-0xffffffff (A,R,W,X)
[    0.000000] Linux version 5.10.0 (ken@ken-Inspiron-7370) (riscv32-unknown-linux-gnu-gcc () 12.2.0, GNU ld (GNU Binutils) 2.40.0.20230214) #18 SMP Thu Apr 20 11:40:18 JST 2023
[    0.000000] OF: fdt: Ignoring memory range 0x80000000 - 0x80400000
[    0.000000] earlycon: sbi0 at I/O port 0x0 (options '')
[    0.000000] printk: bootconsole [sbi0] enabled
[    0.000000] printk: debug: skip boot console de-registration.
[    0.000000] Zone ranges:
[    0.000000]   Normal   [mem 0x0000000080400000-0x0000000087ffffff]
[    0.000000] Movable zone start for each node
[    0.000000] Early memory node ranges
[    0.000000]   node   0: [mem 0x0000000080400000-0x0000000087ffffff]
[    0.000000] Initmem setup node 0 [mem 0x0000000080400000-0x0000000087ffffff]
[    0.000000] SBI specification v0.2 detected
[    0.000000] SBI implementation ID=0x1 Version=0x8
[    0.000000] SBI v0.2 TIME extension detected
[    0.000000] SBI v0.2 IPI extension detected
[    0.000000] SBI v0.2 RFENCE extension detected
[    0.000000] SBI v0.2 HSM extension detected
[    0.000000] riscv: ISA extensions acdfimsu
[    0.000000] riscv: ELF capabilities acdfim
[    0.000000] percpu: Embedded 13 pages/cpu s20620 r8192 d24436 u53248
[    0.000000] Built 1 zonelists, mobility grouping on.  Total pages: 31496
[    0.000000] Kernel command line: console=ttyS0 earlycon=sbi keep_bootcon bootmem_debug
[    0.000000] Dentry cache hash table entries: 16384 (order: 4, 65536 bytes, linear)
[    0.000000] Inode-cache hash table entries: 8192 (order: 3, 32768 bytes, linear)
[    0.000000] Sorting __ex_table...
[    0.000000] mem auto-init: stack:off, heap alloc:off, heap free:off
[    0.000000] Memory: 104552K/126976K available (4268K kernel code, 6352K rwdata, 4096K rodata, 172K init, 209K bss, 22424K reserved, 0K cma-reserved)
[    0.000000] Virtual kernel memory layout:
[    0.000000]       fixmap : 0x9dc00000 - 0x9e000000   (4096 kB)
[    0.000000]       pci io : 0x9e000000 - 0x9f000000   (  16 MB)
[    0.000000]      vmemmap : 0x9f000000 - 0x9fffffff   (  15 MB)
[    0.000000]      vmalloc : 0xa0000000 - 0xbfffffff   ( 511 MB)
[    0.000000]       lowmem : 0xc0000000 - 0xc7c00000   ( 124 MB)
[    0.000000] SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
[    0.000000] rcu: Hierarchical RCU implementation.
[    0.000000] rcu:     RCU restricting CPUs from NR_CPUS=8 to nr_cpu_ids=1.
[    0.000000] rcu:     RCU debug extended QS entry/exit.
[    0.000000]     Tracing variant of Tasks RCU enabled.
[    0.000000] rcu: RCU calculated value of scheduler-enlistment delay is 25 jiffies.
[    0.000000] rcu: Adjusting geometry for rcu_fanout_leaf=16, nr_cpu_ids=1
[    0.000000] NR_IRQS: 64, nr_irqs: 64, preallocated irqs: 0
[    0.000000] riscv-intc: 32 local interrupts mapped
[    0.000000] plic: plic@c000000: mapped 53 interrupts with 1 handlers for 2 contexts.
[    0.000000] random: get_random_bytes called from start_kernel+0x384/0x518 with crng_init=0
[    0.000000] riscv_timer_init_dt: Registering clocksource cpuid [0] hartid [0]
[    0.000000] clocksource: riscv_clocksource: mask: 0xffffffffffffffff max_cycles: 0x24e6a1710, max_idle_ns: 440795202120 ns
[    0.000194] sched_clock: 64 bits at 10MHz, resolution 100ns, wraps every 4398046511100ns
[    0.003556] Console: colour dummy device 80x25
[    0.007976] Calibrating delay loop (skipped), value calculated using timer frequency.. 20.00 BogoMIPS (lpj=40000)
[    0.008501] pid_max: default: 32768 minimum: 301
[    0.009673] Mount-cache hash table entries: 1024 (order: 0, 4096 bytes, linear)
[    0.010008] Mountpoint-cache hash table entries: 1024 (order: 0, 4096 bytes, linear)
[    0.032979] rcu: Hierarchical SRCU implementation.
[    0.036166] smp: Bringing up secondary CPUs ...
[    0.036460] smp: Brought up 1 node, 1 CPU
[    0.043441] devtmpfs: initialized
[    0.048204] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645041785100000 ns
[    0.048752] futex hash table entries: 256 (order: 2, 16384 bytes, linear)
[    0.087730] SCSI subsystem initialized
[    0.089267] clocksource: Switched to clocksource riscv_clocksource
[    0.160975] workingset: timestamp_bits=30 max_order=15 bucket_order=0
[    0.169413] Block layer SCSI generic (bsg) driver version 0.4 loaded (major 253)
[    0.169821] io scheduler mq-deadline registered
[    0.170072] io scheduler kyber registered
[    0.236230] Serial: 8250/16550 driver, 4 ports, IRQ sharing disabled
[    0.241776] printk: console [ttyS0] disabled
[    0.242693] 10000000.uart: ttyS0 at MMIO 0x10000000 (irq = 2, base_baud = 230400) is a 16550A
[    0.243971] printk: console [ttyS0] enabled
[    0.243971] printk: console [ttyS0] enabled
[    0.260835] loop: module loaded
[    0.260835] loop: module loaded
[    0.262596] mousedev: PS/2 mouse device common for all mice
[    0.262596] mousedev: PS/2 mouse device common for all mice
[    0.267160] goldfish_rtc 101000.rtc: registered as rtc0
[    0.267160] goldfish_rtc 101000.rtc: registered as rtc0
[    0.268848] goldfish_rtc 101000.rtc: setting system clock to 2023-04-20T02:40:37 UTC (1681958437)
[    0.268848] goldfish_rtc 101000.rtc: setting system clock to 2023-04-20T02:40:37 UTC (1681958437)
[    0.272783] syscon-poweroff soc:poweroff: pm_power_off already claimed (ptrval) sbi_shutdown
[    0.272783] syscon-poweroff soc:poweroff: pm_power_off already claimed (ptrval) sbi_shutdown
[    0.273986] syscon-poweroff: probe of soc:poweroff failed with error -16
[    0.273986] syscon-poweroff: probe of soc:poweroff failed with error -16
[    0.274954] debug_vm_pgtable: [debug_vm_pgtable         ]: Validating architecture page table helpers
[    0.274954] debug_vm_pgtable: [debug_vm_pgtable         ]: Validating architecture page table helpers
[    0.308213] Freeing unused kernel memory: 172K
[    0.308213] Freeing unused kernel memory: 172K
[    0.310379] Run /init as init process
[    0.310379] Run /init as init process
/ # ls
bin                init               proc               usr
dev                initramfs.cpio.gz  sbin
etc                linuxrc            sys
home               mnt                tmp
/ #

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です