2025-10-16 05:10:04
諏訪子
lowlevel
assembly

【アセンブリ言語】if-else及びswitchケースの説明

アセンブリ言語に益々魅了されていくので、此の言語の基本をさらに提供し続けます。
此の記事は、forループの記事よりも短くなります。
今回は復習する内容がないからです。

先ず、C言語で簡単なコードを書いてみましょう:

#include <stdio.h>

int main() {
  int i = 2;
  if (i == 2) {
    printf("i = 2\n");
  } else if (i == 3) {
    printf("i = 3\n");
  } else {
    printf("i = n\n");
  }

  return 0;
}
$ cc ifelse.c -o ifelseC
$ ./ifelseC
i = 2
$ file ifelseC
ifelseC: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD), statically linked, for FreeBSD 14.3, FreeBSD-style, with debug_info, not stripped
$ ls -thal ifelseC
-rwxr-xr-x  1 suwako suwako  3.4M 10月 16 02:35 ifelseC
$ strip ifelseC
$ file ifelseC
ifelseC: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD), statically linked, for FreeBSD 14.3, FreeBSD-style, stripped
$ ls -thal ifelseC
-rwxr-xr-x  1 suwako suwako  603K 10月 16 02:35 ifelseC
$ time ./ifelseC
i = 2
./ifelseC  0.00s user 0.00s system 69% cpu 0.001 total

此れをアセンブリ言語に翻訳してみましょう。
お浚いですが、GhostBSD(デスクトップ用のFreeBSD)を使用します。
Linuxをお使いの方は、syscall番号(書き込みは1、終了は60)を変更する必要があります。

section .data
  i: dd 2                 ; int i = 2;

section .rodata
  itwo:   db 'i = 2', 10  ; 10 = '\n'
  ithree: db 'i = 3', 10
  iany:   db 'i = n', 10

section .text
  global _start

_start:
  cmp dword [i], 2        ; if (i == 2)
  jne elif                ; i != 2 の場合、elif へジャンプ
  mov rax, 4              ; FreeBSDでの書込のシステムコール番号
  mov rdi, 1              ; ファイルディスクリプター1(標準出力)
  mov rsi, itwo           ; バッファー "i = 2\n"
  mov rdx, 6              ; "i = 2\n" の長さ = 6
  syscall                 ; カーネルを呼び出す
  jmp endif               ; 末尾へジャンプ

elif:
  cmp dword [i], 3        ; else if (i == 3)
  jne else                ; i != 3 の場合、else へジャンプ
  mov rax, 4              ; FreeBSDでの書込のシステムコール番号
  mov rdi, 1              ; ファイルディスクリプター1(標準出力)
  mov rsi, ithree         ; バッファー "i = 3\n"
  mov rdx, 6              ; "i = 3\n" の長さ = 6
  syscall                 ; カーネルを呼び出す
  jmp endif               ; 末尾へジャンプ

else:
  mov rax, 4              ; FreeBSDでの書込のシステムコール番号
  mov rdi, 1              ; ファイルディスクリプター1(標準出力)
  mov rsi, iany           ; バッファー "i = n\n"
  mov rdx, 6              ; "i = n\n" の長さ = 6
  syscall                 ; カーネルを呼び出す

endif:
  mov rax, 1              ; FreeBSDでの終了のシステムコール番号
  mov rdi, 0              ; 終了ステータス 0 (成功) = return 0;
  syscall                 ; カーネルを呼び出す
$ nasm -f elf64 ifelse.asm -o ifelseASM.o
$ ld ifelseASM.o -o ifelseASM
$ ./ifelseASM
i = 2
$ file ifelseASM
ifelseASM: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD), statically linked, not stripped
$ ls -thal ifelseASM
-rwxr-xr-x  1 suwako suwako  1.5K 10月 16 02:40 ifelseASM
$ strip ifelseASM
$ file ifelseASM
ifelseASM: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD), statically linked, stripped
$ ls -thal ifelseASM
-rwxr-xr-x  1 suwako suwako  1.0K 10月 16 02:40 ifelseASM
$ time ./ifelseASM
i = 2
./ifelseASM  0.00s user 0.00s system 56% cpu 0.001 total

最初に気づくのは、C言語では「iが2に等しいならこれをする」という見た目ですが、アセンブリでは「iは2に等しいか?等しくなければこのコードをスキップして次のチェックへ」という見た目です。
此れがC言語がどれだけ人間にとって理解し易く抽象化されているかを示しています。
勿論、jne(等しくない場合にジャンプ)ではなくje(等しい場合にジャンプ)を使う事もで出来すが、等しくないかを比較する方が実際には速いです。

視点を変えると、i = 0の場合、elseを実行するには2回のジャンプが必要です。
先ずifを比較し、次にelifを比較し、其の後は「残った物を実行するだけ」で、ジャンプは2回だけです。
然し、jeを使用すると、先ずifを比較し、jmpを実行し、elifを比較し、jmpを実行し、そして「残ったものを実行する」となり、ジャンプは4回になります。

コードで表すと:

  cmp dword [i], 2
  jne elif

は次の様になります:

  cmp dword [i], 2
  je if
  jmp elif

此の操作だけで1つの余分な命令ができてしまい、パフォーマンスに悪影響です。
特に大きなif-elseif-elseブロックがある場合は特にそうです。

又、ifelifの部分の後に、末尾へジャンプしている事に気づくでしょう。
jmp endifを省略すると、コードが続行されてしまい、出力は次の様になります:

i = 2
i = 3
i = n

switch文の方がパフォーマンスが良いのではないかと思われるかもしんね?
そうです!
此の場合、コード行数は多くなりますが、ジャンプの回数は少なく、特に何も一致しない場合に効果的です。
if-elseでは、最初の条件が一致する場合にパフォーマンスが良いです。

本当は別の記事に取っておきたかったのですが、簡単にやってみましょう。

section .data
  i: dd 4                   ; int i = 2;

section .rodata
  itwo: db 'i = 2', 10
  ithree: db 'i = 3', 10
  iany: db 'i = n', 10

section .text
  global _start

_start:
  mov eax, dword [i]        ; eax = i (ジャンプテーブル用 32-bit)

  ; 範囲チェック
  cmp eax, 2                ; eax = 2 の場合
  jb case_default           ; i < 2 の場合、case_default へジャンプ
  cmp eax, 3                ; eax = 3 の場合
  ja case_default           ; i > 3 の場合、case_default へジャンプ

  ; テーブルインデックス計算 (i - 2) * 8 バイト
  sub eax, 2                ; eax = i - 2 (0 又は 1)
  shl rax, 3                ; rax *= 8 (ポインタサイズ)

  ; ジャンプテーブルのベースアドレス読み込み
  lea rbx, [rel jumptable]  ; rbx = &jumptable[0]
  add rbx, rax              ; rbx = &jumptable[index]
  jmp [rbx]                 ; ハンドラーへの間接ジャンプ!

; ケースハンドラー
case2:
  mov rax, 4                ; FreeBSDでの書込のシステムコール番号
  mov rdi, 1                ; ファイルディスクリプター1(標準出力)
  mov rsi, itwo             ; バッファー "i = 2\n"
  mov rdx, 6                ; "i = 2\n" の長さ = 6
  syscall                   ; カーネルを呼び出す
  jmp endswitch             ; 末尾へジャンプ

case3:
  mov rax, 4                ; FreeBSDでの書込のシステムコール番号
  mov rdi, 1                ; ファイルディスクリプター1(標準出力)
  mov rsi, ithree           ; バッファー "i = 3\n"
  mov rdx, 6                ; "i = 3\n" の長さ = 6
  syscall                   ; カーネルを呼び出す
  jmp endswitch             ; 末尾へジャンプ

case_default:
  mov rax, 4                ; FreeBSDでの書込のシステムコール番号
  mov rdi, 1                ; ファイルディスクリプター1(標準出力)
  mov rsi, iany             ; バッファー "i = n\n"
  mov rdx, 6                ; "i = n\n" の長さ = 6
  syscall                   ; カーネルを呼び出す

endswitch:
  mov rax, 1                ; FreeBSDでの終了のシステムコール番号
  mov rdi, 0                ; 終了ステータス 0 (成功) = return 0;
  syscall                   ; カーネルを呼び出す

jumptable:
  dq case2                  ; インデックス 0: i == 2
  dq case3                  ; インデックス 1: i == 3
$ nasm -f elf64 switch.asm -o switchASM.o
$ ld switchASM.o -o switchASM
$ strip switchASM
$ file switchASM
switchASM: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD), statically linked, stripped
$ ls -thal switchASM
-rwxr-xr-x  1 suwako suwako  1.1K 10月 16 02:45 switchASM
$ time ./switchASM
i = 2
./switchASM  0.00s user 0.00s system 55% cpu 0.001 total

C言語ではこうなります:

#include <stdio.h>

int main() {
  int i = 2;
  switch (i) {
    case 2:
      printf("i = 2\n");
      break;
    case 3:
      printf("i = 3\n");
      break;
    default:
      printf("i = n\n");
      break;
  }
  return 0;
}
$ cc switch.c -o switchC -static
$ strip switchC
$ file switchC
switchC: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD), statically linked, for FreeBSD 14.3, FreeBSD-style, stripped
$ ls -thal switchC
-rwxr-xr-x  1 suwako suwako  603K 10月 16 02:47 switchC
$ time ./switchC
i = 2
./switchC  0.00s user 0.00s system 64% cpu 0.001 total

jbjaの説明:jbは「此の値より前へジャンプ」で、i < 2を意味します。jaは「この値より後へジャンプ」で、i > 3を意味します。

shlは「shift left(左シフト)」で、2のべき乗で乗算します:shl rax, 3 = Cのrax *= 8。比較の為に、imulは「signed multiply(符号付き乗算)」:imul rax, 8 = Cのrax *= 8。其の為、shlは2サイクル速く、imulは3サイクル、shlは1サイクルで実行されます。更に、shlではゼロ拡張(movzx rax, al)も不要です。

そしてleaは「load effective address(有効アドレス読み込み)」で、派手なポインタ演算です。データを読み込まずにメモリアドレスを計算するので、CやC++のポインタと同様にアドレスだけを参照します。

以上