프로그램은 위에서 아래로 한 줄씩 읽힌다. 책을 읽다가 “37쪽으로 가시오”를 만나면 그쪽으로 넘어가듯, 다음에 읽을 줄을 바꿔 주는 명령이 둘 있다. 하나는 따지지 않고 무조건 넘어가고, 다른 하나는 남은 횟수를 하나 줄인 뒤 0이 아니면 넘어가서 같은 줄들을 정해진 횟수만큼 되풀이한다. 횟수를 세는 칸이 하나뿐이라, 처음에 0을 넣으면 한 번도 안 도는 것이 아니라 40억 번 넘게 돌고, 반복 안에 반복을 넣을 때는 바깥 횟수를 다른 곳에 치워 둬야 한다.
예시로 보기
배열의 수 네 개를 더하려면 “더하기”를 네 번 쓰면 된다. 수가 천 개면 천 줄을 쓸 수는 없다. 같은 줄로 되돌아가는 명령이 필요하다.
CPU는 다음에 읽을 명령의 주소를 EIP라는 칸(x86 레지스터)에 적어 둔다. 보통은 명령 하나를 읽을 때마다 그다음 주소로 넘어간다. JMP는 이 칸에 다른 주소를 써넣는다.
top:
.
.
jmp top ; EIP ← top의 주소. 조건 없이 되돌아간다
top:처럼 이름 뒤에 쌍점을 붙인 것은 그 줄의 주소에 붙인 이름표(레이블)다. 위 코드는 빠져나갈 길이 없어 끝없이 돈다. 정해진 횟수만 돌려면 LOOP를 쓴다.
mov ax,0
mov ecx,5 ; 다섯 번 돈다
L1: add ax,cx
loop L1 ; ECX를 1 줄이고, 0이 아니면 L1로
| 실행한 줄 | AX | ECX | 다음 |
|---|---|---|---|
add ax,cx |
0005h | 5 | |
loop L1 |
0005h | 4 | L1로 |
add ax,cx |
0009h | 4 | |
loop L1 |
0009h | 3 | L1로 |
add ax,cx |
000Ch | 3 | |
loop L1 |
000Ch | 2 | L1로 |
add ax,cx |
000Eh | 2 | |
loop L1 |
000Eh | 1 | L1로 |
add ax,cx |
000Fh | 1 | |
loop L1 |
000Fh | 0 | 아래로 빠진다 |
\(5 + 4 + 3 + 2 + 1 = 15\), 16진수로 0Fh다.
정의
JMP. JMP 목적지는 조건 없이 목적지 레이블로 넘어간다. 하는 일은 \(\text{EIP} \leftarrow \text{목적지}\) 하나다(화살표는 “오른쪽 값을 왼쪽 칸에 넣는다”로 읽는다). 목적지는 보통 같은 프로시저 안의 레이블이다. 프로시저 밖으로 뛰려면 전역 레이블이어야 한다. 레이블이 어디까지 보이는지는 레지스터 매개변수와 USES에 있다.
LOOP. LOOP 목적지는 두 일을 이 순서로 한다.
- \[\text{ECX} \leftarrow \text{ECX} - 1\]
- \(\text{ECX} \ne 0\)이면 목적지로 넘어간다. 0이면 바로 아래 줄로 간다.
먼저 줄이고 나서 0인지 본다. 이 순서가 아래 Your turn의 답을 정한다.
상대 오프셋. 기계어 속의 LOOP에는 목적지 주소가 통째로 들어 있지 않다. 어셈블러가 “LOOP 바로 다음 명령의 주소에서 목적지까지 몇 바이트 떨어졌는가”를 계산해 그 거리만 넣는다. 이 거리를 상대 오프셋(relative offset)이라 부른다. CPU는 실행할 때 이 값을 EIP에 더한다.
위 예제를 어셈블한 목록이다.
오프셋 기계어 소스
00000000 66 B8 0000 mov ax,0
00000004 B9 00000005 mov ecx,5
00000009 66 03 C1 L1: add ax,cx
0000000C E2 FB loop L1
0000000E
loop L1의 기계어는 E2 FB 두 바이트다. 뒤의 FB가 상대 오프셋이다. FBh를 부호 있는 8비트로 읽으면 −5다(정수의 2진 표현과 2의 보수). LOOP 다음 명령의 주소 0000000Eh에 −5를 더하면 00000009h, 곧 L1이다. 식에 넣어 보면 \(9 - 14 = -5\)다.
예제
Your turn 1. AX의 마지막 값은?
mov ax,6
mov ecx,4
L1:
inc ax
loop L1
답
- 몸통이 네 번 돌아 \(6 + 4 = 10\)이다.
Your turn 2. 이 루프는 몇 번 도는가?
mov ecx,0
X2:
inc ax
loop X2
답
4,294,967,296번(\(2^{32}\)번). LOOP는 먼저 줄인다. 0에서 1을 빼면 32비트 칸에서는 FFFFFFFFh가 된다. 0이 아니므로 되돌아가고, 거기서부터 0이 될 때까지 4,294,967,295번을 더 줄여야 한다1.
배열의 합. 16비트 정수 배열을 처음부터 끝까지 더한다. 주소를 레지스터에 넣고 한 칸씩 옮겨 가는 방식은 간접 피연산자에서 본 것이다.
.data
intarray WORD 100h,200h,300h,400h
.code
mov edi,OFFSET intarray ; 배열의 주소
mov ecx,LENGTHOF intarray ; 횟수 = 원소 수 4
mov ax,0 ; 합을 0으로
L1:
add ax,[edi] ; 원소 하나를 더한다
add edi,TYPE intarray ; 다음 원소로 (2바이트 뒤)
loop L1
AX는 0100h → 0300h → 0600h → 0A00h로 바뀐다. 횟수와 건너뛸 크기를 4, 2라고 직접 쓰지 않고 LENGTHOF와 TYPE으로 쓴 덕에, 배열을 고쳐도 코드는 그대로다.
문자열 복사. 인덱스로 같은 자리의 글자를 하나씩 옮긴다.
.data
source BYTE "This is the source string",0
target BYTE SIZEOF source DUP(0)
.code
mov esi,0 ; 인덱스
mov ecx,SIZEOF source ; 횟수 = 26 (글자 25개 + 끝의 0)
L1:
mov al,source[esi] ; 글자 하나를 꺼내
mov target[esi],al ; 같은 자리에 넣는다
inc esi
loop L1
메모리에서 메모리로 한 번에 옮기는 MOV는 없어서 AL을 거친다(데이터 전송 명령).
반복 안의 반복. 안쪽 루프도 ECX를 쓴다. 그래서 안쪽으로 들어가기 전에 바깥 횟수를 다른 곳에 옮겨 두고, 나와서 되돌린다. 바깥 100번, 안쪽 20번이면 안쪽 몸통은 2,000번 돈다.
.data
count DWORD ?
.code
mov ecx,100 ; 바깥 횟수
L1:
mov count,ecx ; 바깥 횟수를 치워 둔다
mov ecx,20 ; 안쪽 횟수
L2: .
.
loop L2
mov ecx,count ; 바깥 횟수를 되돌린다
loop L1
치워 두지 않으면 어떻게 될까? 안쪽 루프가 끝날 때 ECX는 0이다. 바깥 loop L1이 이것을 1 줄여 FFFFFFFFh로 만들고 되돌아간다. 매번 같은 일이 벌어져 끝나지 않는다1. 변수 대신 스택에 치워 두는 방법(push ecx … pop ecx)은 런타임 스택과 PUSH·POP에 있다.
활용
- 이 기법을 떠올릴 신호: 돌 횟수를 시작 전에 안다(배열 길이, 문자열 크기). 횟수를 모르고 조건으로 멈춰야 하면 조건 점프나 조건부 반복 명령을 쓴다.
- 흔한 실수: ECX가 0일 수 있는데 그대로
LOOP에 들어간다. 빈 배열에서 40억 번을 돈다. 들어가기 전에 ECX가 0이면 건너뛰는JECXZ가 조건 점프에 있다. - 흔한 실수: 몸통 안에서 ECX를 다른 일에 쓴다. 횟수가 망가진다.
- 흔한 실수: 다음 원소로 갈 때 원소 크기를 틀린다. WORD 배열에서
inc edi를 쓰면 1바이트만 움직여 두 원소에 걸친 값을 읽는다. - Java의
for (int i = 5; i > 0; i--)가 하는 일을LOOP한 줄이 한다. 다만for는 돌기 전에 조건을 보고,LOOP는 몸통을 한 번 돈 뒤에 본다(제어문의do-while과 같은 꼴이다)1.
연결
- 선수: x86 레지스터(EIP, ECX), 간접 피연산자와 인덱스 피연산자
- 조건이 붙은 점프와 반복: 조건 점프, 조건부 반복 명령
if와while을 점프로 만들기: 조건 구조를 점프로 옮기기- 연습: LOOP로 배열 돌기 예제 사다리
- 컴파일러가 만드는 중간 코드의
goto가JMP다: 3주소 코드
확인 문제
C1 다음 코드가 끝났을 때 AX와 ECX의 값은? mov ax,0 / mov ecx,3 / L1: add ax,cx / add ax,cx / loop L1
답: AX = 12(000Ch), ECX = 0.
이유: 한 바퀴에 CX를 두 번 더한다. \(2 \times (3 + 2 + 1) = 12\)다. LOOP가 ECX를 0으로 만든 순간 아래로 빠진다.
C2 mov ecx,0 뒤에 바로 LOOP가 있는 루프는 왜 0번이 아니라 \(2^{32}\)번 도는가?
답: LOOP는 0인지 보기 전에 먼저 1을 줄인다. 0에서 1을 줄이면 FFFFFFFFh가 되고, 이것은 0이 아니라서 되돌아간다. 다시 0이 되려면 \(2^{32}\)번을 줄여야 한다.
이유: “ECX가 0이면 안 돈다”고 읽는 오답이 많다. 검사는 줄인 뒤의 값에 한다.
C3 loop L1(2바이트)이 주소 0000001Ah에 있고 L1이 00000010h에 있다. 기계어에 들어가는 상대 오프셋을 16진수 한 바이트로 쓰라.
답: F4h.
이유: LOOP 다음 명령의 주소는 \(\text{1Ah} + 2 = \text{1Ch}\)다. \(\text{10h} - \text{1Ch} = -12\)이고, −12를 8비트 2의 보수로 쓰면 F4h다. LOOP 자신의 주소 1Ah에서 재서 F6h라고 쓰면 틀린다.