わらばんし仄聞記

南の国で引きこもってるWeb屋さん

原書で学ぶ64bitアセンブラ入門(3)

さて、間が空いてしまいましたが続きをば。
本当なら前回に続いてchapter3について書くのが筋なんでしょうが、つい先日にやった勉強会での範囲に当たるところを先に書いてしまおうかと。

chapter8 Branching and looping

ということで、一気に飛んで8章で、分岐とループについてです。
分岐といってもif文のような明確な構造があるわけでもなく、ほかの言語で言うところのGOTO文のようなものを駆使して実現する事になります。

unconditional jump

無条件ジャンプとの名の通り、この命令が実行されると条件無く対象のラベルの位置へとばされる命令です。
書式としては

jmp     label

という具合にjmpというニーモニックに飛び先のラベルを指定するだけです。
この節についてはこれだけで終わってしまうこともできますが、せっかくなのでもうちょっと掘り下げてみることに。
この飛び先のラベルですが、機械語になる場合は今の位置から前後どちらに何バイト移動した所かという差分値が入ることになります。つまり、命令とアドレスの差が即値として入るわけですが、これが127バイト以内の場合は機械語で2バイトとして表されます。それ以上の場合は差分値用の値が4バイト分割り当てられ、合計5バイトとなります。

無条件ジャンプではないですが、jz命令の場合はこんな感じ

0x74 0x0e                 # 差分が127バイト以内の場合
0x0f 0x00 0x01 0x00 0x00  # 差分が128以上となる場合

また、jmpだけしか使えなくてもswitch文に相当する構文を実装することができます。コードは本書から引用。


Conditional jump

これについてはアセンブラをやると頻出ですね。なのであまりそこまで改まって書くほどではないと思うので、軽めに。
基本的にはcmp命令やmov命令を実行する事によってフラグレジスタの値を操作し、そのフラグの値に応じてジャンプするか否かを判別して挙動を変える命令を実行される流れとなります。
注意点として、C言語などで記述されたif文と同様の内容をアセンブラで似せて書こうとすると、分岐条件の条件式を反対にする必要があります。
具体的には

if (a < b) {
    a = b;
}

とあったとるすると、アセンブラでは

    mov     rax, [a]
    mov     rbx, [b]
    cmp     rax, rbx
    jge     skip
    mov     [b], rax
skip:

となります。jgeに当てはまる場合ジャンプしてるので、rax >= rbx である場合にskipへジャンプさせる事になりますね。
必ずこう書かないといけないわけでは無いが、if文と同様に条件式の内容がtrueの場合に直下の処理が入る方が直感的にわかりやすいかと思われる。

Looping with conditional jumps

続いてループのお話。
単純に考えて、あるところで無条件に一定の箇所へ戻るジャンプ命令があれば、それでまずは無限ループができあがる。あとはその無限ループを脱出する処理があれば良いことになる。
本書の内容では、以下のような内容のコードについて考えてみている。

dataという何らかの値が入った変数に対して、この変数に1のビットが何個あるかをカウントしているコードである。
これをアセンブリに直してみると

こんな感じに。
rcxがカウンタの役割を果たし、以下の3命令でビットが1ならば結果をインクリメントするようになってます。

        bt      rax, 0
        setc    bl
        add     edx, ebx

ビットテスト命令(bt)でraxの0番目のビットが1であるか確かめます。1の場合はキャリーフラグが立つこととなり、setc命令でblへキャリーフラグの中身が入れられることとなる。結局ビット単位なら1か0なので、それをsumの値に加算する事と同義となります。

さて、上記のアセンブラコードは手動で元のCコードをアセンブラへ変換したもので、実際にコンパイラへ任せて実行してみるとどうなるか。
こんな感じで実際にC言語での省略されていた前後を整えて実行してみると

これが、下記のようなコマンドを通してコンパイルすると

$ gcc -O3 -S -masm=intel countbits.c


以下のように変換される

ループのテストが最後に移動し、ループしている箇所の命令数も先述のアセンブラコードよりも短くなっていますね・・・。というわけで、下手に自分でアセンブラコードを書くより、コンパイラの最適化に任せた方が効率がいいようです。
ちなみにこれ、コンパイル元のC言語でsumをreturnすらしない様にして試したら、結局このコードで得るべき結果は無いということで、そもそも何もしないようなアセンブラコードになりました。

repeat string(array) instruction

この章の最後はrep命令(リピート命令)について。
とりあえずこの命令で重要視されるレジスタは以下の通り

rcx 繰り返し回数のカウント
rax 特定の値の保持
rsi 反復処理のソース
rdi 反復処理のディスティネーション

これらのレジスタとは別に、ディレクションフラグ(DF)も挙動に影響します。また、rep命令での挙動を指示する際に、何バイトずつ処理するかでb,w,d,qの接尾辞を使用します。

例:movs命令
lea rsi, [source]
lea rdi, [destination]
omv rcx, 10000
rep movsb

このように記述すると、rsiレジスタの内容をrdiレジスタへmovするという処理を10000回繰り返す事になります。つまりは、配列sourceから配列destinationへ10000バイトをコピーするというような挙動になります。

例:stos命令

この命令はstosXのXに入る何バイトを処理するかを示す文字によって、rax, eax, ax, alのいずれかから、データをrdiが示すアドレスへコピーします。なお、rdiが示すアドレスは自動的に更新されます(増えるか減るかはDFに依る)。

mov eax, 1
mov ecx, 1000000
lea rdi, [destination]
rep stosd

上記の例ではdouble wordずつ、1という値が入った要素でrdiに1000000回渡すという処理を行っています。

その他

loads命令やscas命令なんかがありますが、上記を踏まえておけばあとはrep命令のパターンなんかを合わせて調べれば理解できる内容かと思われます。