※本記事はInterface誌2022年12 月号に掲載されたものの原稿版になります
第4回まではハードウェアのアーキテクチャ、設計の話が続きました。第5回は少し視点を変えてハードウェアを動かすために欠かせない、ソフトウェアの開発ツールの話をしたいと思います。
新しいCPUを動かすために必須の開発ツール
ソフトウェアを新しいCPU上で動作させるには、そのCPU用の命令列を生成する必要があります。命令列はバイナリエディタでも書けますが、大規模なソフトウェアになると扱いきれません。通常はクロスコンパイルといって、PCなど既存のアーキテクチャ上でソフトウェアを別のCPU向けの命令列に変換する作業を行います。
図1: コンパイルとクロスコンパイルの違い。
コンパイラ(クロスコンパイラと区別する文脈ではセルフコンパイラと呼ぶこともあります)はPC上で実行し、PC用の命令列を生成します。クロスコンパイラはPC上で実行し、新CPU用の命令列を生成します。
コンパイラを含むコンパイルに使用するツール群を開発ツールまたはツールチェーンと呼びます。例えばGNUの開発ツールであれば、
- GNU binutils: アセンブラ、リンカー
- GDB: デバッガー
- GCC: コンパイラ
- 標準Cライブラリ(glibcを始めとして他ライブラリも使用可能)
からなります。クロスコンパイル用であることを明示したいとき、ツール名の頭にクロスとつけることもあります。
開発ツールから見たRISC-Vの利点
CPU開発エンジニアにとって開発ツールの用意は悩み所でした。大企業ならまだしも個人や小規模開発で開発ツール、特にコンパイラのような複雑なソフトウェアをゼロから作成するのは大変だからです。
その点RISC-Vは著名なオープンソース(以降OSS)コンパイラにて既に対応済みであり、商用コンパイラも対応を発表しています。この動きは今後も広がると予想されます。
- OSS
- GCC: https://gcc.gnu.org/
- LLVM: https://llvm.org/
- 商用
- IAR Embedded Workbench
自身で開発ツールをゼロから作成する必要のないRISC-VはCPU開発者にとって大きな利点と言えるでしょう。
クロス開発ツール構築の仕組み
GNU開発ツールを構成するモジュールは4つあります。GCCは内部にいくつかライブラリを内包しています。GDBはデバッグ時には必要ですが、クロスコンパイルそのものには必要ありませんので、今回の説明からは割愛します。
- binutils
- GCC
- libgcc: 32bitプロセッサでの64bit演算、FPUのないプロセッサでの浮動小数点演算などのエミュレーションを行うライブラリ。
- libstdc++: 標準C++ライブラリ。
- 標準Cライブラリ(今回はnewlibを使います)
- GDB(今回の説明からは割愛)
これら3つのモジュールに対し、クロス開発ツールを構築する手順は4つ必要です。
- binutils: クロスアセンブラ、クロスリンカーを構築
- GCC stage1: 標準Cライブラリを生成するため、機能が限定されたクロスコンパイラを構築
- newlib: 標準Cライブラリを構築
- GCC stage 2: 標準Cライブラリを含めたC言語機能を利用可能なクロスコンパイラを構築
GCCは1つにも関わらずStage1とStage2という2つのクロスコンパイラが出現します。理由はクロスコンパイラと依存ライブラリが持つ循環した依存関係を解消する必要があるためです。標準C++ライブラリであるlibstdc++を例にとると図2に示した依存関係があり、最初から全機能を有効にしたクロスコンパイラは構築できません。問題を解消するため図3に示した手順で、最初に依存関係のない機能を限定したStage1コンパイラを構築、依存ライブラリを構築、最後に全機能が有効なStage2コンパイラを構築します。
コンパイラだけでなく、アセンブラやリンカーも含めた依存関係の概要を図4と図5に示します。
図2: GCCと依存ライブラリの依存関係。
GCCはlibstdc++がないと動かず、libstdc++はnewlibがないと構築できず、newlibはGCCがないと構築できません。どのモジュールからビルドしようとしても依存関係が解消できません。
図3: GCCの構築手順。
(1)初めに依存関係がなく機能も限定されたStage1コンパイラを構築します。(2)(3)newlibやlibstdc++のような依存ライブラリを構築します。(4)全機能を有効にしたStage2コンパイラを構築します。
図4: アセンブラ、リンカーも含めた依存関係の概要。
図3の手順(1)(2)に相当します。クロスアセンブラ、リンカー、コンパイラはクロスコンパイルを行う環境(通常はPCだと思います)で構築します。newlibはクロスコンパイラStage1とクロスアセンブラで構築します。グレーは新CPU用の命令列からなるバイナリを意味します。
図5: アセンブラ、リンカーも含めた依存関係の概要。
図3の手順(3)(4)に相当します。libgccは図3にありませんがGCC Stage2を構築時にともに構築されます。
図6: C++アプリケーション構築の際の依存関係の概要。
アプリケーションのオブジェクトと、手順(3)(4)でビルドしたライブラリをリンクして実行ファイルを生成します。
OSS開発ツールを改造
OSSの特徴として改造して独自のRISC-V開発ツールを作成することができる点が挙げられます。改造の内容によって難易度は変わりますが、比較的簡単な改造例としてベクトル拡張に対応した開発ツールを構築する手順を紹介します。
使用するのはGNU開発ツールです。LLVMは既にベクトル拡張に対応していますし、GNU開発ツールを自動的に構築する便利なツールもあります。今回はそれらに頼らず1つずつ手順を追って構築します。
binutilsとGCCはベクトル命令の対応がまだ本家リポジトリに導入されていません(もしくは導入途中で機能的に合わない)ので、RISC-V開発チームによるriscv-binutils-gdb、riscv-gccリポジトリを使用します。newlibは本家リポジトリをそのまま使います。
- binutils: https://github.com/riscv-collab/riscv-binutils-gdb/
- GCC: https://github.com/riscv-collab/riscv-gcc/
- riscv-gcc-nextブランチ
環境はUbuntu 20.04 LTSで確認しています。WORKとPREFIXのディレクトリはホームディレクトリ直下にしていますが、好きな場所に変更してください。
export WORK=~/work export PREFIX=~/riscv export PATH=$PATH:/${PREFIX}/bin mkdir -p ${WORK} mkdir -p ${PREFIX} # Install required modules apt-get update && apt-get -y dist-upgrade apt-get install -y git build-essential gettext \ flex bison texinfo zlib1g-dev python3-dev # Clone source code repositories cd ${WORK} git clone https://github.com/riscv-collab/riscv-binutils-gdb cd ${WORK}/riscv-binutils-gdb cd ${WORK} git clone https://github.com/riscv-collab/riscv-gcc cd ${WORK}/riscv-gcc git checkout -b riscv-gcc-rvv-next origin/riscv-gcc-rvv-next ./contrib/download_prerequisites cd ${WORK} git clone https://cygwin.com/git/newlib-cygwin.git mkdir -p ${WORK}/test cd ${WORK}/test curl -L https://raw.githubusercontent.com/riscv-non-isa/rvv-intrinsic-doc/master/examples/rvv_sgemm.c > rvv_sgemm.c # binutils cd ${WORK}/riscv-binutils-gdb rm -rf build mkdir -p build cd build ../configure \ --target=riscv64-unknown-elf \ --prefix=${PREFIX}/ \ --with-expat=yes \ --without-intel_pt \ --disable-werror \ --disable-gdb \ --disable-shared \ --enable-sim \ --enable-libdecnumber \ --enable-libreadline \ --enable-gas \ --enable-binutils \ --enable-ld \ --enable-gold \ --enable-gprof make -j16 && make install # GCC Stage 1 cd ${WORK}/riscv-gcc rm -rf build_stage1 mkdir -p build_stage1 cd build_stage1 ../configure \ --target=riscv64-unknown-elf \ --prefix=${PREFIX}/ \ --with-sysroot=${PREFIX}/riscv64-unknown-elf \ --with-newlib \ --with-native-system-header-dir=/include \ --with-system-zlib \ --disable-shared \ --disable-threads \ --disable-libmudflap \ --disable-libssp \ --disable-libquadmath \ --disable-libgomp \ --disable-nls \ --enable-tls \ --enable-multilib \ --enable-languages=c \ --enable-checking=yes \ --with-abi=lp64d \ --with-arch=rv64gc \ CFLAGS_FOR_TARGET="-O2 -mcmodel=medany" \ CXXFLAGS_FOR_TARGET="-O2 -mcmodel=medany" make -j16 all-gcc && make install-gcc # newlib cd ${WORK}/newlib-cygwin rm -rf build mkdir -p build cd build ../configure \ --target=riscv64-unknown-elf \ --prefix=${PREFIX}/ \ --enable-newlib-io-long-double \ --enable-newlib-io-long-long \ --enable-newlib-io-c99-formats \ CFLAGS_FOR_TARGET="-O2 -mcmodel=medany" \ CXXFLAGS_FOR_TARGET="-O2 -mcmodel=medany" make -j16 && make install # GCC Stage 2 cd ${WORK}/riscv-gcc rm -rf build_stage2 mkdir -p build_stage2 cd build_stage2 ../configure \ --target=riscv64-unknown-elf \ --prefix=${PREFIX}/ \ --with-sysroot=${PREFIX}/riscv64-unknown-elf \ --with-newlib \ --with-native-system-header-dir=/include \ --with-system-zlib \ --disable-shared \ --disable-threads \ --disable-libmudflap \ --disable-libssp \ --disable-libquadmath \ --disable-libgomp \ --disable-nls \ --enable-tls \ --enable-multilib \ --enable-languages=c,c++ \ --enable-checking=yes \ --with-abi=lp64d \ --with-arch=rv64gc \ CFLAGS_FOR_TARGET="-O2 -mcmodel=medany" \ CXXFLAGS_FOR_TARGET="-O2 -mcmodel=medany" make -j16 && make install
RISC-Vベクトル拡張を使用したプログラムは下記を用います。ソースコード中にはvsetvl_e32m1()のような名前の関数がいくつか確認できると思います。ベクトル拡張を使用するintrinsics関数と呼ばれるコンパイラ組み込み関数です。
https://github.com/riscv-non-isa/rvv-intrinsic-doc/blob/master/examples/rvv_sgemm.c
$ cd ${WORK}/test $ riscv64-unknown-elf-gcc -march=rv64gcv -g rvv_sgemm.c
コンパイル出来たら逆アセンブルしてみましょう。
$ riscv64-unknown-elf-objdump -drS a.out
(出力結果の一部)
vl = vsetvl_e32m1(c_n_count ); 10390: c22027f3 csrr a5,vlenb 10394: 40f007b3 neg a5,a5 10398: fc078793 addi a5,a5,-64 1039c: 17c1 addi a5,a5,-16 1039e: 97a2 add a5,a5,s0 103a0: fd043703 ld a4,-48(s0) 103a4: 05077757 vsetvli a4,a4,e32,m1,ta,mu 103a8: e398 sd a4,0(a5)
RISC-Vベクトル拡張命令vsetvliが出力されていることがわかります。riscv-gcc-nextブランチは開発中のため異常な動作(明らかに不要な命令列が出力される、コンパイラがエラーで落ちるなど)をすることがありますので、ご注意ください。
おわりに
普段のソフトウェア開発ではあまり意識されることのない開発ツールについて、簡単ではありますが構築方法や簡易的な改造についてご紹介しました。開発ツールの仕組みやハードウェアとソフトウェアの橋渡しという役割を理解する一助となれば幸いです。
****
すずき かつひろ
****