요약

고급 언어의 “만약 ~이면 이 묶음을 해라”에는 묶음을 감싸는 괄호가 있다. 기계에게는 괄호가 없고 “저 줄로 가라”만 있다. 그래서 “~이면 해라”를 “~이 아니면 이 묶음을 건너뛰어라”로 뒤집어 쓴다. 이 한 가지 요령으로 갈림, 여러 조건의 묶음, 되풀이를 모두 옮길 수 있다. 조건을 뒤집을 때 “크다”의 반대를 “작다”로 쓰면 같을 때 틀리고, 수가 부호 있는지에 맞는 점프를 고르지 않으면 음수에서 틀린다.

예시로 보기

C++나 Java로는 이렇게 쓴다.

if( op1 == op2 )
    X = 1;
else
    X = 2;

어셈블리어에는 if도 중괄호도 없다. 있는 것은 CMP와 조건 점프, 그리고 레이블뿐이다. 코드는 위에서 아래로 흐르니, “참일 때 할 일”을 바로 아래에 두고 거짓이면 그것을 뛰어넘게 한다.

    mov eax,op1
    cmp eax,op2
    jne L1          ; 다르면(조건이 거짓이면) else로
    mov X,1         ; 참일 때 할 일
    jmp L2          ; else를 건너뛴다
L1: mov X,2         ; 거짓일 때 할 일
L2:

if의 조건은 ==인데 점프는 jne(다르면)다. 조건을 뒤집었다. 참일 때 할 일 끝에는 jmp L2가 있다. 없으면 X = 1을 한 뒤 아래로 흘러 X = 2까지 해 버린다.

flowchart TD
    C{"op1과 op2가 같은가? (cmp)"} -->|"다르다 (jne L1)"| E["L1: X에 2를 넣는다"]
    C -->|"같다 (그냥 아래로)"| T["X에 1을 넣는다"]
    T -->|"jmp L2"| N["L2: 다음 문장"]
    E --> N
검증: 이 문서의 어셈블리 코드 여덟 개를 흉내기로 돌려 같은 뜻의 파이썬 if·while과 대조(경계값 9~10개의 모든 조합), AND 두 구현이 같은 답을 내고 명령 수가 7개와 5개인 것, 단락 평가에서 둘째 CMP가 실행되지 않는 것, 뒤집은 조건 표, 카드 C2·C4 — 34_conditional-structures_verify.py (파이썬 흉내로 확인. 실제 MASM으로 돌린 것이 아니다)

옮기는 법

조건 뒤집기

“조건이 거짓이면 건너뛰어라”를 쓰려면 조건의 반대로 뛰는 점프가 필요하다1.

고급 언어의 조건 거짓일 때 뛰는 점프 (부호 없는 수) 거짓일 때 뛰는 점프 (부호 있는 수)
a == b jne jne
a != b je je
a > b jbe jle
a >= b jb jl
a < b jae jge
a <= b ja jg

>의 반대는 <가 아니라 <=다. 같을 때가 어느 쪽에 드는지가 달라진다. 어느 열을 쓸지는 부호 있는 비교 점프와 부호 없는 비교 점프를 따른다.

IF

else가 없는 if는 몸통을 건너뛰는 점프 하나면 된다. 값이 모두 부호 없는 수일 때다.

if( ebx <= ecx )
{
    eax = 5;
    edx = 6;
}
    cmp ebx,ecx
    ja  next        ; ebx > ecx면(조건이 거짓이면) 건너뛴다
    mov eax,5
    mov edx,6
next:

else가 있으면 두 묶음 가운데 어느 것을 위에 둘지 고를 수 있다. 아래 풀이는 조건을 뒤집지 않고 “참이면 아래쪽 묶음으로 가라”로 쓴다. 값이 32비트 부호 있는 정수일 때다.

if( var1 <= var2 )
    var3 = 10;
else
{
    var3 = 6;
    var4 = 7;
}
    mov eax,var1
    cmp eax,var2
    jle L1          ; 참이면 L1으로
    mov var3,6      ; 거짓일 때 할 일(else)이 위에 왔다
    mov var4,7
    jmp L2
L1: mov var3,10     ; 참일 때 할 일
L2:

조건을 뒤집어(jg L1) 참일 때 할 일을 위에 두어도 같은 답이 나온다. 이런 문제에는 맞는 풀이가 여럿이다. cmp var1,var2라고 바로 쓰지 않고 EAX를 거치는 것은 CMP도 두 피연산자가 모두 메모리일 수는 없기 때문이다(데이터 전송 명령)1.

AND로 묶인 조건

고급 언어는 A AND B에서 A가 거짓이면 B를 아예 따지지 않는다. 이것을 단락 평가(short-circuit evaluation)라 부른다. 하나만 거짓이어도 전체가 거짓이기 때문이다.

if (al > bl) AND (bl > cl)
    X = 1;

한 가지 구현은 조건을 뒤집지 않는다. 참이면 다음 검사로 가고, 아니면 jmp로 빠져나간다.

    cmp al,bl       ; 첫째 조건
    ja  L1
    jmp next
L1:
    cmp bl,cl       ; 둘째 조건
    ja  L2
    jmp next
L2:                 ; 둘 다 참
    mov X,1
next:

첫째 조건을 뒤집으면 ja L1 / jmp next 두 줄이 jbe next 한 줄이 된다. 참이면 아무 데도 안 뛰고 그대로 아래 검사로 흘러 내려간다(fall through).

    cmp al,bl       ; 첫째 조건
    jbe next        ; 거짓이면 끝
    cmp bl,cl       ; 둘째 조건
    jbe next        ; 거짓이면 끝
    mov X,1         ; 둘 다 참
next:

명령이 일곱 개에서 다섯 개로 줄었다. \(2 \div 7 \approx 0.29\), 곧 코드가 29% 줄었다.

값이 부호 없는 수일 때 다음 코드를 옮겨 보라.

if( ebx <= ecx && ecx > edx )
{
    eax = 5;
    edx = 6;
}
답
    cmp ebx,ecx
    ja  next        ; ebx <= ecx가 거짓이면 끝
    cmp ecx,edx
    jbe next        ; ecx > edx가 거짓이면 끝
    mov eax,5
    mov edx,6
next:

OR로 묶인 조건

A OR B에서는 A가 참이면 B를 따지지 않는다. 하나만 참이어도 전체가 참이기 때문이다.

if (al > bl) OR (bl > cl)
    X = 1;
    cmp al,bl       ; AL > BL인가?
    ja  L1          ; 그렇다: 바로 몸통으로
    cmp bl,cl       ; 아니다: BL > CL인가?
    jbe next        ; 아니다: 몸통을 건너뛴다
L1: mov X,1
next:

첫째 조건은 뒤집지 않고 “참이면 몸통으로” 뛴다. 마지막 조건만 뒤집어 “거짓이면 건너뛰어라”로 쓴다.

AND와 OR의 규칙을 나란히 놓으면 이렇다1.

  마지막이 아닌 조건 마지막 조건
AND 뒤집어서, 거짓이면 밖으로 뒤집어서, 거짓이면 밖으로
OR 그대로, 참이면 몸통으로 뒤집어서, 거짓이면 밖으로

WHILE

while은 if 하나, 그 뒤의 몸통, 그 뒤에 맨 위로 돌아가는 무조건 점프다.

while( eax < ebx)
    eax = eax + 1;
top: cmp eax,ebx    ; 조건 검사
     jae next       ; 거짓이면 루프를 나간다
     inc eax        ; 몸통
     jmp top        ; 되풀이
next:
flowchart TD
    C{"top: eax가 ebx보다 작은가? (cmp)"} -->|"아니다 (jae next)"| N["next: 루프 뒤의 문장"]
    C -->|"그렇다 (그냥 아래로)"| B["몸통: eax를 1 늘린다"]
    B -->|"jmp top"| C

값이 부호 없는 32비트 정수일 때 다음 코드를 옮겨 보라.

while( ebx <= val1)
{
    ebx = ebx + 5;
    val1 = val1 - 1
}
답
top: cmp ebx,val1   ; 조건 검사
     ja  next       ; 거짓이면 루프를 나간다
     add ebx,5      ; 몸통
     dec val1
     jmp top        ; 되풀이
next:

EBX = 0, val1 = 10으로 시작하면 (5, 9), (10, 8)을 거쳐 EBX = 10, val1 = 8에서 멈춘다.

경계 사례. 위 루프를 EBX = 0, val1 = 0으로 시작하면 한 바퀴 뒤 val1이 0에서 1 줄어 FFFFFFFFh가 된다. 부호 없는 수로는 가장 큰 값이라 조건이 계속 참이다. 고급 언어 코드를 그대로 옮긴 것이므로 틀린 번역은 아니지만, 부호 없는 수를 0 아래로 내리는 루프는 이렇게 된다1.

뒤집어도 같은 프로그램인 까닭

주장. if (C) { S }를 “C의 반대 조건으로 next로 뛰는 점프, 그 아래 S, 그 아래 next:“로 옮기면, C가 참일 때만 S가 실행된다1.

증명 펼치기

C의 반대 조건으로 뛰는 점프를 Jnot이라 하자.

  1. 위 표의 짝(je와 jne, ja와 jbe, jae와 jb, jg와 jle, jge와 jl)은 어떤 플래그 값에서도 정확히 하나만 뛴다. — 플래그 네 개의 16가지 조합을 모두 넣어 확인
  2. C가 참이면 Jnot은 뛰지 않는다. 뛰지 않은 조건 점프는 바로 아래 명령으로 간다. 그 아래가 S이므로 S가 실행되고, 이어서 next에 닿는다. — 조건 점프의 정의
  3. C가 거짓이면 Jnot이 next로 뛴다. S의 명령들은 점프와 next 사이에 있어 하나도 실행되지 않는다. — 점프는 EIP를 목적지로 바꾼다
  4. 두 경우 모두 끝나면 next에 있다. 고급 언어의 if가 끝난 자리와 같다. ∎

스스로 설명해 보기

1. 1단계에서 ja와 jbe가 “정확히 하나만 뛴다”는 것은 두 명령의 플래그 조건에서 어떻게 나오는가

ja는 CF = 0이고 ZF = 0일 때, jbe는 CF = 1이거나 ZF = 1일 때 뛴다. 뒤의 것은 앞의 것의 부정이다(“둘 다 0”의 반대는 “하나라도 1”). 그래서 어떤 플래그 값에서도 둘 중 하나만 참이다.

2. if-else에서는 왜 참일 때 할 일 끝에 jmp가 더 필요한가

2단계에서 S가 끝나면 그대로 아래로 흐른다. else가 없으면 아래가 next라 괜찮다. else가 있으면 아래가 else의 몸통이라, 건너뛰는 jmp가 없으면 두 묶음을 다 실행한다.

3. AND에서 조건마다 같은 next로 뛰어도 되는 까닭

조건 하나라도 거짓이면 전체가 거짓이고, 그때 할 일은 조건이 몇째였든 “몸통을 건너뛴다”로 같다. 그래서 목적지가 하나다.

이 기법의 핵심 아이디어는?

코드는 가만두면 아래로 흐른다. 이 흐름을 “참일 때의 길”로 삼고, 거짓일 때만 점프로 벗어난다. 그러면 조건마다 점프가 하나씩만 든다.

이 방법을 쓸 수 있는 다른 상황은?

for는 초기화 뒤의 while로, do-while은 조건 검사를 몸통 아래에 두고 참이면 위로 뛰는 것으로 옮긴다. 컴파일러가 if와 while에서 분기 명령을 만들어 내는 방식도 같다(단순 연산과 분기의 코드 생성).

활용

  • 이 기법을 쓸 조건: 조건식이 CMP나 TEST 한 번으로 플래그에 담기는 견주기들로 이루어져 있다.
  • 이 기법을 떠올릴 신호: 고급 언어 코드나 의사코드를 주고 “어셈블리어로 구현하라”고 한다. 이 문서의 연습 두 개도 이 꼴이다.
  • 대표 문제 둘: 복합 조건 if(AND, OR)를 가장 짧게 옮기기, while을 옮기고 몇 바퀴 도는지 따라가기.
  • 선택지가 아주 많은 갈림(switch)은 CMP와 점프를 줄줄이 쓰는 대신 표로 바꾼다(표 기반 선택).
  • 컴파일러는 이 일을 두 단계로 한다. 먼저 if와 while을 “조건이면 어디로 가라”와 goto로 된 중간 코드로 풀고(3주소 코드), 그것을 기계의 분기 명령으로 바꾼다. 이 문서의 코드를 점프 자리에서 끊으면 한 덩어리씩 떨어지는데, 그 덩어리가 기본 블록이고 위의 순서도가 제어 흐름 그래프다1.
  • 흔한 실수: 조건을 뒤집지 않고 그대로의 점프를 쓴 뒤 몸통을 바로 아래에 둔다. 참일 때 몸통을 건너뛰는 정반대 코드가 된다.
  • 흔한 실수: >를 뒤집어 <로 쓴다. 같을 때 틀린다.
  • 흔한 실수: if-else에서 jmp를 빼먹는다. while에서 jmp top을 빼먹으면 한 번만 도는 if가 된다.

연결

자주 하는 오해

“if (a <= b)를 옮길 때는 ‘이하면 점프’인 jbe를 쓴다”

몸통을 바로 아래에 두는 보통의 꼴에서는 틀렸다. 조건과 같은 이름의 점프를 쓰고 싶은 것은 고급 언어의 if가 “참이면 한다”로 읽히기 때문이다. 하지만 점프는 “참이면 떠난다“다. 몸통이 바로 아래에 있으면 참일 때 떠나면 안 되고, 거짓일 때 떠나야 한다. 그래서 a <= b의 반대인 ja를 쓴다. 확인하는 법: EBX = 1, ECX = 5에서 cmp ebx,ecx / jbe next / mov eax,5 / next:를 따라가면, 조건이 참인데 mov를 건너뛴다. jbe를 그대로 쓰려면 위 if-else 풀이(jle L1)처럼 뛰어간 곳에 참일 때 할 일을 둬야 한다.

확인 문제

C1 부호 없는 수의 조건 a > b, a >= b, a < b, a <= b, a == b가 거짓일 때 뛰는 점프를 차례로 쓰라.

답: jbe, jb, jae, ja, jne.
이유: 조건의 반대로 뛴다. >의 반대는 <=, >=의 반대는 <다.

C2 부호 있는 수로 if (eax > ebx) ecx = 1; else ecx = 2;를 cmp eax,ebx / jle L1 / mov ecx,1 / jmp L2 / L1: mov ecx,2 / L2:로 옮겼다. EAX = FFFFFFFDh, EBX = 2일 때 ECX는? jle를 jbe로 잘못 쓰면 ECX는?

답: 2. jbe로 쓰면 1.
이유: FFFFFFFDh는 부호 있게 −3이다. −3 > 2가 거짓이라 jle가 뛰어 ECX = 2다. jbe는 부호 없이 4,294,967,293과 2를 견줘 뛰지 않고, 참일 때의 길로 흘러 ECX = 1이 된다.

C3 AND 조건의 두 구현(명령 7개, 명령 5개)은 무엇이 다른가? 짧은 쪽이 왜 같은 일을 하는가?

답: 긴 쪽은 조건이 참이면 다음 검사로 뛰고(ja L1), 그 아래의 jmp next가 거짓일 때 빠져나간다. 짧은 쪽은 조건을 뒤집어 거짓일 때 바로 빠져나가고(jbe next), 참이면 뛰지 않고 아래 검사로 흘러간다. ja가 안 뛰는 때와 jbe가 뛰는 때가 정확히 같아서 두 코드가 가는 길이 같다.

C4 top: cmp eax,ebx / jae next / inc eax / jmp top / next:와 top: inc eax / cmp eax,ebx / jb top은 둘 다 “EAX가 EBX보다 작은 동안 1씩 늘린다”처럼 보인다. EAX = 9, EBX = 5(부호 없는 수)에서 각각 끝난 뒤 EAX는?

답: 앞의 것은 9, 뒤의 것은 10.
이유: 앞의 것은 조건을 먼저 본다(while). 처음부터 거짓이라 몸통을 한 번도 안 돈다. 뒤의 것은 몸통을 먼저 돌고 조건을 본다(do-while). 적어도 한 번은 돈다.

  1. 표의 짝이 서로 반대라는 것은 플래그 16가지 조합 전수로, 코드의 결과는 흉내기와 파이썬 if·while의 대조로 확인했다. ↩ ↩2 ↩3 ↩4 ↩5 ↩6