Help us understand the problem. What is going on with this article?

QEMU virt向けRISC-V (RV32) バイナリを生成する

概要

RISC-V RV32ICの実行バイナリを出力してエミュレーション実行するために、クロスコンパイル用GCCとQEMUをビルドし、サンプルプログラムを実行します。

背景

学習としてRISC-VエミュレータをC言語で書いていますが、まず実行バイナリと正常な実行環境が用意できないと挙動の検証がつらいので、その環境整備のログです。
クロスコンパイル用GCCとQEMUをビルドし、QEMUのvirt環境で実行できるバイナリを生成します。

筆者は、ハードウェアとかバイナリとか好きだけど詳しくないので、手探り状態です。

実施環境

ホスト側: Ubuntu Linux 18.04 (x86_64) on WSL
マシンは Windows 10 Pro, Core i5 8250U (4C8T), 16GB 。
POSIX的な環境ならだいたい大丈夫だと思います。WSLはディスクIOが遅いらしいのでおすすめしませんが。
ツールは /opt/riscv に展開することにします。

ターゲット: RISC-V RV32IC
RV32I == 32bitレジスタ/メモリ空間の基本命令セット
C == 圧縮命令セット(RISC-Vの基本命令は32bit長、C拡張では16bit長命令が追加される)
今回の記事ではほとんど関係ないですが、マイクロコントローラーくらいのデバイスを想定しています。エミュレータ書いたら次はVerilogでFPGAで実装したい。

ツールの準備

クロスコンパイルするGCCのビルド

Ubuntu Linux 18.04 (x86_64)のパッケージマネージャでは、RV64のGCCしかインストールできないようなので、GCCは自前でビルドです。所要時間はだいたい1時間くらい。
RISC-V本家でビルドのためのレポジトリを用意してくれているので、それを利用します。
(検証リビジョン: 2020-01-28 b426ddd55c1e8cfc6ed991e8cdeca873f5ab2c17 )

git clone --depth=1 --recursive https://github.com/riscv/riscv-gnu-toolchain
./configure --prefix=/opt/riscv --with-arch=rv32g --with-abi=ilp32
make -j4

※補足
configureのprefixには空白文字が使えないので注意(引用符で囲んで指定しても、生成されるMakefileがおかしくなる)。
makeは、ビルドしたうえで make install相当まで実行するので、実行時の権限に注意。手軽に対応するなら、/opt/riscvディレクトリの権限を緩くするのが楽か。

QEMUのビルド

あんまり検証していないのですが、もしかしたらパッケージマネージャでインストールすれば十分かもしれません。
ビルドする場合は、現在はQEMU本家でRISC-V対応しているので、本家のコードをビルドします。所要時間はだいたい30分くらいだった気がする。

wget https://download.qemu.org/qemu-4.2.0.tar.xz
tar xvf ./qemu-4.2.0.tar.xz
cd qemu-4.2.0
./configure --prefix=/opt/riscv --target-list=riscv32-softmmu
make -j4
make install

実行バイナリを生成する

QEMU virtで動かす場合の条件

  • メモリの0x80000000番地から実行される。(0x1000番地にいるローダーからジャンプしてくる。)
  • ELFのエントリアドレスは無視されるようなので(無条件に0x80000000番地にジャンプする)、実行バイナリ内で最初に実行されるべき関数は先頭に配置されるようにする。
  • 0x10000000番地に書き込むと、シリアル通信として出力される。(QEMUに与えるオプションで、これを標準出力にリダイレクトする。)
  • 0x100000番地に0x5555の2バイトを書き込むと、シャットダウンする。

参考: qemu/hw/riscv/virt.c

サンプルプログラム

以下の2ファイルを用意する。

hello.c
volatile char* UART0_ADDR = (char*)0x10000000;
volatile short* VIRT_TEST_ADDR = (short*)0x100000;

void _start(void) __attribute__((section(".text.startup")));

void _start(void) {
  *UART0_ADDR = '!';
  *UART0_ADDR = '\n';
  *VIRT_TEST_ADDR = 0x5555;
}
link.lds
OUTPUT_ARCH("riscv")
ENTRY(_start)

SECTIONS
{
    . = 0x80000000;
    .text.startup : { *(.text.startup) }
    .text   : { *(.text) }
    .rodata : { *(.rodata) }
    .data   : { *(.data) }
    .bss    : { *.(.bss) }
    . = ALIGN(8);
    . = . + 0x4000;
    sp_top = .;
}

コンパイルと実行。

riscv32-unknown-elf-gcc -march=rv32ic -mabi=ilp32 -nostartfiles -Tlink.lds -Wall -O2 test.c -o test.elf
qemu-system-riscv32 -nographic -M virt -m 4096 -serial mon:stdio -bios none -kernel test.elf

中身を探索する

逆アセンブルする場合は、

riscv32-unknown-elf-objdump -D test.elf

エントリアドレスなどELFのヘッダー情報を見る場合は、

riscv32-unknown-elf-readelf -a test.elf

補足: QEMUトラブル対応

途中で実行を終了させる:
Ctrl-a x
なんかわかんないけどコンソールに反応がない:
-d in_asm,int -D log.txtを付加してqemuを実行。数秒したら停止し、ログを眺める。

今後の課題

  • リンカスクリプトに過不足はないか。
  • GCCで-nostartfilesしながらも、自前で初期化みたいなことをしていないのだが、それでよいのか。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした