rflagsという特別なレジスタがあります。
このレジスタの各ビットは、さまざまな条件を表すフラグとして機能します。
その一部を以下に示します。
| 名前 | 記号 | ビット |
|---|---|---|
| キャリー | CF | 0 |
| ゼロ | ZF | 6 |
| 符号 | SF | 7 |
| オーバーフロー | OF | 11 |
完全な一覧については、Intelのマニュアルを参照してください。
rflags内のフラグは_直接変更されません_。
代わりに、さまざまな命令によって設定されます。
たとえば、ZFは、多くの算術演算やビット演算で結果がゼロのときに設定されます。
条件を判定するためによく使われる命令の1つがcmpです。
この命令は2つのオペランドを受け取り、フラグを更新しますが、オペランド自体は変更しません。
cmp命令は、第1オペランドから第2オペランドを引き、その結果に応じてフラグを設定します。
Aを第1オペランド、Bを第2オペランドとすると、次のようになります。
| フラグ | 設定される条件 |
|---|---|
| CF | A < B(符号なし) |
| ZF | A == B |
| SF | A < B(符号あり、オーバーフローなし) |
| OF | 符号ありの減算でオーバーフロー |
デフォルトでは、x86-64のコードは上から下へ順番に実行されます。
しかし、この動作を変える必要がある場面は多くあります。 たとえば、条件に応じて別の命令の組を実行する場合です。
高水準言語では、通常、if...elseのような抽象化を使ってこれを行います。
しかし、x86-64にはそのようなものはありません。
代わりに、x86-64には、実行をコードの別の場所へ移す命令があります。
これをbranchingと呼びます。
このような命令はすでに2つ見てきました。callとretです。
関数が呼び出されると、実行は呼び出し元から呼び出された関数へ移ります。 そして、戻るときには、実行は呼び出し元へ戻ります。
retが見つからない場合、実行はある関数から次の関数へフォールスルーします。
これは、コードの流れを最適化するために使われることがあります。
jmp命令は、プログラムの実行を無条件にコードの別の位置へ移します。 この命令の唯一のオペランドは、実行が続く位置のアドレスを持つラベルです。
たとえば、次の関数を考えてみましょう。
fn:
mov rax, 5
jmp end
add rax, 10
end:
ret
fnが呼び出されると、実行はmov rax, 5から始まります。
これにより、その時点でraxの値は5になります。
次の命令はjmp endで、実行をラベルendへ移します。
endの次にある命令はretで、実行を呼び出し元の関数へ戻します。
add rax, 10はjmp endの後、endの前に位置しているため、決して実行されないことに注意してください。
fnが戻るときのraxの値は5です。
jcc命令群は、特定の条件が満たされた場合にのみ、プログラムの実行を別の位置へ移します。 それ以外の場合、実行は順番に続きます。
各条件は、rflags内の1つ以上のフラグに対応します。
jccのバリアントの中には、フラグがセットされていることを調べるものもあれば、クリアされていることを調べるものもあります。
jccのccはそのままの文字ではなく、調べるフラグに対応する特定の接尾辞を指します。
接尾辞は多くあり、その多くは別の接尾辞と同じ条件を調べます。 その一部はフラグを直接参照するため、特定のフラグがセットされていると、命令はラベルへジャンプします。
| 接尾辞 | ジャンプする条件 |
|---|---|
| z | ZF == 1 |
| c | CF == 1 |
| s | SF == 1 |
| o | OF == 1 |
他の多くは、cmp命令における意味を表すために選ばれています。
たとえば、次のとおりです。
| 命令 | 接尾辞 | ジャンプする条件 |
|---|---|---|
| cmp A, B | e | A == B |
| cmp A, B | l | A < B(符号あり) |
| cmp A, B | b | A < B(符号なし) |
| cmp A, B | g | A > B(符号あり) |
| cmp A, B | a | A > B(符号なし) |
l、b、g、aの後にeを付けると、条件に等号を含めることができます。
cmp rcx, r8
jge two ; this jumps to 'two' if rcx is greater than, or equal to, r8 in a signed comparison
jbe two ; this jumps to 'two' if rcx is lesser than, or equal to, r8 in an unsigned comparison
すべての接尾辞には、反対の動作を調べるバリアントがあります。
構文は同じですが、接尾辞の前にnが付きます。
たとえば、jnzはZFがセットされていないときにジャンプします。
同様に、jnaeはAがB以上ではないときにジャンプします(AとBは符号なし整数として解釈されます)。
一部の接尾辞は、同じ条件の別名です。
たとえば、jz(接尾辞z、ZF用)とje(接尾辞e、等しい用)は、どちらもZFがセットされているときにジャンプします。
これは、cmpでは減算の結果がゼロ、つまり2つのオペランドが等しいときにZFがセットされるためです。
一方、他の接尾辞は複数のフラグの組み合わせを調べるため、単一のフラグ接尾辞で直接置き換えることはできません。
比較の意味をよりよく表す接尾辞を選びましょう。
ラベルはソースファイル全体から見えます。関数内だけのローカルなものではありません。 そのため、同じラベル名を再利用することはできません。
ローカルラベルの動作を模倣するために、NASMには、先頭にピリオド(.)を付けて宣言するラベルの特別な記法があります。
この記法では、直前のピリオドなしラベルの名前を暗黙的に含むラベルを定義します。
section .text
fn1:
...
.example: ; this is fn1.example
...
ret
fn2:
...
.example: ; this is fn2.example
...
ret
完全なラベル名を使えば、コードのどこからでもこのラベルへジャンプできます。たとえば、jmp fn1.exampleです。
ただし、ピリオドから始まるラベルの部分を使ったジャンプは、上位の関数の_内側_のラベルに対して行われます。
たとえば、.exampleはその関数にローカルであるかのように動作します。
section .text
fn1:
...
.example:
...
jmp .example ; this jumps to fn1.example
fn2:
...
.example:
...
jmp .example ; this jumps to fn2.example
関数内のピリオドなしラベルは、実際には別の関数を定義することに注意してください。
section .text
fn1:
...
.example: ; this is 'fn1.example'
...
non_dotted:
...
.example: ; this is 'non_dotted.example'
...
ret