2017年には新しい命令セットアーキテクチャである「RISC-V」の話題が盛り上がりました。今回はUbuntu上でRISC-V用のツールチェインをエミュレーターを導入し、実行してみましょう。
はじめに
RISC-V(りすく・ふぁいぶ)はRISC-V Foundationが開発を主導している新しい命令セットアーキテクチャ(ISA:Instruction Set Architecture)です。
ISAとは、コンピューター・アーキテクチャに対するソフトウェア側のインターフェースを定義したもので、要するに「こういう命令を送れば、このコンピューターはこう動きます」という決まり事をひとつにまとめた規格です。ISAにはたとえばAMD/Intel系の64bit CPUで使われているAMD64/Intel 64や、スマートフォンで一般的なARMv7/ARMv8などが存在します[1] 。任意のプログラムを実行できるコンピューターは、何らかのISAを持っており、ソースコードはそのISAに基づいて実行可能なバイナリデータへと変換されます。このため同じソースコードであっても、ARMv8向けに作られた実行可能なバイナリデータを、AMD64上でそのまま実行することはできません。
RISC-Vは2010年頃からカリフォルニア大学バークレー校で設計が始まり、現在ではRISC-V Foundationの元にGoogleやNVIDIAといった名だたる企業が参加している新進気鋭のISAです。単に新しいというだけでなく、70を超える企業・団体が参画している点も注目すべき特徴と言えるでしょう。
RISC-Vが注目されている理由のひとつが、オープン規格であるということです。一般的にISAやその策定作業は開発する企業(チップベンダー)の特許やらノウハウやら機密の塊です。少なくともISAの新しいバージョンを、どのタイミングでどのようにリリースするかは開発する企業が決めますし、リリースされたISAに従ったCPUを作るためには相応のライセンス料を支払う必要があります。ベンダーが膨大な開発費をかけて作ったものであるから、当然と言えば当然の話です。
それに対してRISC-V Foundationは複数の企業・団体・個人に開かれた非営利団体であり、RISC-V Foundationの成果物は原則として公開されています 。たとえばISAマニュアル はCreative Commons BY 4.0で提供されていますし、RISC-Vベースの製品を展開しているSiFiveもRTL(Register
Transfer Level)から開発ツールに至るまで主にApache License 2.0のもとGitHub上で公開しています 。ソフトウェアで行われたFLOSSのムーブメントをハードウェアでも実現しようとしているわけです。
RISC-V用のツールチェインについて
残念ながらRISC-Vは、UbuntuはもとよりDebianでもサポートアーキテクチャになっていません。よってRISC-Vのバイナリを作成するためには、GCCやbinutilsといったツールチェインから自分で構築する必要があります。GCCなどにRISC-Vのサポートが取り込まれたのもごく最近なので、当面はこの状態が続くでしょう。
特定のアーキテクチャの実機が存在しない場合や存在したとしてもコンパイルするのにリソースが足りない場合は、より高速でリソースも十分にあるホストマシン上でターゲットアーキテクチャのバイナリを「クロスコンパイル」することが一般的です。おそらく大半の読者はRISC-Vの実機を持っていないと思われますので、今回もRISC-Vバイナリの構築は、amd64アーキテクチャのホスト上でビルドすることにしましょう。
幸い「riscv-tools 」と呼ばれるツールチェインを一括してビルドするスクリプトが用意されているので、十分に高速でビルド時によくわからないSEGVを起こさないようなマシンがあれば、簡単にクロスコンパイル環境を構築できます。
もちろんツールチェインをビルドするためのツールチェインが必要になりますが、これはホストマシンにパッケージ管理システムからインストールできるGCCを利用します。riscv-toolsの構築にはGCC-4.8以上が必要とのことなので、Ubuntu 14.04 LTS以上であれば特に問題ないでしょう。今回はすべてUbuntu 16.04 LTS上のGCC 5.3で動作を確認しています。
LXDを使っているなら、LXD上に環境を構築すると良いでしょう。riscv-toolsはさまざまな外部リポジトリをgit submoduleを使って取り込んでいます。これらのリポジトリを一通りビルドするにはそれなりの時間が必要です。LXD上のクリーンな環境で構築した環境をコンテナイメージとしてアーカイブしておけば、他のマシンでも簡単に流用できます。もちろんDockerなどの他のコンテナ技術を使うのも一つの手です[2] 。
以下に、一般的なLXDのインストール手順を紹介しておきます。もちろんLXDを使わなくても問題はありません。
1. LXDのインストール
$ sudo snap install lxd
2. LXDの初期設定
$ sudo lxd init
Do you want to configure a new storage pool (yes/no) [default=yes]?
Name of the new storage pool [default=default]:
Name of the storage backend to use (dir, btrfs, ceph, lvm, zfs) [default=zfs]:
Create a new ZFS pool (yes/no) [default=yes]?
Would you like to use an existing block device (yes/no) [default=no]?
Size in GB of the new loop device (1GB minimum) [default=23GB]:
Would you like LXD to be available over the network (yes/no) [default=no]?
Would you like stale cached images to be updated automatically (yes/no) [default=yes]?
Would you like to create a new network bridge (yes/no) [default=yes]?
What should the new bridge be called [default=lxdbr0]?
What IPv4 address should be used (CIDR subnet notation, “auto” or “none”) [default=auto]?
What IPv6 address should be used (CIDR subnet notation, “auto” or “none”) [default=auto]?
LXD has been successfully configured.
3. ユーザーをLXDグループに追加
$ sudo adduser $USER lxd
4. コンテナイメージの作成
$ lxc launch ubuntu:16.04 riscv
$ lxc exec riscv bash
5. SSH公開鍵の登録
$ sudo -i -u ubuntu ssh-import-id gh:(GitHubのアカウント名)
$ apt update
$ apt full-upgrade -y
クロスコンパイル環境の構築
まずはRISC-Vのツールチェインをビルドするためのツールチェインを導入しましょう。
$ sudo apt install autoconf automake autotools-dev curl \
device-tree-compiler libmpc-dev libmpfr-dev libgmp-dev \
libusb-1.0-0-dev gawk build-essential bison flex texinfo \
gperf libtool patchutils bc zlib1g-dev pkg-config libncurses5-dev
次にriscv-toolsリポジトリとそのgit submoduleを導入します。submoduleのsubmoduleなどもダウンロードするため、それなりに時間がかかります。
$ git clone https://github.com/riscv/riscv-tools.git
$ cd riscv-tools
$ git submodule update --init --recursive
submoduleのダウンロード後にディレクトリのサイズを確認したところ、2.8GiB程度使用していました。
riscv-tools本体はただのビルドスクリプトで、ビルド対象はすべてsubmoduleとして外部参照しています。それぞれのツールは以下のとおりです。
ビルドしたツールは環境変数RISCVの先にインストールされるようになっています。そこで今回はホームディレクトリに任意のディレクトリを作って、環境変数をexportしておきましょう[3] 。
$ mkdir $HOME/riscv/
$ export RISCV=$HOME/riscv/
あとはビルドスクリプトを実行するだけです。
$ time ./build.sh
Starting RISC-V Toolchain build process
(中略)
RISC-V Toolchain installation completed!
real 77m30.212s
user 70m26.496s
sys 5m59.994s
IvyBridge世代でi5-3317UのCPUと4GiBのメモリーで、1時間ちょっとぐらいだったので今風のPCならもう少し早く終わるでしょう。また、build.sh
からはパラレルビルドする方法がわからなかったのですが、個別に「-j
」オプションを付けてmakeコマンドを実行すればさらに早くなるものと思います。
コンパイル後の$RISCVの下には400MiB弱のバイナリが用意されることになります。
ツールチェインのテスト
インストールしたツールチェインを使って、RISC-V用の実行バイナリを作ってみましょう。
$ mkdir ~/test && cd $_
$ export PATH=$PATH:$RISCV/bin
$ echo -e '#include <stdio.h>\n int main(void) { printf("Hello world!\\n"); return 0; }' > hello.c
$ riscv64-unknown-elf-gcc -o hello hello.c
$ file hello
hello: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV), statically linked, not stripped
riscv-toolsは特に指定せずにビルドした場合、標準Cライブラリの実装として一般的なLinuxディストリビューションで使われているglibcではなく、組み込みでよく使われているNewlib を静的にリンクします。
実行したバイナリはRISC-V ISAのシミュレーターであるspikeから立ち上げたpk(Proxy Kernel)越しに実行できます。
$ which spike
/home/ubuntu/riscv//bin/spike
$ find $RISCV -name pk
/home/ubuntu/riscv/riscv64-unknown-elf/bin/pk
$ spike pk hello
Hello world!
ちなみにspikeコマンドはデバッグ機能も備えていますので、たとえば「-d
」オプションを付けることでステップ実行もできますし、「 -l
」オプションを付けることで1命令ごとのログを残すことも可能です。
64bit向けNewlib用のツールチェインは「riscv64-unknown-elf-
」のプレフィックスが付きます。
$ riscv64-unknown-elf-
riscv64-unknown-elf-addr2line riscv64-unknown-elf-gcc riscv64-unknown-elf-gdb riscv64-unknown-elf-readelf
riscv64-unknown-elf-ar riscv64-unknown-elf-gcc-7.2.0 riscv64-unknown-elf-gprof riscv64-unknown-elf-run
riscv64-unknown-elf-as riscv64-unknown-elf-gcc-ar riscv64-unknown-elf-ld riscv64-unknown-elf-size
riscv64-unknown-elf-c++ riscv64-unknown-elf-gcc-nm riscv64-unknown-elf-ld.bfd riscv64-unknown-elf-strings
riscv64-unknown-elf-c++filt riscv64-unknown-elf-gcc-ranlib riscv64-unknown-elf-nm riscv64-unknown-elf-strip
riscv64-unknown-elf-cpp riscv64-unknown-elf-gcov riscv64-unknown-elf-objcopy
riscv64-unknown-elf-elfedit riscv64-unknown-elf-gcov-dump riscv64-unknown-elf-objdump
riscv64-unknown-elf-g++ riscv64-unknown-elf-gcov-tool riscv64-unknown-elf-ranlib
「$RISCV/riscv64-unknown-elf/share/riscv-tests/
」以下にはいくつかのベンチマークプログラムや各命令のテストコマンドが存在します。試しに手元のマシンでDhrystone を実行した結果は、以下のとおりです。
$ spike /home/ubuntu/riscv/riscv64-unknown-elf/share/riscv-tests/benchmarks/dhrystone.riscv
Microseconds for one run through Dhrystone: 393
Dhrystones per Second: 2544
mcycle = 196524
minstret = 196530
RISC-Vバイナリの中身
たとえばgdbを使って、helloプログラムのmain関数をdisassembleしてみましょう[4] 。
$ riscv64-unknown-elf-gdb hello -batch -ex 'disassemble /r main'
Dump of assembler code for function main:
0x000000000001019a <+0>: 41 11 addi sp,sp,-16
0x000000000001019c <+2>: 06 e4 sd ra,8(sp)
0x000000000001019e <+4>: 22 e0 sd s0,0(sp)
0x00000000000101a0 <+6>: 00 08 addi s0,sp,16
0x00000000000101a2 <+8>: c9 67 lui a5,0x12
0x00000000000101a4 <+10>: 13 85 87 fd addi a0,a5,-40 # 0x11fd8
0x00000000000101a8 <+14>: ef 00 a0 20 jal ra,0x103b2 <puts>
0x00000000000101ac <+18>: 81 47 li a5,0
0x00000000000101ae <+20>: 3e 85 mv a0,a5
0x00000000000101b0 <+22>: a2 60 ld ra,8(sp)
0x00000000000101b2 <+24>: 02 64 ld s0,0(sp)
0x00000000000101b4 <+26>: 41 01 addi sp,sp,16
0x00000000000101b6 <+28>: 82 80 ret
End of assembler dump.
各命令の意味はRISC-Vの「User-Level ISA Specification(PDF) 」の以下の表などが参考になるでしょう。
Table 20.1: Assembler mnemonics for RISC-V integer and floating-point registers.
Table 20.2: RISC-V pseudoinstructions.
やっていることは至極単純です。
addi sp,sp,-16 スタックポインタを16バイト分減らす
sd ra,8(sp) スタック領域に呼び出し元のアドレスを保存
sd s0,0(sp) スタック領域に呼び出し元のフレームポインタを保存
addi s0,sp,16 フレームポインタに呼び出し時のスタックアドレスを保存
lui a5,0x12 a5レジスタに「0x12 << 12 = 0x12000」を保存
addi a0,a5,-40 # 0x11fd8 a0レジスタに「a5 - 40 = 0x11fd8」
つまり"Hello wolrd!"が保存されたアドレス」を保存
jal ra,0x103b2 <puts> puts("Hello world!")を呼び出し
li a5,0 a5レジスタに0を保存
mv a0,a5 a0レジスタ(mainの戻り値)にa5レジスタの内容をコピー
ld ra,8(sp) スタック領域から呼び出し元のアドレスを復帰
ld s0,0(sp) スタック領域から呼び出し元のフレームポインタを復帰
addi sp,sp,16 スタックポインタを元に戻す
ret 呼び出し元に戻る(raレジスタのアドレスに戻る)
最適化されていないため、少しばかり無駄な処理が含まれています。「 Hello world!」の文字列はrodataセクションを確認するとアドレスがわかります。
$ riscv64-unknown-elf-objdump -sj .rodata hello
hello: ファイル形式 elf64-littleriscv
セクション .rodata の内容:
11fd8 48656c6c 6f20776f 726c6421 00000000 Hello world!....
11fe8 0a000000 00000000 ........
RISC-Vの命令長さは32bit固定ですが、「 標準拡張」機能のひとつとしてARMのThumbのような16bit幅の命令もサポートしています。そのためdisassembleしたときに、同じ加算命令であるにも関わらず、複数の長さが現れていました。
16bit幅の加算命令(s0 = sp + 16):
0x00000000000101a0 <+6>: 00 08 addi s0,sp,16
32bit幅の加算命令(a0 = a5 - 40):
0x00000000000101a4 <+10>: 13 85 87 fd addi a0,a5,-40 # 0x11fd8
RISC-Vの拡張性
RISC-Vの「標準拡張(Standard Extention) 」とはモダンなCPUであれば備えておいたほうが良い便利な命令集です。
RISC-Vは組み込みからハイパフォーマンスコンピューティングに至るまで、できるだけ広い範囲のプロセッサを構築できるよう作られています。特定の分野では便利な命令でも、別の分野では不要であるケースも多々あるため、必要最低限な整数演算やjump/load/storeのみを必須の機能とし、それ以外は「標準拡張」としてオプション扱いとしています。「 標準拡張」はRISC-Vとして標準化されているため、将来的にその命令が別の機能に割り当てられることはありません。これとは別にベンダーごとに「非標準拡張(non-Standard
Extention) 」を作ることも可能です。
基本整数命令と標準拡張は、各ソフトウェアやチップでサポートしているかどうかを判別しやすいように、それぞれ機能ごとに「フラグ」が設定されています。
I: 基本整数命令
E: レジスタの数を減らしたIのサブセット
M: 整数の乗算除算命令
A: アトミック命令
F: 単精度の浮動小数点演算命令
D: 倍精度の浮動小数点演算命令
Q: 四倍精度の浮動小数点演算命令
L: 十進の浮動小数点演算命令(未定義)
C: 圧縮命令
B: ビット演算命令(未定義)
J: JavaのようなDynamic Translation用命令(未定義)
T: トランザクショナルメモリー用命令(未定義)
P: Packed-SIMD命令
V: ベクタ命令
N: ユーザーレベル割り込み
IMAFDQC以外は「策定中」というステートです。今後大きく変更される可能性があります。
たとえば「RV32I」であれば、レジスタのサイズが32bitで基本整数命令のみをサポートしていることがわかりますし、「 RV64IMAFD」であれば64bitアーキテクチャの一般的なPC用のCPUを構築できる程度には機能が揃っていることがわかります。ちなみに「IMAFD」は「G」と省略することも可能です。つまり「RV64IMAFD=RV64G」です。また「I」からレジスタの数を半減させた「E」というフラグも存在します。
今回作ったツールチェインは、「 RV64IMAFDC」です。gccのconfigureオプションを見ると「rv64imafdc
」が指定されていることがわかります。
$ riscv64-unknown-elf-gcc -v
Using built-in specs.
COLLECT_GCC=riscv64-unknown-elf-gcc
COLLECT_LTO_WRAPPER=/home/ubuntu/riscv/libexec/gcc/riscv64-unknown-elf/7.2.0/lto-wrapper
Target: riscv64-unknown-elf
Configured with:
/home/ubuntu/riscv-tools/riscv-gnu-toolchain/build/../riscv-gcc/configure
--target=riscv64-unknown-elf --prefix=/home/ubuntu/riscv --disable-shared
--disable-threads --enable-languages=c,c++ --with-system-zlib --enable-tls
--with-newlib --with-sysroot=/home/ubuntu/riscv/riscv64-unknown-elf
--with-native-system-header-dir=/include --disable-libmudflap
--disable-libssp --disable-libquadmath --disable-libgomp --disable-nls
--src=../../riscv-gcc --enable-checking=yes --disable-multilib
--with-abi=lp64d
--with-arch=rv64imafdc
'CFLAGS_FOR_TARGET=-Os -mcmodel=medlow'
Thread model: single
gcc version 7.2.0 (GCC)
「C」フラグが付いているので、16bit幅の命令(RVC命令)も使えることがわかります。
命令長は「最下位ビットの1が連続する数」で判定されます(1.2 Instruction Length Encoding) 。最下位2ビットが「11」ではない場合、16bit長の命令です。最下位2ビットが「11」で、なおかつ最下位5ビットのうち上位3ビットが「111」ではない場合、32bitの命令長になります。さらに32bitよりも長い命令に拡張できるようにも作られています。
前述の2つのaddi
を比べてみましょう。
16bit幅の加算命令(s0 = sp + 16):
0x00000000000101a0 <+6>: 00 08 addi s0,sp,16
32bit幅の加算命令(a0 = a5 - 40):
0x00000000000101a4 <+10>: 13 85 87 fd addi a0,a5,-40 # 0x11fd8
RISC-Vの標準ではリトルエンディアンなので、命令部分のバイト列・ビット列は次のとおりです。
16bitのaddi:0x0800 = 0000 1000 0000 0000
32bitのaddi:0xfd878513 = 1111 1101 1000 0111 1000 0101 0001 0011
太字で表示している最下位2ビットを比べてみると、16bit版は「00」に32bit版は「11」になっていることがわかりますね。
さらにISA仕様書を見れば他のビットの意味も確認できます。たとえば「Table 12.5: Instruction listing for RVC, Quadrant 1.」や「Table 19.3: RISC-V control and status register (CSR) address map.」には関連命令の一覧表が掲載されています。
16bitのaddi:0x0800
FNC [RD]OP
C.ADDI4SPN 0000 1000 0000 0000
^ ^^^^ ^^^
Non-Zero IMM[5:4|9:6|2|3]
OP = 00
FNC = 000
RD = 000
IMM = 0000010000 = 0x10
OP/FNCからこの命令が「C.ADDI4SPN」だということがわかります。この命令はスタックポインタ(spレジスタ)にIMMの値を足して、RDで指定されたレジスタに保存する命令です。
IMMは「nzuimm」と書かれていますので、符号拡張しない即値となります。つまり命令に含まれていない上位ビットは0で埋められます。また下位2ビットも0で埋めます。結果的にIMMの値は「0x10 = 16」です。
RDは「000」となっています。これは「Table 12.2: Registers specified by the three-bit rs1’ , rs2’ , and rd’ fields of the CIW, CL, CS, and CB formats.」からs0レジスタであることがわかります。
まとめるとdisassembleした結果どおり「s0 = sp + 16」になりましたね。
32bit版のaddiも同様にパースできます。
32bitのaddi:0xfd878513
[IMM ] [RS1 ]FNC [RD ][OP ]
ADDI 1111 1101 1000 0111 1000 0101 0001 0011
OP = 0010011
FNC = 000
RD = 01010 = 10
RS1 = 01111 = 15
IMM = 1111 1101 1000 => 1111 1111 1101 1000 = -0x28 = -40
ADDI命令は符号拡張したIMMをRS1で指定されたレジスタに足して、RDで指定されたレジスタに保存する命令です。
IMMは下位12ビットしか記録されていないので32ビット値(RV32の場合)や64ビット値(RV64の場合)に拡張するには、最上位ビットをそのまま左に埋めることになります(符号拡張) 。今回最上位ビットは1なので、2の補数 表現からこれが「-40」であることがわかります。
「Table 20.1: Assembler mnemonics for RISC-V integer and floating-point registers.」を参考にRDはa0レジスタ(x10レジスタ) 、RS1はa5レジスタ(x15レジスタ)であることがわかります。こちらもdisassembleした結果どおり「a0 = a5 - 40」となりました。
2018年はRISC-Vで遊ぼう
いろいろなところで言われているように半導体プロセスの進化は限界に近づきつつあります。特に微細化による恩恵は、開発コスト的にも消費電力的にもこれまでのようには汎用プロセッサの性能に寄与することはないでしょう。そのような状況で、より高性能な計算能力を提供する方法の一つとして考えられているのが、「 ドメイン固有アーキテクチャ(Domain Specific Architecture) 」です。汎用プロセッサとしての性能があげられなかったとしても、特定の用途に限定してチューニングすればまだ改良の余地があるだろうということです。
グラフィックの用途に限定してGPUが進化してきたように、最近では深層学習用のTPUやブロックチェーン用のCatapultと言った専用コアが注目を浴びていますし、Pixel Visual Core/Neural Engineのように「Neural Processing Unit」が搭載されたスマートフォンも登場しています。
いわゆる専用プロセッサは開発コストが大きくなりがちです。いろいろな「コプロセッサ」がCPU/SoCに統合されていったことからもわかるでしょう。それなりに量産できるのであれば多少コストが増えてもなんとかなるのでしょうが、そもそも「特定用途」という縛りがある以上、そこまで数が必要になるわけでもありません。開発コストを抑えるために既存のIPコアを流用するとなると、今度はライセンスフィーなどの形で量産コストが増えることになります。
商用アーキテクチャの場合、開発している企業が別の企業に買収されて、ライセンス形態等が変わってしまうという可能性もあります。特にここ数年は半導体企業の大型買収にともなう再編が進んでいました。2016年にARMがソフトバンクに買収されたニュースを覚えている方もいることでしょう。ある程度落ち着いてきたとは言うものの、昨年11月末にはMarvellがCaviumを買収していますし、QualcommによるNXPの買収は結局2018年までずれ込むことになりました。そのQualcomm自身もBroadcom(=旧Broadcomを買収した旧Avago)から敵対的買収をかけられています。もうなにがなんだか。
RISC-Vの場合は、原則としてライセンスフィーは発生しませんし、基本的な情報はすべて公開されているのでカスタマイズも比較的容易です。特定の企業に依存していないため、ある日突然別の企業に買収されて、ビジネスを考え直す必要が出てくることもほぼありません。
このようにオープン規格であるということは、参入障壁や将来のリスクを低減し、よりイノベーションを起こしやすくなる可能性を秘めているのです。もちろん良いことばかりではなく、広く様々なユーザーに使われなければオープンである強みは出てこないですし、強いマネタイズの手段がない故に研究開発への投資はどうしても制限されてしまうリスクも持っています。幸い、現時点ではRISC-Vは広い注目を集めています。昨年末に日本で開催されたRISC-V Day 2017 Tokyo も大盛況でした。
2018年の新たな目標として、他の商用アーキテクチャとは別の解としてRISC-Vが流行ることを期待しつつ、注目されているうちに様々な可能性を見出してみるのはいかがでしょうか。