switch()/case/default
source
#include <stdio.h>
void f (int a)
{
switch (a)
{
case 0: printf("zero\n"); break;
case 1: printf("one\n"); break;
case 2: printf("two\n"); break;
default: printf("something unknown\n"); break;
}
}
int main()
{
f(2);
}
x86 MSVC
$SG5332 DB 'zero', 0aH, 00H
$SG5334 DB 'one', 0aH, 00H
$SG5336 DB 'two', 0aH, 00H
$SG5338 DB 'something unknown', 0aH, 00H
EXTRN ___acrt_iob_func:PROC
EXTRN ___stdio_common_vfprintf:PROC
_main PROC
push ebp
mov ebp, esp
push 2
call ?f@@YAXH@Z
add esp, 4
xor eax, eax
pop ebp
ret 0
_main ENDP
tv64 = -4 ; size = 4
_a$ = 8 ; size = 4
f PROC
push ebp
mov ebp, esp
push ecx
mov eax, DWORD PTR _a$[ebp]
mov DWORD PTR tv64[ebp], eax
cmp DWORD PTR tv64[ebp], 0
je SHORT $LN4@f
cmp DWORD PTR tv64[ebp], 1
je SHORT $LN5@f
cmp DWORD PTR tv64[ebp], 2
je SHORT $LN6@f
jmp SHORT $LN7@f
$LN4@f:
push OFFSET $SG5332
call _printf
add esp, 4
jmp SHORT $LN1@f
$LN5@f:
push OFFSET $SG5334
call _printf
add esp, 4
jmp SHORT $LN1@f
$LN6@f:
push OFFSET $SG5336
call _printf
add esp, 4
jmp SHORT $LN1@f
$LN7@f:
push OFFSET $SG5338
call _printf
add esp, 4
$LN1@f:
mov esp, ebp
pop ebp
ret 0
f ENDP
x86 MSVC (최적화: 옵션 /Ox)
$SG5332 DB 'zero', 0aH, 00H
$SG5334 DB 'one', 0aH, 00H
$SG5336 DB 'two', 0aH, 00H
$SG5338 DB 'something unknown', 0aH, 00H
EXTRN ___acrt_iob_func:PROC
EXTRN ___stdio_common_vfprintf:PROC
_main PROC
push OFFSET $SG5336
call _printf
add esp, 4
xor eax, eax
ret 0
_main ENDP
_a$ = 8 ; size = 4
f PROC
mov eax, DWORD PTR _a$[esp-4]
sub eax, 0
je SHORT $LN4@f
sub eax, 1
je SHORT $LN5@f
sub eax, 1
je SHORT $LN6@f
mov DWORD PTR _a$[esp-4], OFFSET $SG5338
jmp _printf
$LN6@f:
mov DWORD PTR _a$[esp-4], OFFSET $SG5336
jmp _printf
$LN5@f:
mov DWORD PTR _a$[esp-4], OFFSET $SG5334
jmp _printf
$LN4@f:
mov DWORD PTR _a$[esp-4], OFFSET $SG5332
jmp _printf
f ENDP
설명
이 코드에서는 몇 가지 이해하기 난해한 트릭을 배울 수 있다.
우선 a
변수의 값을 EAX
에 저장한 후 여기서 0을 뺀다. 이상해 보이지만 EAX
레지스터의 값이 0이었는지 검사하기 위함이다. EAX
가 0이었다면 ZF
플래그가 설정되며 첫 번째 조건부 점프 JE
가 실행되었을 것이다.
두 번째로 이상한 부분은 printf()
호출이다. 문자열 포인터를 변수 a
에 저장한 후 CALL
이 아니라 JMP
를 이용해서 printf()
를 호출하고 있다. 이에 대한 설명은 다음과 같다.
호출자는 스택에 값을 푸시하고 CALL
을 이용해서 함수를 호출한다. CALL
자신은 리턴 주소를 스택에 푸시하고 호출된 함수의 주소로 무조건적으로 점프한다. 호출된 함수의 스택 레이아웃은 함수 실행 내내 다음과 같다.
- ESP: 리턴 주소를 가리킴
- ESP + 4: 변수 a를 가리킴
예제 코드에서 printf()
를 호출할 때 필요한 스택 레이아웃도 문자열을 가리키는 printf()
의 첫 번째 인자만 제외하면 이와 똑같다. 코드는 마치 함수 f()
를 애초에 호출하지 않고 바로 printf()
를 호출한 것처럼 함수의 첫 번째 인자를 문자열의 주소로 대체한 다음 printf()
로 점프한다. printf()
는 문자열을 stdout
으로 출력한 후 RET
명령어를 실행한다. 이 명령어는 스택에서 리턴 주소를 꺼낸 후 제어 흐름을 f()
가 아닌 f()
의 호출자로 넘긴다. 결과적으로 f()
함수의 끝부분은 건너뛰게 된다.