rflags라는 특별한 레지스터가 있어요.
이 레지스터의 비트들은 여러 조건에 대한 플래그처럼 동작해요.
그중 일부는 아래와 같아요:
| 이름 | 기호 | 비트 |
|---|---|---|
| 캐리 | CF | 0 |
| 제로 | ZF | 6 |
| 부호 | SF | 7 |
| 오버플로 | OF | 11 |
전체 목록은 Intel 매뉴얼을 참고해요.
rflags의 플래그는 직접 수정되지 않아요.
대신, 여러 명령어가 이 플래그를 설정해요.
예를 들어, ZF 플래그는 산술 연산이나 비트 연산의 결과가 0일 때 설정돼요.
조건을 검사할 때 가장 흔히 쓰이는 명령어 중 하나가 cmp예요.
cmp는 피연산자 두 개를 받아 플래그를 갱신하지만, 피연산자 자체는 바꾸지 않아요.
cmp 명령어는 첫 번째 피연산자에서 두 번째 피연산자를 빼고, 그 결과에 따라 플래그를 설정해요.
A가 첫 번째 피연산자이고 B가 두 번째 피연산자라면:
| 플래그 | 설정되는 조건 |
|---|---|
| CF | A < B (부호 없음) |
| ZF | A == B |
| SF | A < B (부호 있음, 오버플로 없음) |
| OF | 부호 있는 뺄셈에서 오버플로 |
기본적으로 x86-64의 코드는 위에서 아래로 순차적으로 실행돼요.
하지만 이 동작을 바꿔야 하는 상황이 많아요. 예를 들어, 어떤 조건에 따라 다른 명령어들을 실행해야 할 때가 그래요.
고급 언어에서는 보통 if...else 같은 추상화로 이걸 처리해요.
하지만 x86-64에는 그런 게 없어요.
대신 x86-64는 실행을 코드의 다른 위치로 옮기는 명령어를 제공해요.
이걸 branching이라고 해요.
그런 명령어는 이미 두 개 봤어요: 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의 플래그 하나 이상에 대응해요.
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에서는 뺄셈 결과가 0이면 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