前回の記事では機械語の基礎知識概念について書きましたが、今回はフロー制御、条件判断、ループの実装についてまとめました。
マシンコードの一部は、
マシンコードでは、通常、jmp 命令は特定のコード ブロックにジャンプするために使用されます。たとえば、マシンコードは次のようになります。
decision:
subq $8, %rsp
testl %edi, %edi
je .L2
call op1
jmp .L1
.L2:
call op2
.L1:
addq $8, %rsp
ret
行ごとの実行では、jmp 命令が見つかると、C 言語の GOTO と同様に、対応するコード ブロックに直接移動し、中間部分をスキップできます。
待って、それは何ですか?jeについて話す前に、まず条件コードCondition Codesを書きます。
コンディションコード
コンディション コード (Condition Code) は、演算結果のステータスを示すために使用されるフラグ ビットのセットです。これらのフラグは通常、単一ビット レジスタ (前に紹介した %rax や %rbx などの追加の小さなレジスタと考えることができます) に格納されます。
- CF (Carry Flag) : 符号なし演算に使用されるキャリー フラグ。
- SF (Sign Flag) : 符号付き演算に使用される符号フラグ。
- ZF (Zero Flag) : 演算結果がゼロかどうかを示すゼロフラグ。
- OF (オーバーフロー フラグ) : 符号付き演算に使用されるオーバーフロー フラグ。
GDB デバッガでは、これらのフラグは次のように「eflags」という名前のレジスタとして出力されます。
eflags 0x246 [ PF ZF IF ] Z set, CSO clear
算術演算を実行する場合、通常、条件コードは暗黙的に (演算の追加結果として) 設定されます。addq Src, Dest
たとえば、加算演算を実行する演算を考えてみましょう t = a + b
。このプロセス中に、条件コードは次のルールに従って自動的に設定されます。
- CF (キャリーフラグ) : 最上位ビットがキャリーを生成したときにセットされます (符号なしオーバーフローを示します)。
- ZF(ゼロフラグ):
t == 0
その時に。 - SF(サインフラグ):
t < 0
その時点で(結果が否定的であることを示します)。 - OF (オーバーフロー フラグ) : 補数 (符号付き) オーバーフローが発生したときにセットされます。このような状況には次のようなものがあります。
- いつ
a > 0
、b > 0
そしてt < 0
いつ。 - いつ
a < 0
、b < 0
そしてt >= 0
いつ。
- いつ
比較命令(cmp)とテスト命令(test)
冒頭のサンプルコードでは、je の上に testl コマンドがあることがわかります。ここからは cmp と test につながります。
cmp
命令は2 つのオペランドの a
合計を 比較するために使用されますb
。その実行プロセスは次のとおりです。
- 計算します
b - a
(sub
命令と同じ)。 - 結果に基づいて条件コードを設定しますが、オペランドの値は変更しません
b
。
test
命令は2 つのオペランドの a
合計 をテストするために使用されますb
。その実行プロセスは次のとおりです。
- 計算します
b & a
(and
命令と同じ)。 - 結果に応じて条件コード (SF と ZF のみ) を設定しますが、オペランドの値は変更しません
b
。
jX命令 ジャンプ命令
ついにジェに来ました!je は実際には jX の一種であり、実際には cmp や test などの前のコマンドの結果に基づいて次のステップに進みます。jX 命令は通常、条件分岐やループなどの制御構造を実装するために使用されます。例えば:
je
:ゼロフラグ(ZF)がセットされたときにジャンプします。jne
:ゼロフラグ(ZF)がセットされていないときにジャンプします。jg
: 符号付き数値の比較結果が大きい場合にジャンプします。jl
: 符号付き数値の比較結果が小さい場合にジャンプします。
setXコマンド
setX と jX のロジックは非常に似ています。SetX命令は、条件コードの組み合わせに応じて対象レジスタの下位バイト(下位8ビット)を0または1に設定します。SetX 命令は、宛先レジスタの残りのバイトを変更しません。例えば:
sete
: ゼロフラグ (ZF) がセットされている場合はデスティネーションレジスタの下位バイトを 1 に設定し、それ以外の場合は 0 に設定します。setne
: ゼロフラグ (ZF) がセットされていない場合はデスティネーションレジスタの下位バイトを 1 に設定し、それ以外の場合は 0 を設定します。setg
: 符号付き数値の比較結果がより大きい場合、対象レジスタの下位バイトを 1 に設定し、それ以外の場合は 0 を設定します。setl
: 符号付き数値の比較結果が未満の場合、対象レジスタの下位バイトを 1 に設定し、それ以外の場合は 0 を設定します。
たとえば、2 つの整数 (レジスタ %rax
と に格納されている%rbx
) が等しいかどうかを比較し、その結果を %rcx
レジスタの下位バイトに格納したいとします (つまり 1
、等しい、0
等しくない)。次のようになります。
cmp %rax, %rbx ; 比较 %rax 和 %rbx 的值
sete %cl ; 如果它们相等(即零标志 ZF 设置),则将 %rcx 的低字节(%cl)设置为 1,否则设置为 0
少し異なりますが、setX の後にターゲット レジスタ ( や など %cl
) などの複数のパラメータが必要であることです%bl
。setX の引数は常にこれらの下位レジスタ (%al、%r8b など) です。前に述べたように、下位レジスタは実際には通常のレジスタの下位部分の名前です。たとえば、%al は実際にはレジスタ %rax の最後のバイトです。%eax は実際には %rax の最後の 4 バイトです。レジスタの数が異なるため、アプリケーションの操作コマンドには注意が必要です。もちろん、movzbl など、異なる番号のレジスタを一致させる特別なコマンドもあります。
movzbl %al, %eax
movzbl
これはx86アセンブリ命令であり、正式名称は「Move with Zero-Extend Byte to Long」です。この命令は、データのバイト (8 ビット) をソース オペランドからデスティネーション オペランドに移動し、それをロングワード (32 ビット) またはクワッドワード (オペランドのサイズに応じて 64 ビット) にゼロ拡張します。
さて、話はそれましたが、本題に戻ります。
条件付きコード ブロックの興味深い例:
long absdiff (long x, long y) {
long result;
if (x > y)
result = x-y;
else
result = y-x;
return result;
}
マシンコード:
absdiff:
movq %rdi, %rax # x
subq %rsi, %rax # result = x-y
movq %rsi, %rdx
subq %rdi, %rdx # eval = y-x
cmpq %rsi, %rdi # x:y
cmovle %rdx, %rax # if <=, result = eval
ret
一見すると、少し奇妙に思えますが、なぜ最初に cmp xy を実行し、その結果に従ってジャンプしなかったのでしょうか? なぜジャンプしなかったのでしょう。これは、ここでは条件付き移動命令 Conditional Move 命令が使用されており、両方の場合の値が事前に計算され、同じ要件を完了するために cmovle が使用されているためです。条件付き移動命令の主な利点は、命令ストリーム内の順次実行を中断しないことです。最新のプロセッサのパイプライン アーキテクチャでは、分岐命令 (ジャンプなど) によってパイプライン内の命令の順序が中断され、パフォーマンスが低下する可能性があります。条件付き移動命令は制御転送を必要としないため、プロセッサ パイプラインでより効率的です。movle における le の意味については、言うまでもなく上記の jX や setX と同じです。
もちろん、条件付き移動命令がすべての場合に最適な選択であるわけではありません。条件付き移動命令では、実行前に 2 つの値を計算する必要があります。これは、計算コストが高い場合、条件付き移動命令が最適な選択ではない可能性があることを意味します。別の例としては、計算が危険であるか、計算によってグローバル変数が変更される場合などが挙げられます。以下に、条件付き移動命令に適さないコード例をいくつか示します。
val = Test(x) ? Hard1(x) : Hard2(x); //计算量大
val = p ? *p : 0; //风险计算
val = Test(x) ? FunctionWithSideEffect1(x) : FunctionWithSideEffect2(x); //造成不必要的执行
switch ステートメントとループ ループ
最後に、スイッチとループループについて説明します。switch ステートメントの例を次に示します。
long switch_eg(long x, long y, long z)
{
long w = 1;
switch(x) {
// case statements...
}
return w;
}
マシンコードは次のとおりです。
switch_eg:
movq %rdx, %rcx
cmpq $6, %rdi
ja .L8
jmp *.L4(,%rdi,8)
上記のアセンブリ コードでは、直接ジャンプ ja.L8 と間接ジャンプ jmp の 2 種類のジャンプ命令が確認できます。
*.L4(,%rdi,8)。直接ジャンプについてはすでによく知られていますが、間接ジャンプは、switch ステートメント内のさまざまなコード ブロックのアドレスに対してマシンによって生成されるジャンプ テーブルです。多くの場合、これらのコード ブロックのアドレスは、ジャンプの開始位置。たとえば、このカテゴリのジャンプ テーブルのジャンプ テーブルは次のようになります。
.section .rodata
.align 8
.L4:
.quad .L8
.quad .L3
.quad .L5
.quad .L9
.quad .L8
.quad .L7
.quad .L7
この例では、ジャンプ テーブルの開始アドレスは です.L4
。テーブル内の各アドレスは 8 バイトを必要とするため、正しいオフセットを取得するにはx
( に格納されている) を 8 で乗算する必要があります。%rdi
次に、.L4
最初からオフセットを追加して、x*8
実際のジャンプ ターゲットを取得します。
関数ではswitch_eg
、間接ジャンプを使用して、の値に基づいてx
対応する分岐を選択します。ジャンプ テーブルにはターゲット アドレスが 7 つしか含まれていないため (値 0 ~ 6 の場合に対応) case
、間接ジャンプは 0 ≤ x ≤ 6 の範囲でのみ有効です。6 より大きい場合、プログラムはデフォルトの分岐を実行します。つまり、 に直接ジャンプします。.L4
x
x
.L8(对应 ja .L8)
もちろん、なぜ L8 が x=0 に対応し、L3 が x=1 に対応するかについては、L8 の機械語コードと元のコードを比較することでわかります。。
ループ ループ
C言語にはループの書き方がたくさんありますが、while-do、do-while、forなどどれを使っても、機械語に変換するとほとんど同じです。ループの種類に関係なく、ループには「init」初期化、「条件」、「本文」、「更新」などのいくつかの部分があり、for、while、または goto ステートメントで記述するのと同じ種類のロジックが含まれます。と結果として得られるマシンのコードは同じであると推定されます。詳しいことは書かないでね〜