前回、前々回と実機練習をサボって調べ物をしていたので、今回は練習あるのみです。でも溜まっている数が多いです。今回は、前々回のMOV命令に絞って練習してみます。一通り撫でるだけでも命令数多いです。単なるMOV、転送命令なんだけれども。そう単純でもないんだこれが。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
※動作確認は普及価格帯のAndroidスマホで行っています。Cortex-A73/Cortex-A53 各4コアの bigLITTLE 64ビット。Android上にインストールしたTermux環境にWindowsPCからSSH接続し、clang/llvmのツールチェーンでビルドしております。
今回練習する命令のアセンブラコード
以下に、基本1命令=1関数のスタイルでA64命令をエクササイズするアセンブラコード(アセンブリ言語ソース)を掲げています。ただ前々回で、ほとんどのMOV命令は他の命令のエイリアスであることが分かりました。そこで実体命令とそのエイリアスであるMOV命令がある場合、その対応が分かるように、
実体が同じ命令を2個並べて
記述してあるものがあります。これら関数を後でディスアセンブルすれば、機械語レベルでは同一であることが確認できます。
まずは「レジスタ間の」movの部分から。
.globl movW, movX, movspW, movspX, movzW, movzX, movzLHW, movzLHX, movnLHW, movnLHX, movkLHW, movkLHX .text .balign 4 movW: orr w0, wzr, w1 mov w0, w1 ret movX: orr x0, xzr, x1 mov x0, x1 ret movspW: add w0, wsp, #0 mov w0, wsp ret movspX: add x0, sp, #0 mov x0, sp ret
コマケー話ですが、私は movspXのところで、ついxspなどと書いてアセンブラに怒られました。32ビット幅のスタックポインタはwspですが、64ビット幅の本物のスタックポインタの御名前はただの sp です。ボーンヘッドでX=64bitなどと思い込んでいるとエラーがでますで。
上記部分をディスアセンブルしてみたものが以下に。並べて書いてある2命令がまったく同じ機械語コードになっているのが分かるかと思います。なお、ディスアセンブラは mov 表記がお好みみたいデス。実体命令の論理演算、算術演算は隠しておこうという魂胆か?知らんけど。
続いて実体命令が movz になる命令です。Arm社の以下のマニュアルによれば、
Arm Architecture Reference Manual for A-profile architecture
シフト量が0ビットのときは単純な16ビットの即値転送に見えるので、MOVがエイリアスで使えるように書かれています。シフト量が0ビット以外では本来のMOVZで書けという感じ。
movzW: movz w0, #63, LSL #0 mov w0, #63 ret movzX: movz x0, #63, LSL #0 mov x0, #63 ret movzLHW: movz w0, #63, LSL #16 ret movzLHX: movz x0, #63, LSL #16 ret
上記をディスアセンブルしたものが以下に。エイリアス表記ができるものがエイリアスのMOV優先でディスアセンブルされているのは良いとして、Arm社のマニュアルではMOVZで書くことになっているシフト0ビット以外の命令もMOV表記にディスアセンブルしています。その上、即値をみると「エンコード上はありえない」24ビット幅表記です。まあ、分かり易いっちゃ分かり易いのだけれども。
最後に MOVN と MOVK です。こいつらは前々回に勉強したとおりで、反転したり、前の値の一部を書き換えるだけだったり、ちょっと普通のMOVというにはクセが強いので、そのまま記述してます。特に MOVKは、レジスタの前の値の一部が残るので、先にMOVNした後で、MOVKしています。
movnLHW: movn w0, #63, LSL #16 ret movnLHX: movn x0, #63, LSL #16 ret movkLHW: movn w0, #0, LSL #0 movk w0, #63, LSL #16 ret movkLHX: movn x0, #0, LSL #0 movk x0, #63, LSL #16 ret
上記をディスアセンブルしたものが以下に。ディスアセンブラはともかくmov表記に帰着させたいみたいで、MOVNも先ほどのMOVZ同様「エンコード上はありえないビット幅」のMOV表記にディスアセンブルされてます。しかし、MOVK命令では、前の命令の値次第なので、1命令単位で結論だしている筈のディスアセンブラはMOVに帰着させることができず、本来のMOVK命令が見えてます。是々非々なご対応。
実験に使用したC言語ソース
上記のアセンブリ言語ソースで定義された関数を呼び出して結果を観察するためのC言語コードが以下に。通り一遍、撫でるだけのもの。
#include <stdio.h> extern uint32_t movW(uint32_t, uint32_t); extern uint64_t movX(uint64_t, uint64_t); extern uint32_t movspW(); extern uint64_t movspX(); extern uint32_t movzW(); extern uint64_t movzX(); extern uint32_t movzLHW(); extern uint64_t movzLHX(); extern uint32_t movnLHW(); extern uint64_t movnLHX(); extern uint32_t movkLHW(); extern uint64_t movkLHX(); int main(void) { uint32_t result; uint64_t resultX; result = movW(0, 0x12345678); printf ("movW(0x12345678): 0x%x\n", result); resultX = movX(0, 0xabcdef0012345678); printf ("movX(0xabcdef0012345678): 0x%lx\n", resultX); result = movspW(); printf ("movspW(): 0x%x\n", result); resultX = movspX(); printf ("movspX(): 0x%lx\n", resultX); result = movzW(); printf ("movzW(): 0x%x\n", result); resultX = movzX(); printf ("movzX(): 0x%lx\n", resultX); result = movzLHW(); printf ("movzLHW(): 0x%x\n", result); resultX = movzLHX(); printf ("movzLHX(): 0x%lx\n", resultX); result = movnLHW(); printf ("movnLHW(): 0x%x\n", result); resultX = movnLHX(); printf ("movnLHX(): 0x%lx\n", resultX); result = movkLHW(); printf ("movkLHW(): 0x%x\n", result); resultX = movkLHX(); printf ("movkLHX(): 0x%lx\n", resultX); return 0; }
ビルドして実行
例によってTermux上でのビルドは clang 使用です。
$ clang -g -O0 mov.c mov.s
実行結果は以下です。
$ ./a.out movW(0x12345678): 0x12345678 movX(0xabcdef0012345677): 0xabcdef0012345678 movspW(): 0xc42570a0 movspX(): 0x7fc42570a0 movzW(): 0x3f movzX(): 0x3f movzLHW(): 0x3f0000 movzLHX(): 0x3f0000 movnLHW(): 0xffc0ffff movnLHX(): 0xffffffffffc0ffff movkLHW(): 0x3fffff movkLHX(): 0xffffffff003fffff
転送命令が転送しておる値は予定どおり。そんなもんか。