CPU 주소 지정 방식과 유효 주소 계산
CPU 주소 지정 방식의 유효 주소 계산, 메모리 접근 특성, x86·ARM·MIPS·RISC-V 활용 방식을 정리한다.
2026-08-14 · 최초 발행 2026-01-16
명령어는 피연산자를 어떻게 가리키는가
CPU 명령어의 오퍼랜드 필드는 값 자체를 담을 수도 있고, 메모리 위치나 레지스터를 가리킬 수도 있다. 이 해석 규칙이 주소 지정 방식(Address Mode)이다. 즉시 주소, 직접 주소, 간접 주소, 레지스터 주소, 인덱스 주소는 프로그램의 유연성·실행 속도·메모리 효율성에 서로 다른 영향을 준다.
명령어는 연산 코드와 주소 지정 방식, 피연산자 정보로 나눠 볼 수 있다.
┌─────────────┬─────────────┬─────────────┐
│ Opcode │ Address Mode│ Operand │
│ (연산 코드) │ (주소 방식) │ (피연산자) │
└─────────────┴─────────────┴─────────────┘
주소 지정 방식은 포인터·배열·구조체 같은 데이터 접근 패턴을 가능하게 하며, 루프와 서브루틴 구현에도 관여한다. 메모리 접근 횟수와 명령어 크기를 조절하고, 큰 주소 공간을 작은 필드로 표현해 코드 밀도에도 영향을 준다.
명령어에 값을 넣는 즉시 주소 지정
즉시 주소 지정(Immediate Addressing)은 피연산자 값을 명령어 안에 직접 넣는다. 상수를 곧바로 사용하므로 메모리 접근이 필요 없고 가장 빠르지만, 명령어에 담을 수 있는 값의 크기에는 제한이 있다.
ADD R1, #100 ; R1 = R1 + 100
MOV R2, #0xFF ; R2 = 255
유효 주소(EA)를 별도로 계산하지 않는다. 오퍼랜드 필드 값 자체가 데이터다.
EA = 없음 (데이터가 명령어에 포함)
Operand = 명령어의 오퍼랜드 필드 값
메모리 주소를 해석하는 직접·간접 방식
직접 주소 지정(Direct Addressing)에서는 오퍼랜드 필드가 메모리 주소를 바로 나타낸다. 한 번의 메모리 접근으로 데이터를 가져오며, 절대 주소를 사용한다. 반면 오퍼랜드 필드가 표현할 수 있는 주소 공간에는 제한이 있다.
LOAD R1, [1000] ; R1 = Memory[1000]
STORE R2, [2000] ; Memory[2000] = R2
EA = 오퍼랜드 필드 값
Operand = Memory[EA]
간접 주소 지정(Indirect Addressing)은 오퍼랜드 필드가 실제 주소가 저장된 메모리 위치를 가리킨다. 포인터 개념을 구현할 수 있고 큰 주소 공간에 접근하거나 동적으로 주소를 바꿀 수 있지만, 실제 데이터를 얻기까지 메모리에 두 번 접근한다.
LOAD R1, @[1000] ; R1 = Memory[Memory[1000]]
EA = Memory[오퍼랜드 필드]
Operand = Memory[EA]
레지스터를 값과 포인터로 쓰는 방식
레지스터 주소 지정(Register Addressing)은 오퍼랜드가 레지스터를 지정하는 형태다. 메모리에 접근하지 않고 짧은 오퍼랜드 필드로 데이터를 다룰 수 있어 매우 빠르지만, 사용할 수 있는 레지스터 수는 제한된다.
ADD R1, R2, R3 ; R1 = R2 + R3
MOV R4, R5 ; R4 = R5
EA = 레지스터 번호
Operand = Register[EA]
레지스터 간접 주소 지정(Register Indirect Addressing)에서는 레지스터에 저장된 값을 메모리 주소로 사용한다. 포인터를 레지스터에 보관하면서 한 번의 메모리 접근으로 데이터를 가져오므로, 동적인 주소 계산과 배열 접근에 유용하다.
LOAD R1, [R2] ; R1 = Memory[R2]
STORE R3, [R4] ; Memory[R4] = R3
EA = Register[오퍼랜드 필드]
Operand = Memory[EA]
기준 위치에 변위를 더해 유효 주소를 만든다
변위 주소 지정(Displacement Addressing)은 기준 주소와 변위를 더해 유효 주소를 구한다. 기준점이 무엇인지에 따라 상대 주소, 베이스 레지스터 주소, 인덱스 주소로 나뉜다.
상대 주소 지정은 PC를 기준으로 한다. 분기 명령어, 위치 독립적 코드, 조건부 점프에서 사용한다.
EA = PC + Displacement
BEQ +10 ; if equal, jump to PC+10
JMP -5 ; jump to PC-5
베이스 레지스터 주소 지정은 베이스 레지스터와 변위를 조합한다. 구조체 필드, 스택 프레임, 재배치 가능 코드를 다룰 때 쓰인다.
EA = Base Register + Displacement
LOAD R1, [BP+8] ; R1 = Memory[BP+8]
STORE R2, [SP-4] ; Memory[SP-4] = R2
인덱스 주소 지정은 베이스 주소와 인덱스 레지스터를 더한다. 배열 요소, 루프, 테이블 참조에 적합하다.
EA = Base Address + Index Register
LOAD R1, Array[R2] ; R1 = Memory[Array + R2]
복합 주소 계산이 필요한 데이터 구조
베이스-인덱스 주소 지정(Base-Indexed)은 베이스 레지스터와 인덱스 레지스터를 함께 사용한다. 2차원 배열이나 복잡한 데이터 구조처럼 주소 계산이 유연해야 하는 상황에 맞는다.
EA = Base Register + Index Register + Displacement
LOAD R1, [R2+R3+8] ; R1 = Memory[R2+R3+8]
스케일드 인덱스 주소 지정(Scaled Index)은 인덱스에 스케일 팩터를 곱한 뒤 베이스와 변위를 더한다. 요소 크기가 있는 배열을 접근할 때 사용한다.
EA = Base + (Index × Scale) + Displacement
mov eax, [ebx + ecx*4 + 100] ; 4바이트 배열 접근
mov eax, [array + esi*8] ; 8바이트 배열 접근
스케일 팩터는 배열 요소 단위에 따라 달라진다.
| 스케일 팩터 | 대상 |
|---|---|
| 1 | 바이트 배열 |
| 2 | 워드 배열 |
| 4 | 더블워드 배열 |
| 8 | 쿼드워드 배열 |
메모리 접근과 유연성의 차이
| 주소 방식 | 메모리 접근 | 속도 | 유연성 | 용도 |
|---|---|---|---|---|
| 즉시 | 0회 | 매우 빠름 | 낮음 | 상수 |
| 레지스터 | 0회 | 매우 빠름 | 낮음 | 임시 저장 |
| 직접 | 1회 | 빠름 | 중간 | 전역 변수 |
| 레지스터 간접 | 1회 | 빠름 | 높음 | 포인터 |
| 간접 | 2회 | 느림 | 높음 | 이중 포인터 |
| 인덱스 | 1회 | 중간 | 높음 | 배열 |
| 베이스-인덱스 | 1회 | 중간 | 매우 높음 | 복잡한 구조 |
아키텍처가 제공하는 주소 계산 형태
x86/x64는 여러 주소 모드를 지원한다.
; 즉시
mov eax, 100
; 레지스터
mov eax, ebx
; 직접
mov eax, [0x1000]
; 레지스터 간접
mov eax, [ebx]
; 베이스 + 변위
mov eax, [ebp-8]
; 스케일드 인덱스
mov eax, [ebx+ecx*4+10]
x64에서는 RIP 상대 주소를 기본 주소 모드로 사용한다.
; 64비트에서 기본 주소 모드
mov rax, [rip+offset]
lea rax, [rip+label]
ARM은 Load/Store 아키텍처에서 오프셋과 레지스터 조합으로 주소를 계산한다.
; 즉시
MOV R0, #100
; 레지스터
MOV R0, R1
; 오프셋 (Pre-indexed)
LDR R0, [R1, #4]
; 오프셋 (Post-indexed)
LDR R0, [R1], #4
; 레지스터 오프셋
LDR R0, [R1, R2]
; 스케일드 레지스터
LDR R0, [R1, R2, LSL #2]
| 모드 | 형식 | 설명 |
|---|---|---|
| Pre-indexed | [Rn, #offset] | Rn+offset 접근 |
| Pre-indexed Writeback | [Rn, #offset]! | Rn 업데이트 후 접근 |
| Post-indexed | [Rn], #offset | 접근 후 Rn 업데이트 |
MIPS는 단순한 주소 모드를 사용하며, 메모리 접근은 베이스와 오프셋을 결합하는 방식으로 이뤄진다.
# 즉시
li $t0, 100
addi $t0, $t0, 10
# 레지스터
move $t0, $t1
# 베이스 + 오프셋 (유일한 메모리 접근 모드)
lw $t0, 100($t1) # $t0 = Memory[$t1 + 100]
sw $t0, 200($t1) # Memory[$t1 + 200] = $t0
RISC-V도 Load/Store 아키텍처에서 즉시값, 레지스터, 베이스+오프셋, PC 상대 방식을 사용한다.
# 즉시
addi x1, x0, 100 # x1 = 100
# 레지스터
add x1, x2, x3 # x1 = x2 + x3
# 베이스 + 오프셋
lw x1, 100(x2) # x1 = Memory[x2 + 100]
sw x1, 200(x2) # Memory[x2 + 200] = x1
# PC 상대
auipc x1, 0x12345 # x1 = PC + (0x12345 << 12)
배열·구조체·연결 리스트에서의 주소 계산
배열 순회에서는 스케일드 인덱스가 요소 위치 계산을 담당한다.
int array[100];
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += array[i];
}
mov ecx, 0 ; i = 0
mov eax, 0 ; sum = 0
loop:
add eax, [array + ecx*4] ; sum += array[i]
inc ecx ; i++
cmp ecx, 100
jl loop
구조체 필드는 포인터가 가리키는 기준 위치에 필드 오프셋을 더해 접근한다.
struct Point {
int x; // offset 0
int y; // offset 4
int z; // offset 8
};
struct Point *p = ...;
int result = p->x + p->y + p->z;
LDR R0, [R1] ; R0 = p->x
LDR R2, [R1, #4] ; R2 = p->y
ADD R0, R0, R2 ; R0 = x + y
LDR R2, [R1, #8] ; R2 = p->z
ADD R0, R0, R2 ; R0 = x + y + z
연결 리스트는 현재 노드의 주소를 따라 다음 노드의 주소를 읽어 가는 레지스터 간접 접근의 예다.
struct Node {
int data;
struct Node *next;
};
struct Node *curr = head;
while (curr != NULL) {
process(curr->data);
curr = curr->next;
}
LOAD R1, head ; R1 = head
loop:
CMP R1, #0 ; curr == NULL?
BEQ done
LOAD R2, [R1] ; R2 = curr->data
CALL process
LOAD R1, [R1+4] ; curr = curr->next
JMP loop
done:
코드 생성 시 고려할 점
메모리 접근을 줄이려면 자주 사용하는 데이터를 레지스터에 두는 방식이 유리하다. 접근 특성은 레지스터 > 즉시 > 직접 > 간접 순으로 볼 수 있다. 짧은 변위를 사용하고 레지스터 주소를 선호하면 명령어 크기와 코드 캐시 효율성에도 영향을 준다.
컴파일러는 지역 변수를 레지스터에 할당하고, 스필(Spill)이 발생하면 스택을 사용한다. 강도 감소(Strength Reduction)에서는 배열 인덱스 접근을 포인터 증가로 바꾼다.
array[i] 접근을 포인터 증가로 변환
p = array;
*p++ 형태로 최적화
루프 불변 코드 이동과 인덱스 계산 최소화도 주소 계산 비용을 줄이는 최적화 대상이다. CISC 아키텍처는 다양한 주소 모드를 제공하고, RISC 아키텍처는 단순한 주소 모드를 제공한다.