皆様こんにちは.18のhiraです.
僕はコンピュータ工学をやっているつもりで,最近は作成したプロセッサでLinuxを動かそうとしているらしいです.
そのため対称実験用に正しく動くものが欲しいと思い,作っているプロセッサと同じ32bit RISC-VでLinuxを動かす環境を探していました.そこでプロセッサエミュレータであるQEMUでLinuxを動かしました.
その際,64bit向けはあっても意外に32bit向けの構築方法のまとめは少なかったため,32bit環境でLinuxをソースコードからRISC-V向けにビルドしたり,それをQEMUで実行する流れをまとめました.
やっている内容はほぼリンクのブログ(1)とリンクのブログ(2)の丸パクリなのですが,一部変える点があったり,補足したい点があったため記録します.
注: もし何も表示されないなどのエラーが出た場合,面倒ですが一度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」などの実行ファイルができているはずです.
これらが生成されていれば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」などが生成されているはずです.
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項目を変更します:
- 「Base ISA」を「RV32I」にする
- 「Emit compressed~」のチェックを外して無効化する(先程のUEFI~を先に無効化する必要あり)
- 「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を動かす上で使用するのが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」にチェックを入れます.
変更したら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
やっていることとしては,
- ddコマンドでゼロの羅列をコピーして1MBサイズのファイルbusybox.binを作製する
- mkfs.ext2コマンドでbusybox.binをext2形式のファイル構造にフォーマットする
- mountコマンドでmntディレクトリにbusybox.binのファイル構造を展開する
- 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」と入力すると,正しくフォルダ内が表示されています.
この状態ではまだLinuxにBusyBoxがインストールされてはいないため,QEMU上で下記のコマンドを入力してインストールします.
/bin/busybox --install -s
mount -t proc proc /proc
これを行うことで,いちいち/bin/busyboxなど指定しなくても,unameコマンドやlsコマンドが使えるようになります.
補足: 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 / #