現在のブログ
ゲーム開発ブログ (2025年~) Gamedev Blog (2025~)
レガシーブログ
テクノロジーブログ (2018~2024年) リリースノート (2023~2025年) MeatBSD (2024年)
【Assembly Language】Explaining The if-else and switch Cases
I'm becoming increasingly fascinated with assembly language, so I'll continue to provide more basics of this language.
This article will be shorter than the for loop article.
This is because there's no content to review this time.
First, let's write a simple code in 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
Let's translate this into assembly language.
Just a reminder: I'm using GhostBSD (FreeBSD for desktops).
Linux users need to change the syscall numbers (write is 1, exit is 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 ; if i != 2, jump to elif
mov rax, 4 ; FreeBSD write syscall number
mov rdi, 1 ; file descriptor 1 (stdout)
mov rsi, itwo ; buffer "i = 2\n"
mov rdx, 6 ; length of "i = 2\n" = 6
syscall ; call kernel
jmp endif ; jump to end
elif:
cmp dword [i], 3 ; else if (i == 3)
jne else ; if i != 3, jump to else
mov rax, 4 ; FreeBSD write syscall number
mov rdi, 1 ; file descriptor 1 (stdout)
mov rsi, ithree ; buffer "i = 3\n"
mov rdx, 6 ; length of "i = 3\n" = 6
syscall ; call kernel
jmp endif ; jump to end
else:
mov rax, 4 ; FreeBSD write syscall number
mov rdi, 1 ; file descriptor 1 (stdout)
mov rsi, iany ; buffer "i = n\n"
mov rdx, 6 ; length of "i = n\n" = 6
syscall ; call kernel
endif:
mov rax, 1 ; FreeBSD exit syscall number
mov rdi, 0 ; exit status 0 (success) = return 0;
syscall ; call kernel
$ 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
The first thing you'll notice is that in C, it looks like "if i equals 2, do this", but in assembly, it looks like "Is i equal to 2? If not, skip this code and go to the next check".
This shows how much C abstracts things to make them easier for humans to understand.
Of course, you could use je (jump if equal) instead of jne (jump if not equal), but comparing for inequality is actually faster.
From another perspective, when i = 0, executing the else requires 2 jumps.
First compare if, then compare elif, and then "just execute what's left" with only 2 jumps.
However, using je would mean: first compare if, execute jmp, compare elif, execute jmp, and then "execute what's left", resulting in 4 jumps.
In code:
cmp dword [i], 2
jne elif
Becomes:
cmp dword [i], 2
je if
jmp elif
This single operation creates one extra instruction, negatively impacting performance.
This is especially true for large if-elseif-else blocks.
Also, notice that after the if and elif sections, we're jumping to the end.
If you omit jmp endif, the code continues and the output becomes:
i = 2
i = 3
i = n
You might think a switch statement would have better performance?
You're right!
In this case, the code lines increase, but the number of jumps decreases, especially when nothing matches.
if-else performs best when the first condition matches.
I was going to save this for another article, but let's do it quickly.
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 (for jump table, 32-bit)
; Range check
cmp eax, 2 ; if eax = 2
jb case_default ; if i < 2, jump to case_default
cmp eax, 3 ; if eax = 3
ja case_default ; if i > 3, jump to case_default
; Calculate table index (i - 2) * 8 bytes
sub eax, 2 ; eax = i - 2 (0 or 1)
shl rax, 3 ; rax *= 8 (pointer size)
; Load jump table base address
lea rbx, [rel jumptable] ; rbx = &jumptable[0]
add rbx, rax ; rbx = &jumptable[index]
jmp [rbx] ; indirect jump to handler!
; Case handlers
case2:
mov rax, 4 ; FreeBSD write syscall number
mov rdi, 1 ; file descriptor 1 (stdout)
mov rsi, itwo ; buffer "i = 2\n"
mov rdx, 6 ; length of "i = 2\n" = 6
syscall ; call kernel
jmp endswitch ; jump to end
case3:
mov rax, 4 ; FreeBSD write syscall number
mov rdi, 1 ; file descriptor 1 (stdout)
mov rsi, ithree ; buffer "i = 3\n"
mov rdx, 6 ; length of "i = 3\n" = 6
syscall ; call kernel
jmp endswitch ; jump to end
case_default:
mov rax, 4 ; FreeBSD write syscall number
mov rdi, 1 ; file descriptor 1 (stdout)
mov rsi, iany ; buffer "i = n\n"
mov rdx, 6 ; length of "i = n\n" = 6
syscall ; call kernel
endswitch:
mov rax, 1 ; FreeBSD exit syscall number
mov rdi, 0 ; exit status 0 (success) = return 0;
syscall ; call kernel
jumptable:
dq case2 ; index 0: i == 2
dq case3 ; index 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
In C, it would be:
#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
Explanation of jb and ja: jb means "jump below this value" (i < 2).ja means "jump above this value" (i > 3).
shl is "shift left" and multiplies by powers of 2: shl rax, 3 = C's rax *= 8.
For comparison, imul is "signed multiply": imul rax, 8 = C's rax *= 8.
Therefore, shl is 2 cycles faster; imul takes 3 cycles, shl takes 1 cycle.
Additionally, shl doesn't require zero extension (movzx rax, al).
And lea is "load effective address" for fancy pointer arithmetic.
It calculates the memory address without loading data, just like referencing addresses in C/C++ pointers.
That's all