前回条件分岐をやり、今回はロード、ストアのつもりだったのですが、1つ忘れていることに気づきました。32ビットの即値、つまりはメモリアドレスのロードです。RISC系CPUでは限られた命令のビット幅との兼ね合いで苦労する部分ですが、RISC-Vはカッコよく始末している方ではないかと思います。とりあえずシンボルアドレスをGETしてメモリロードをしてみます。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
RISC-Vのメモリアドレッシングは基本1種類でなんでもやってしまうのである意味楽で良いのですが、必ずメモリの「ベースアドレス」をレジスタに置いておく必要があります。ビット幅はRV32なら32ビット、RV64なら64ビットと明快です。
以前にやりましたが、通常の即値データを扱う命令でレジスタにロードできるのは、12ビットの値を符号拡張して32ビット化したものでした。最悪12ビットセットしてはシフトして論理演算という感じで(3回目は8ビットだけれども)繰り返せばそれでも32ビット値をレジスタに置くことは不可能ではありません。しかしあんまりです。
当然RISC-Vではアドレスとして使うような「フルにビット幅を使う」即値のロードも考えられていて、RV32Iでは「とりあえず」2命令を使えば任意の32ビットの即値(アドレスには限らない)をレジスタに置くことができます。また、絶対アドレスに引きずられない「PC相対」のアドレスをレジスタ上に準備する方法も用意されています。そしてどちらも「疑似命令」を使えば、アセンブラ上では1命令に見えるのでカッコいいです。
まずは毎度の参照資料へのリンクの再掲載です。
RV32Iの32ビット幅取扱い可能な即値ロード疑似命令
一つ目は、汎用の即値ロード疑似命令 li です。
li rd, immediate
この疑似命令により、レジスタrdにレジスタ幅いっぱいの32bitまでの即値をロードすることができます。実際には以下の2命令に展開されてました。
-
- lui rd, immediateの上位20ビット相当
- addi rd, rd, immediateの下位12ビット相当
「相当」などと歯切れの悪い書き方をしているのが、この疑似命令が結構「善きに計らってくれる」もので、単純にビット分割しているわけでもないからです。実例は後で。
メモリマップのIOレジスタにアクセスする場合などは、この即値ロード命令でペリフェラルのベースアドレスをロードし、そこからのオフセットを付加するスタイルでロード、ストアができます。また、IOアクセスは絶対番地固定なので、オブジェクトのリロケーションなど不要でもあります。
一方、純然たるメモリ上のオブジェクトのアドレスを指す場合にはPC相対のアドレシングの方が便利です。しかし、RISC-Vでは、プログラムカウンタPCをそのままアドレシングのベースアドレスとして使用する「表立っての」PC相対アドレシングモードはサポートされていません。代わりに使用されるのが、以下の命令です。
auipc rd, immediate
pcの値に、immediate(20bit幅)を12ビット左シフトして符号拡張した値を加えた結果をrdに書き込む、という命令です。そして引き続くaddi命令で下12ビット分の即値をrdに加えることができます。すると rd には
pc + 32bitの符号付き即値
というポインタが得られるというわけです。pc相対アドレシングを使えるプロセッサの場合、到達可能範囲はpcの前後の限られた範囲であることが多いです。しかしRISC-Vの場合は、間接的なpc相対ですが、制限なく全メモリ空間をカバーできるので非常に使いでが良い感じがします。pc相対アドレシングを使ったんだけれども微妙に遠かったりしてエラーになった経験がおありになる人には朗報かもしれません(そんなやつおったんか、と雑音が聞こえる気がする。)
実験用のアセンブラ関数コード
以下に実験用のコード(追加部分、基本部分は 第28回 に掲載)をかかげます。最初の2つはどちらも12ビットでは収まらない即値のロードです。li 疑似命令を利用。どんなコードが生成するのかお楽しみです。
3つ目は、テキストセグメント内の「定数」的な数値を読み取って返却する関数です。シンボル(メモリアドレス)を la 疑似命令でレジスタにロード(PC相対)し、そのアドレスを使ってメモリ上の値を読み出しています。ワード幅のロード命令1個だけ使用(ロード、ストア一族の面々は次回練習の予定。)
li1: li a0, 0x7FFFFFFF ret li2: li a0, 0x7FFF ret la1: la t0, LA1_LBL0 lw a0, 0(t0) ret LA1_LBL0: .int 12345678
さてお楽しみの生成コードの確認ですが、以下のようになりました。
080008d6 <li1>: 80008d6: 80000537 lui a0,0x80000 80008da: fff50513 addi a0,a0,-1 # 7fffffff <_sp+0x5fff7fff> 80008de: 8082 ret 080008e0 <li2>: 80008e0: 00008537 lui a0,0x8 80008e4: fff50513 addi a0,a0,-1 # 7fff <__stack_size+0x77ff> 80008e8: 8082 ret 080008ea <la1>: 80008ea: 00000297 auipc t0,0x0 80008ee: 00e28293 addi t0,t0,14 # 80008f8 <LA1_LBL0> 80008f2: 0002a503 lw a0,0(t0) 80008f6: 8082 ret
疑似命令は先ほどの説明どおり各2命令に展開されてますが、li の展開などそう来ましたか、という感じ。
アセンブラ関数を呼び出すためのCコード(追加部分)
ヘッダファイルに追加したのは以下の3つのプロトタイプ宣言です。
int li1(void); int li2(void); int la1(void);
main()関数内に置いたのは、以下の関数呼び出し確認部分です。
printf("li 0x7FFFFFFF = 0x%08x\n", li1()); printf("li 0x7FFF = 0x%08x\n", li2()); printf("la + ld = %d\n", la1());
実行結果
実行結果が以下に。期待どおりの動きであります。
li 0x7FFFFFFF = 0x7fffffff li 0x7FFF = 0x00007fff la + ld = 12345678
来週こそ、ロード、ストアですな。まあ、アドレスが出来てしまえば後は簡単。本当か?