みねっちょのマイコン関係ブログ

組込開発系フリーソフトやハードの情報発信ブログ

WSL 標準パッケージで RISC-V 32ビット版 RV32E のコンパイルをする

サイト内 Google 検索:


最終更新: 2021-06-09
Windows10 上で WSL (Windows Subsystem for Linux) にインストールされた Ubuntu-20.04 の標準パッケージのクロスコンパイラを使って、RISC-V 32ビット組み込み用途版 RV32E の、OS 無しベアメタルなバイナリを生成する方法を紹介します。

尚、一部の項目は別途記載したARM用ベアメタルの【こちらの記事】と被って居ます。

前提条件:

  • Windows 10 を使用していること
  • Microsoft Store から WSL の Ubuntu-20.04 をインストール済なこと

パッケージのインストール:

sudo apt install crossbuild-essential-riscv64

パッケージ名としては RISCV64 なのですが、実はオプションを付ければ32ビット版のコンパイルも出来ますので、これをインストールします。コマンド群の名前は「riscv64-linux-gnu-*」(* は gcc, as, ld, objdump, objcopy 等) となります。これをインストールすると、ubuntu の「/usr/lib/riscv64-linux-gnu/ldscripts」ディレクトリの下に 32ビット用と64ビット用の両方のリンカスクリプトがインストールされまます。

また、ライブラリ群は「 /usr/lib/gcc-cross/riscv64-linux-gnu」にインストールされますが、こちらは64ビット用 のみの様です。ベアメタル用の32ビット ライブラリを使用したい場合には、newlib 等を別途、野良ビルドする必用が有りそうです。

尚、その辺りの話は、こちらの RISC-V の公式 git レポジトリの頁の下の方に記載されています。

github.com

ベアメタル用スタートアップルーチンの作成:

一般論として、通常のプロセッサで C 言語のスタートアップルーチン (いわゆる、crt0) として必要な最小限の事は、次の2点です。正式な C 言語の仕様には準拠しませんが、他は、やりたい事が増えた時に拡張して行く事にします。
C 言語の仕様として、スタティック変数のゼロ初期化、グローバル変数の使用、配列の初期化に対応していません。また、プロセッサの割り込みに対応していません。

  1. スタックポインタの番地設定
  2. C 言語のメインルーチンのコール

次のコマンドを WSL のターミナル上にコピー&ペーストすると、start.s というファイルが出来ます。適当なディレクトリを作って、その下で実行してください。尚、下に記載されているのは疑似的なアセンブラなので機械語と1対1ではなく、最終的なバイナリになる前にアセンブラとリンカが勝手に書き換えます。

cat <<! > tmp.txt
.equ    STACK_TOP, 0x04000000

        .global  _start
        .text

### Reset Code ###
        .align 2
_start:
        nop      /* This NOP is just for GDB simulator */
        li sp,   STACK_TOP
        call     main
        
### Loop to the Beginning ###
        j       _start /* Infinite loop */

### Padding for Linking Codes ###
        .align 3        
!
/usr/bin/unexpand -t8 tmp.txt > start.s
/usr/bin/rm tmp.txt

疑似的なアセンブラなので、上に記載した動作を各1行で記述出来、非常にスッキリしています。尚、先頭の NOP 命令は、GDB (The GNU Project Debugger) に内蔵されたシミュレータを動かす為に記載しています。

動作確認用 C 言語ソースコードの作成:

次のコマンドを WSL のターミナル上にコピー&ペーストすると、main.c というファイルが出来ます。

cat <<! > main.c
int iadd(int a, int b);

int main() {
  volatile int a, b, c;
  a=0xffff0000;
  b=4;
  c=iadd(a, b);
  return c;
}

int iadd(int a, int b){
  return (a+b);
}
!

変数宣言の最初に volatile (揮発性) を付けて居るのは、コンパイラが最適化で勝手に変数を除去しない様にする為です。volatile を付けておくと、コンパイラはこの変数は揮発性指示なので、最適化はせずに毎回必ず値を代入せねばならないと認識します。このソースコードの目的は、RISC-V のコンパイラがどの様なソースコードを生成するのかを見る為であって、答えが 0xffff0004 である事を知りたい訳では無いという事です。関数呼び出しをしているのも、コーリングコンベンション (関数呼び出しの規約。ABI とも言います) がどの様な物かを見る為です。

オブジェクトファイルの生成:

スタートアップルーチンとして上記で作成した専用の start.s を使用する為に、アセンブルコンパイル、リンクは夫々別手順で行います。

RV32E 用のビルドを行う為には、アセンブルコンパイル時に、CPU アーキテクチャとして rv32e を指定し、また同時にコーリングコンベンション (関数呼び出し規約、ABI) として ilp32e を指定します。リンク時にはエミュレーションモードとして elf32lriscv_ilp32 を指定します。

但し、CPU アーキテクチャに rv32e を指定しても、対応する C 言語ライブラリや Math ライブラリは、Ubuntuの標準には有りません。有効になるのはコーリングコンベンションだけです。とは言え、libc.a や libm.a を使用しないプログラムであれば問題は有りません。

標準ライブラリのスタートアップ ルーチンの代わりに上記で作成した専用の start.o をリンクする為には、main.c のコンパイル時に -c オプションを指定して、一旦オブジェクトの main.o を生成します。その後、start.o と main.o をリンクして実行形式の ELF ファイルを生成します。start.o と main.o はリンカが指定した順番通りに配置しますので、順番は大事です。また、OS 無しベアメタルでのブートの為には、リンク時に .text セクションを 0 番地から開始するよう指定し、CPU のリセット時開始番地と合わせます。

riscv64-linux-gnu-as  -g -gdwarf-2     -march=rv32e -mabi=ilp32e    -o start.o start.s
riscv64-linux-gnu-gcc -g -gdwarf-2 -Og -march=rv32e -mabi=ilp32e -c -o main.o  main.c
riscv64-linux-gnu-ld -Bstatic -Ttext 0x0 -melf32lriscv_ilp32 start.o main.o -o main.out

コンパイラでサポートされている ABI は riscv64-linux-gnu-gcc --help=target 、リンカでサポートされているエミュレーションモードは iscv64-linux-gnu-gcc -mhoge とすると分かります。

生成されたファイルの確認と、ヘキサや Verilog 用出力:

次のコマンドで、生成された実行形式ファイル (ELF) やデバッグ情報 DWARF の確認と、実行形式ファイルから逆アセンブルしたリストの出力、及び、インテル (Intellec) 形式ヘキサ、モトローラ S レコード形式ヘキサ、Verilog 用 メモリファイルの生成を行います。

file main.out
riscv64-linux-gnu-objdump main.out --dwarf=info | head -10
riscv64-linux-gnu-objdump main.out --disassemble-all --source --section=.text --section=.rodata --section=.bss --section=.data > main.txt

riscv64-linux-gnu-objcopy main.out -O ihex main.ihex --only-section=.text --only-section=.rodata
riscv64-linux-gnu-objcopy main.out -O srec main.srec --only-section=.text --only-section=.rodata
riscv64-linux-gnu-objcopy main.out -O verilog main.verilog --only-section=.text --only-section=.rodata

riscv64-linux-gnu-objcopy main.out -O binary main.binary --only-section=.text --only-section=.rodata
od -A d -t u4 -v main.binary | \
awk '{printf("@%06x %08x %08x %08x %08x\n", $1/4, $2, $3, $4, $5)}' > main.vhex

 file コマンドで「ELF 32-bit LSB relocatable, UCB RISC-V, version 1 (SYSV), with debug_info, not stripped」と出力されますので、32ビットの RISC-V 用実行形式ファイルである事が分かります。

2行目の objdump コマンドで、デバッグ情報 DWARF のバージョンが2である事を確認します。DWARF2 は、各種 ICE (In-CIrcuit Emulaor) やデバッガに読み込み可能な形式です。高級言語でのソースコードデバッグに必要となります。

3行目の objdump コマンドで、ELF 実行形式ファイルをディスアセンブルして、かつ、C のソースコードを間に挟み込んで居ます。

4行目から6行目の objcopy コマンドで、ELF 実行形式ファイルからインテルヘキサ、モトローラヘキサ、Verilogのメモリファイルの、各アスキー形式で人間が可読なファイルに変換しています。

7行目と8行目は、ELF 実行形式ファイルからベアメタル用の機械語だけのバイナリに変換し、それを UNIX の od (Octal Dump) コマンドでアスキー形式にダンプ後、Verilog にも読み込み可能なヘキサ形式に変換して居ます。
2021-03-29 訂正:
32ビット ROM 用にダンプする場合、アドレスを4で割らなければなりません。更に、od コマンドによる連続値の省略で「*」が出力されないよう「-v」オプションが必要です。


【RISC-V 関係の目次へ戻る】   【WSL 関係の目次へ戻る】