Everyday Pieces ::
  • Webサービス
  • ブログパーツ
  1. ホーム
  2. プログラミング

RISC-Vを試してみる(2)

2020年12月27日 プログラミング RISC-V

 前回の続きです。
それでは実際にRISC-Vなコードにコンパイルして試してみましょう。
こちらにある「RISC-V GNU Compiler Toolchain」を使います。
Windowsでは動かないので(WSLとか使えば行けるはずですが)、
Ubuntuな環境でやってみます。
ちなみに自分が使っているのは Kubuntu 20.04LTS です。

RISC-V GNU Compiler Toolchain を導入

 まずは、
作業フォルダを作ったりするなどして、
そこで以下のようにやって、必要となるファイルを導入します。
(※gitが未導入の場合は、$ sudo apt-get install git してください)

$ git clone --recursive https://github.com/riscv/riscv-gnu-toolchain

以前に自分がやった時は --recursive が必要でしたが、今は必要ないようです。

 思った以上に時間を要する感じ。
自分のPCは Ryzen 3600 + NVMeなストレージなので、
そこそこ速いはずですが、数十分くらい掛かったような?
というか通信速度にも依りそうですね。
なお、説明によると上記の作業で8GBほどのディスク容量が必要になるようですが、
makeすると最終的には20GBほど消費するような感じでした、確か。
なので、空き容量には一応注意しておいた方がよいかと思います。

ビルドするのに必要なパッケージを導入

 続いて、
ビルドするのに必要なパッケージを導入します。
以下のようにやります。これは、それほど時間を要しない感じでした。

$ sudo apt-get install autoconf automake autotools-dev curl python3 libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev

事前設定を行ってビルドする

 そして、
Newlibなやつでmakeします。たぶん、Newlibでやるのが無難?
こちらに書かれているようにやれば、
/opt/riscvフォルダにRV64GCな命令セット対応で作られます。
ですが、自分の環境は少々空き容量に余裕が無いのと、
64bitより32bit版の方が早くmakeが終わるかもしれないし、
クロスコンパイルの動作確認する程度なので、
今回はあえてRV32GCでmakeします。
ということで、以下のようにやります。

$ cd riscv-gnu-toolchain
$ ./configure --prefix=/opt/riscv --with-arch=rv32gc --with-abi=ilp32d
$ sudo make

 makeが終わるまでに約40分ほど掛かりました(^_^;)
気長に待ちましょう。

 なお、
しばらく経った後に、最新版のソースをpullしてmakeし直すということが無いようならば、
make時に作られた一時的なファイルは必要ないので、$ sudo make clean で削除できます。
というか、riscv-gnu-toolchain フォルダごと処分してしまって構わないかも(^_^;)

コマンド実行のための準備

 makeの結果 /opt/riscv/bin/ に以下のコマンドが作成されます。

riscv32-unknown-elf-addr2line
riscv32-unknown-elf-ar
riscv32-unknown-elf-as
riscv32-unknown-elf-c++
riscv32-unknown-elf-c++filt
riscv32-unknown-elf-cpp
riscv32-unknown-elf-elfedit
riscv32-unknown-elf-g++
riscv32-unknown-elf-gcc
riscv32-unknown-elf-gcc-10.2.0
riscv32-unknown-elf-gcc-ar
riscv32-unknown-elf-gcc-nm
riscv32-unknown-elf-gcc-ranlib
riscv32-unknown-elf-gcov
riscv32-unknown-elf-gcov-dump
riscv32-unknown-elf-gcov-tool
riscv32-unknown-elf-gdb
riscv32-unknown-elf-gdb-add-index
riscv32-unknown-elf-gprof
riscv32-unknown-elf-ld
riscv32-unknown-elf-ld.bfd
riscv32-unknown-elf-lto-dump
riscv32-unknown-elf-nm
riscv32-unknown-elf-objcopy
riscv32-unknown-elf-objdump
riscv32-unknown-elf-ranlib
riscv32-unknown-elf-readelf
riscv32-unknown-elf-run
riscv32-unknown-elf-size
riscv32-unknown-elf-strings
riscv32-unknown-elf-strip

 絶対パスでいちいち入力するのは面倒なので、パスを通しておきます。
nanoなどのエディタで編集します。

$ nano ~/.bashrc
# ↓この一行を追加。
export PATH=${PATH}:/opt/riscv/bin
$ source ~/.bashrc

クロスコンパイルして出力コードを確認してみる

 ということで、
ようやくクロスコンパイルする準備が整いました。
定番の hello.c でやってみます。

#include <stdio.h>

int main(int argc, char *args[])
{
    printf("Hello, world!\n");
    return 0;
}

 一応、最適化レベル2を指定してやってみます。

$ riscv32-unknown-elf-gcc hello.c -o hello -O2 
$ riscv32-unknown-elf-run hello

 すると、無事に「Hello, world!」が出力されました(^_^)
では、気になるアセンブラなコードを出力して見てみましょう。

$ riscv32-unknown-elf-gcc hello.c -S -O2
	.file	"hello.c"
	.option nopic
	.attribute arch, "rv32i2p0_m2p0_a2p0_f2p0_d2p0_c2p0"
	.attribute unaligned_access, 0
	.attribute stack_align, 16
	.text
	.section	.rodata.str1.4,"aMS",@progbits,1
	.align	2
.LC0:
	.string	"Hello, world!"
	.section	.text.startup,"ax",@progbits
	.align	1
	.globl	main
	.type	main, @function
main:
	lui	a0,%hi(.LC0)
	addi	sp,sp,-16
	addi	a0,a0,%lo(.LC0)
	sw	ra,12(sp)
	call	puts
	lw	ra,12(sp)
	li	a0,0
	addi	sp,sp,16
	jr	ra
	.size	main, .-main
	.ident	"GCC: (GNU) 10.2.0"

 おお、
確かにRISC-Vなコードになっているようです。
.attribute arch, "rv32i2p0_m2p0_a2p0_f2p0_d2p0_c2p0" となっているので、
命令セットのバージョンが32ビット版で基本のI、拡張のM,A,F,D,Cのいずれもが 2.0 準拠となっているのが分かります。
でも、これだと実際のバイナリコードが分かりませんね。
また、16bit縮小命令がちゃんと使われているのかを確認できません。
なので、objdumpしてみましょう。

$ riscv32-unknown-elf-objdump -d hello

 長いので冒頭の一部分だけ載せます。

セクション .text の逆アセンブル:

00010074 <main>:
   10074:	6549              	lui	a0,0x12
   10076:	1141              	addi	sp,sp,-16
   10078:	4f450513          	addi	a0,a0,1268 # 124f4 <__errno+0x6>
   1007c:	c606              	sw	ra,12(sp)
   1007e:	2489              	jal	102c0 <puts>
   10080:	40b2              	lw	ra,12(sp)
   10082:	4501              	li	a0,0
   10084:	0141              	addi	sp,sp,16
   10086:	8082              	ret

00010088 <register_fini>:
   10088:	00000793          	li	a5,0
   1008c:	c789              	beqz	a5,10096 <register_fini+0xe>
   1008e:	6541              	lui	a0,0x10
   10090:	67450513          	addi	a0,a0,1652 # 10674 <__libc_fini_array>
   10094:	ae91              	j	103e8 <atexit>
   10096:	8082              	ret

00010098 <_start>:
   10098:	00004197          	auipc	gp,0x4
   1009c:	c8018193          	addi	gp,gp,-896 # 13d18 <__global_pointer$>
   100a0:	04418513          	addi	a0,gp,68 # 13d5c <__malloc_max_total_mem>
   100a4:	09c18613          	addi	a2,gp,156 # 13db4 <__BSS_END__>
   100a8:	8e09              	sub	a2,a2,a0
   100aa:	4581              	li	a1,0
   100ac:	28d5              	jal	101a0 <memset>
   100ae:	00000517          	auipc	a0,0x0
   100b2:	33a50513          	addi	a0,a0,826 # 103e8 <atexit>
   100b6:	c511              	beqz	a0,100c2 <_start+0x2a>
   100b8:	00000517          	auipc	a0,0x0
   100bc:	5bc50513          	addi	a0,a0,1468 # 10674 <__libc_fini_array>
   100c0:	2625              	jal	103e8 <atexit>
   100c2:	2895              	jal	10136 <__libc_init_array>
   100c4:	4502              	lw	a0,0(sp)
   100c6:	004c              	addi	a1,sp,4
   100c8:	4601              	li	a2,0
   100ca:	376d              	jal	10074 <main>
   100cc:	a0b9              	j	1011a <exit>
                                        ︙

 ふむふむ、
16bitな縮小命令が適宜使われて、
バイナリコードがよりコンパクトになっているようです。
なかなか良い感じですね。
表記上は、lui a0,0x12 となってますが、
実際には c.lui x10,0x12 になるのですね。

 <main>:において、raレジスタをスタックに保存退避していますが、
4バイトで済むはずなのに、16バイト確保しているのが気になりました。
.attribute stack_align, 16 とあるので、
スタックは16バイト単位に確保するようになっているようです。
理由はよくわからないけど、キャッシュラインのサイズに適応させるためかもしれません。

X86_64のコードと比較してみる

 比較のために、
X86_64のコードも見てみましょう。

$ gcc hello.c -o hello -O2
$ objdump -d hello

 こちらも長いので、冒頭の一部分だけ載せます。

セクション .text の逆アセンブル:

0000000000001060 <main>:
    1060:	f3 0f 1e fa          	endbr64 
    1064:	48 83 ec 08          	sub    $0x8,%rsp
    1068:	48 8d 3d 95 0f 00 00 	lea    0xf95(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    106f:	e8 dc ff ff ff       	callq  1050 <puts@plt>
    1074:	31 c0                	xor    %eax,%eax
    1076:	48 83 c4 08          	add    $0x8,%rsp
    107a:	c3                   	retq   
    107b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

0000000000001080 <_start>:
    1080:	f3 0f 1e fa          	endbr64 
    1084:	31 ed                	xor    %ebp,%ebp
    1086:	49 89 d1             	mov    %rdx,%r9
    1089:	5e                   	pop    %rsi
    108a:	48 89 e2             	mov    %rsp,%rdx
    108d:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp
    1091:	50                   	push   %rax
    1092:	54                   	push   %rsp
    1093:	4c 8d 05 46 01 00 00 	lea    0x146(%rip),%r8        # 11e0 <__libc_csu_fini>
    109a:	48 8d 0d cf 00 00 00 	lea    0xcf(%rip),%rcx        # 1170 <__libc_csu_init>
    10a1:	48 8d 3d b8 ff ff ff 	lea    -0x48(%rip),%rdi        # 1060 <main>
    10a8:	ff 15 32 2f 00 00    	callq  *0x2f32(%rip)        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
    10ae:	f4                   	hlt    
    10af:	90                   	nop
                                        ︙

 まるで、ウィンチェスター・ミステリー・ハウスの如く、
X86は長年に渡って、増築の上に増築して来たような所があるので、
命令毎のバイト長がカオスな感じになっているのが否めませんね(^_^;)
ARMのように64bit化の際に、
バッサリとコード体系を切り替えるやり方もあったはずなんだけど・・・。

 あと、
endbr64 という謎な命令が気になりますね。
調べてみた所、

主にプロセッサ・パイプラインが制御フローの違反を検出するためのマーカー命令として使用されます。

 ということらしい。よくわからないけどセキュリティにも関係しているのかな?
いずれにしろ、endbr64自体は何もしないコードのようだ。
う~む。

 それから、
nop や nopl とかが使われているのも気になりました。
調べてみた所、

CPU命令キャッシュ(L1と呼ばれる)は、プロセッサデコーダに一度に16バイトをロードすることで動作します。そのため、16バイトの境界にコードを合わせることができれば、うまく行きます。

 だそうだ。
要するに、16バイト単位に読み込むためにnopで穴埋めしているということ。
確かに各関数のアドレスはちょうど16バイトの整数倍になっているのが分かる。
う~む・・・なんかコード効率がイマイチですな(^_^;)

 でも考えてみると、
X86のコードはもはや中間コードみたいなもので、
実際にはCPUの内部命令に変換された後に実行されていたりします。
なので、こういうやり方が理にかなっているのかもしれませんね。
そのためのオーバーヘッドやコストがどのくらいになるのか気になるけど・・・。

感想など

 さて、
こうしてコードを比較してみると、
RISC-Vがより洗練されたコードを目指していることが分かります。
一方、X86のようなカオスなコードであっても、
長年の互換性から使われ続けているのも事実で、
技術の進歩でしっかり性能も出してきているわけです。
またARMはv8以降、世の中のニーズに応じた命令を追加して進化し続けています。
あのスーパーコンピューター「富岳」でも使われています。
企業が開発したCPUには長年のノウハウが詰まっており、
マーケティングによって必要とされる進化をしています。
さて、オープンソースなRISC-Vはそんな状況にどこまで食い込めるのでしょうか?
個人的には期待をもって今後も追ってみたいと思っています。

関連記事
RISC-Vを試してみる

コメントする キャンセル

メールアドレスが公開されることはありません。が付いている欄は必須項目です。

投稿ナビゲーション

RISC-Vを試してみるPDFなカレンダーを生成してみた(5)

カテゴリー

WordPress つぶやき トピック プログラミング 未分類

タグ

AS3 enchant.js FamilyTreeVis Flash Geolocation gif.js kinect Linux MMD MoneyTrackNote OpenCV PDFカレンダー RISC-V three.js セキュリティ テーマ自作 ブログパーツ 動物 動画 麻雀

アーカイブ

© Everyday Pieces ::