명령은 “무엇을 하라”와 “무엇을 가지고”로 이루어진다. “무엇을 가지고” 자리에 올 수 있는 것은 세 가지뿐이다. 코드에 직접 적은 수, CPU 안의 작은 저장 칸, 메모리의 한 자리다. 메모리의 자리는 변수 이름으로 가리키고, 이름에 수를 더하면 그 변수에서 몇 바이트 뒤의 자리를 가리킨다. 이때 더해지는 것은 주소이지 값이 아니며, 배열 끝을 넘어가도 어셈블러는 막지 않는다.
예시로 보기
“AL에 값을 넣어라”만으로는 명령이 안 된다. 어떤 값인지, 그 값이 어디 있는지를 CPU에게 알려야 한다. 값이 있을 수 있는 곳이 셋이다.
.data
var1 BYTE 10h
.code
mov al,5 ; 코드에 적은 수 5
mov al,bl ; 레지스터 BL에 든 값
mov al,var1 ; 메모리의 var1 자리에 든 값 10h
명령에 쓰는 이런 재료를 피연산자라 부른다. 첫째 줄의 5가 즉시값, 둘째 줄의 bl이 레지스터 피연산자, 셋째 줄의 var1이 메모리 피연산자다.
arrayB+1이 20h인 것, [arrayW+2], [arrayW+4], [arrayD+4], 원소 가운데를 읽을 때의 값, 카드 C2·C3. 파이썬 흉내로 확인 — 15_operand-types_verify.py정의
피연산자는 세 종류다.
| 종류 | 무엇인가 | 기계어 명령 안에 들어가는 것 |
|---|---|---|
| 즉시값(immediate) | 상수 정수(8, 16, 32비트) | 그 값 자체 |
| 레지스터(register) | 레지스터 이름 | 이름을 바꾼 번호 |
| 메모리(memory) | 메모리의 한 자리 | 그 자리의 주소 |
명령마다 어느 자리에 어떤 피연산자를 쓸 수 있는지 적을 때는 다음 줄임말을 쓴다.
| 표기 | 뜻 |
|---|---|
reg8 |
8비트 범용 레지스터: AH, AL, BH, BL, CH, CL, DH, DL |
reg16 |
16비트 범용 레지스터: AX, BX, CX, DX, SI, DI, SP, BP |
reg32 |
32비트 범용 레지스터: EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP |
reg |
아무 범용 레지스터 |
sreg |
16비트 세그먼트 레지스터: CS, DS, SS, ES, FS, GS |
imm |
8, 16, 32비트 즉시값 |
imm8, imm16, imm32 |
8비트, 16비트, 32비트 즉시값 |
reg/mem8 |
8비트 피연산자. 8비트 범용 레지스터이거나 메모리의 바이트 |
reg/mem16, reg/mem32 |
16비트, 32비트 피연산자. 레지스터이거나 메모리의 워드, 더블워드 |
mem |
8, 16, 32비트 메모리 피연산자 |
직접 메모리 피연산자. 변수 이름으로 메모리의 자리를 가리킨다. 어셈블러가 이름을 주소로 바꾸고, 명령은 그 주소에 든 값을 쓴다. 대괄호를 씌워 [var1]로 써도 같다.
mov al,var1 ; AL = 10h
mov al,[var1] ; AL = 10h
직접 오프셋 피연산자. 이름에 상수를 더하면 “그 이름의 자리에서 그만큼 뒤”의 주소가 된다. 이렇게 계산해 얻은 주소를 유효 주소라 부른다. 명령은 그 주소에 든 값을 쓴다.
.data
arrayB BYTE 10h,20h,30h,40h
.code
mov al,arrayB+1 ; AL = 20h
mov al,[arrayB+1] ; 같은 뜻
예제
arrayB+1은 왜 11h가 아닌가. arrayB는 값 10h가 아니라 그 값이 놓인 자리다. +1은 자리를 한 칸 옮긴다. 옮긴 자리에 든 값이 20h다. 값에 1을 더하고 싶으면 값을 꺼낸 뒤에 더하는 명령을 따로 쓴다1.
주소 arrayB+0 arrayB+1 arrayB+2 arrayB+3
바이트 10 20 30 40
▲
arrayB+1
원소가 1바이트보다 클 때. 더하는 수는 바이트 수다. 워드 배열의 다음 원소는 2 뒤, 더블워드 배열의 다음 원소는 4 뒤에 있다.
.data
arrayW WORD 1000h,2000h,3000h
arrayD DWORD 1,2,3,4
.code
mov ax,[arrayW+2] ; AX = 2000h
mov ax,[arrayW+4] ; AX = 3000h
mov eax,[arrayD+4] ; EAX = 00000002h
0번부터 셀 때 \(k\)번째 원소의 자리는 “이름 + \(k\) × 원소 하나의 바이트 수”다1.
원소 가운데를 가리키면. mov ax,[arrayW+1]은 오류가 아니다. arrayW의 메모리는 00 10 00 20 00 30이고, +1 자리부터 두 바이트 10 00을 읽어 AX는 0010h가 된다. 첫 원소의 윗바이트와 둘째 원소의 아랫바이트가 붙은 값이다1.
활용
- 어셈블러는 이름에 더한 수가 배열 범위 안인지 확인하지 않는다.
arrayB+4는 배열 바로 뒤에 놓인 다른 변수의 첫 바이트를 읽는다1. - 주소를 레지스터에 넣어 두고 실행 중에 옮겨 가며 쓰는 방법은 간접 피연산자와 인덱스 피연산자에 있다.
- 명령마다 허용되는 피연산자 조합이 다르다.
MOV의 규칙은 데이터 전송 명령에 있다.
연결
- 선수: x86 레지스터, 데이터 정의, 리틀 엔디언
- 명령 한 줄에서 피연산자가 놓이는 자리: 어셈블리 문장의 구성
- 컴파일러의 중간 코드도 명령 하나에 재료를 많아야 셋 적는다. 그 재료(이름, 상수, 임시 변수)가 여기의 메모리, 즉시값, 레지스터로 옮겨진다: 3주소 코드
확인 문제
C1 다음 명령의 피연산자를 각각 즉시값, 레지스터, 메모리로 나누라. (a) mov eax,[arrayD+4] (b) add bl,5 (c) mov count,al
답: (a) eax 레지스터, [arrayD+4] 메모리. (b) bl 레지스터, 5 즉시값. (c) count 메모리, al 레지스터.
이유: 수를 직접 적었으면 즉시값, 레지스터 이름이면 레지스터, 변수 이름(대괄호나 +수가 붙어도)이면 메모리다. [arrayD+4]의 4는 즉시값이 아니라 주소 계산의 일부다.
C2 arrayW WORD 1000h,2000h,3000h일 때 mov ax,[arrayW+1]을 실행한 뒤 AX는?
답: 0010h.
이유: 메모리는 낮은 주소부터 00 10 00 20 00 30이다. +1 자리부터 두 바이트는 10 00이고, 높은 주소의 바이트가 윗자리라 0010h다. “두 번째 원소 2000h”라고 답하면 틀린다. 더하는 수는 원소 번호가 아니라 바이트 수다.
C3 tbl DWORD 10h,20h,30h에서 세 번째 원소 30h를 EAX로 가져오는 명령을 쓰고, mov eax,[tbl+2]가 그 답이 아닌 이유를 대라. [tbl+2]로 읽히는 값도 쓰라.
답: mov eax,[tbl+8]. [tbl+2]는 00200000h를 읽는다.
이유: 더블워드는 4바이트라 0번부터 센 2번 원소는 2 × 4 = 8 뒤에 있다. +2는 첫 원소의 가운데다. 메모리 10 00 00 00 20 00 00 00 …의 +2부터 네 바이트 00 00 20 00을 읽어 00200000h가 된다.