今回はレジスタ間接分岐命令をエクササイズしてみます。といって一族命令には今までも「お世話」にはなっとります。LR(リンクレジスタ、X30)に向かって分岐するRET命令もレジスタ間接分岐命令と原理的には同じもの。レジスタ間接分岐では論理絶対番地でどこへでも飛べますが、とび先番地には気を付けないと。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
※実機確認には以下を使用しております。
-
- Raspberry Pi 4 model B、Cortex-A72コア(ARMv8-A)
- Raspberry Pi OS (64bit) bullseye
- gcc (Debian 10.2.1-6) 10.2.1 20210110
ARMv8もいろいろレベルがあり、Arm Cortex-A72はARMv8の中でもベーシックな(命令数の少ない)ARMv8p0です。
※A64の最新のマニュアルは以下でダウンロード可能です。
Arm Architecture Reference Manual for A-profile architecture
レジスタ間接分岐
レジスタ間接分岐命令には以下のようなものがあります。
-
- BR
- BLR
- RET
最初のBR命令は、指定したXレジスタに格納されている値をとび先アドレスとして分岐するもの。2番目のBLR命令は、レジスタ間接版のCALL命令です。指定したXレジスタの番地をサブルーチンとしてそこへ分岐するとともに、LR(X30)レジスタに戻り番地 $PC+4を保存するもの。3番目のRET命令は、LRレジスタに格納されている番地へ分岐するもの。
今回は上記のレジスタ間接分岐(すべて無条件)に加えて素の無条件分岐命令Bもエクササイズしてみます。
実験に使用したアセンブリ言語ソース
前回に引き続き、LRレジスタを退避しておかないとCのメイン関数に戻れなくなるので、LRの退避と復帰だけはしているアセンブリ関数ソースです。内部の動作はこんな感じ。
-
- LRをスタック退避
- サブルーチン0をBL命令で普通にCALL
- サブルーチン0は即RET
- LRレジスタの内容をX0にコピーした上で+20する
- サブルーチン1をレジスタX0間接でCALL
- サブルーチン1内で戻り値をX0に書き込み
- サブルーチン1から戻ったら、LRをスタックから復帰
- C言語main()へ戻る
2でサブルーチン0を呼びだすことで、LRに書き込まれるアドレスを加工してサブルーチン1のアドレスを作っているところがミソというか、イケないところというかデス。
.globl tst_ubranch .text .balign 4 tst_ubranch: str lr, [sp, #-16]! bl sub0 b nxt0 sub0: ret nxt0: mov x0, x30 add x0, x0, #20 b nxt1 sub1: mov x0, #1234 ret nxt1: blr x0 ldr lr, [sp], #16 ret
実験に使用したC言語ソース
上記のアセンブリ言語関数を呼び出すC言語ソースが以下に。今回はアセンブリ言語関数内で「飛び回る様子」を観察するのがメインなので、そいつを呼び出すだけが使命っす。
#include <stdio.h> #include <stdint.h> extern uint64_t tst_ubranch(uint64_t); int main(void) { uint64_t resultX; resultX = tst_ubranch(0); printf ("tst_callret(0): %016lx\n", resultX); return 0; }
ビルドしてオブジェクトを確認
ビルドは以下のコマンドラインで行いました。
$ gcc -g -O0 ubranch.c ubranch.s
生成された実行オブジェクト a.out の「アセンブリ言語関数」部分を逆アセンブルしてみたものが以下に。
GDB(TUIモード)でアセンブラ動作を確認
さて「飛び回る」ところの観察には、前回につづき、gdb (tuiモード)を使用してみます。起動のコマンドラインが以下に。
$ gcc -tui ./a.out
起動後、以下のおまじないをいたします。
(gdb) tui reg general (gdb) display/i $pc (gdb) b 10 (gdb) run
ここから、stepi コマンドを使って、1命令づつ実行していきます。まずはアセンブリ言語関数の先頭を実行する直前までプログラムカウンタを進めて」みます。こんな感じ。
上記のLR退避後、sub1をcall(bl)して、retした後にLR(x30)の内容を確認したところが以下に。
さて、x30の内容をx0にコピーした上で+20してやると、sub1ルーチンのアドレスがx0に入っていることになりまする。手品の種?
sub1に突入し先頭の命令を実行しようとしているところが以下に。
上記の命令を実行すると、x0レジスタにはアセンブリ言語関数からの戻り値1234(0x4d2)がロードされていることになりました。
sub1からRET、その後スタックから元のLR値を復帰してさらにRETすればC言語のmainに戻ります。continue一発!
正常に実行終了したみたい。予定通りのジャンプよな。