ぐだぐだ低レベルプログラミング(100)ARM64(AArach64)、レジスタ間接分岐

Joseph Halfmoon

今回はレジスタ間接分岐命令をエクササイズしてみます。といって一族命令には今までも「お世話」にはなっとります。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

レジスタ間接分岐

レジスタ間接分岐命令には以下のようなものがあります。

    1. BR
    2. BLR
    3. RET

最初のBR命令は、指定したXレジスタに格納されている値をとび先アドレスとして分岐するもの。2番目のBLR命令は、レジスタ間接版のCALL命令です。指定したXレジスタの番地をサブルーチンとしてそこへ分岐するとともに、LR(X30)レジスタに戻り番地 $PC+4を保存するもの。3番目のRET命令は、LRレジスタに格納されている番地へ分岐するもの。

今回は上記のレジスタ間接分岐(すべて無条件)に加えて素の無条件分岐命令Bもエクササイズしてみます。

実験に使用したアセンブリ言語ソース

前回に引き続き、LRレジスタを退避しておかないとCのメイン関数に戻れなくなるので、LRの退避と復帰だけはしているアセンブリ関数ソースです。内部の動作はこんな感じ。

    1. LRをスタック退避
    2. サブルーチン0をBL命令で普通にCALL
    3. サブルーチン0は即RET
    4. LRレジスタの内容をX0にコピーした上で+20する
    5. サブルーチン1をレジスタX0間接でCALL
    6. サブルーチン1内で戻り値をX0に書き込み
    7. サブルーチン1から戻ったら、LRをスタックから復帰
    8. 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 の「アセンブリ言語関数」部分を逆アセンブルしてみたものが以下に。

ub_list

 

 

GDB(TUIモード)でアセンブラ動作を確認

さて「飛び回る」ところの観察には、前回につづき、gdb (tuiモード)を使用してみます。起動のコマンドラインが以下に。

$ gcc -tui ./a.out

起動後、以下のおまじないをいたします。

(gdb) tui reg general
(gdb) display/i $pc
(gdb) b 10
(gdb) run

上記run後の tui 画面が以下に。ubTui0

ここから、stepi コマンドを使って、1命令づつ実行していきます。まずはアセンブリ言語関数の先頭を実行する直前までプログラムカウンタを進めて」みます。こんな感じ。ubTui1

上記のLR退避後、sub1をcall(bl)して、retした後にLR(x30)の内容を確認したところが以下に。ubTui2

さて、x30の内容をx0にコピーした上で+20してやると、sub1ルーチンのアドレスがx0に入っていることになりまする。手品の種?ubTui3

実際に sub1 を呼び出す直前の状態が以下に。ubTui4

sub1に突入し先頭の命令を実行しようとしているところが以下に。ubTui5

上記の命令を実行すると、x0レジスタにはアセンブリ言語関数からの戻り値1234(0x4d2)がロードされていることになりました。ubTui6

sub1からRET、その後スタックから元のLR値を復帰してさらにRETすればC言語のmainに戻ります。continue一発!ubContinue

正常に実行終了したみたい。予定通りのジャンプよな。

ぐだぐだ低レベルプログラミング(99) ARM64(AArach64)、所謂call、ret へ戻る

ぐだぐだ低レベルプログラミング(101)ARM64(AArach64)SIMD&FPレジスタ へ進む