배열, 포인터, 주소 계산의 모든 것
이번 포스트에서는 C 코드에서 자주 사용하는 배열이 어셈블리에서는 어떻게 주소를 계산하고 접근하는지를 살펴볼 예정인데요. 포인터 연산, 인덱스 계산, 메모리 접근까지 모두 직관적인 매핑 방식으로 이해해보겠습니다.
어셈블리의 배열 접근 방법, 주소 계산
int A[5];
int x = A[3];
이 단순한 배열 접근은 어셈블리어에서는 다음과 같은 주소 계산으로 바뀝니다.
movl A+12, %eax
이때 int는 4바이트이므로, A[3]은 A + 3 * 4 = A + 12 위치에 있는 값이 됩니다.
주소 계산 공식: D(Rb, Ri, S)
주소 계산 공식은 3.4절에서 메모리 접근 방식에 대해 설명할 때 소개해드린 적 있는데요. 그때 배열 접근에서도 이 공식이 사용된다고 말씀드린 바 있습니다. 이 공식은 C의 배열, 구조체, 포인터 접근을 모두 통합적으로 표현할 수 있는 강력한 구조입니다.
D(Rb, Ri, S)
주소 = Rb + Ri × S + D
- Rb: 배열의 시작 주소 (Base Register)
- Ri: 인덱스 (Index Register)
- S: 요소 크기 (Scale Factor: 1, 2, 4, 8만 가능)
- D: 상수 오프셋 (Displacement)
어셈블리의 배열 요소 접근 예시
int A[5];
int x = A[i];
위의 C 코드를 어셈블리 코드로 나타내면 아래와 같이 나타낼 수 있습니다.
movl (%rdi, %rsi, 4), %eax
- %rdi = A 배열의 시작 주소
- %rsi = 인덱스 i
- 4 = int 타입의 크기
어셈블리에서 다차원 배열의 접근
우리가 보는 다차원 배열은 행과 열로 구성된 형태처럼 보이지만, 메모리 상에서는 실제로 일렬로 나열된 1차원 배열일 뿐입니다. 즉, 다차원 배열은 메모리에서는 1차원으로 평탄화되어 저장되며, 각 차원의 인덱스를 곱하고 더하는 방식으로 실제 메모리 주소가 계산됩니다.
예를 들어, 아래와 같이 2차원 정수 배열을 선언하고 특정 행, 열의 값을 변수에 할당해 보겠습니다.
int A[3][4]; // 3행 4열 배열
int x = A[i][j];
이는 실제 메모리에서는 다음과 같은 수식으로 접근합니다.
주소 = A + (i * 4 + j) * 4 ← 4는 int 크기 (4바이트)
- i * 4: i번째 행으로 점프
- + j: j번째 열로 이동
- * 4: 바이트 단위로 주소 계산
그리고 이를 어셈블리로는 아래와 같이 표현합니다.
imulq $4, %rcx, %rax ; rax = i * 4 ← 행 오프셋 (i번째 행, 4칸씩 이동)
; %rcx = i (행 인덱스)
addq %rdx, %rax ; rax += j ← 열 인덱스 더하기
; %rdx = j (열 인덱스)
leaq (%rdi, %rax, 4), %rax ; rax = A + (i*4 + j)*4 ← 실제 메모리 주소 계산
; %rdi = A (배열 시작 주소)
movl (%rax), %eax ; eax = A[i][j] ← 해당 위치의 값 읽어오기
어셈블리의 포인터 연산
int *p = A;
int x = *(p + i);
위의 C 코드를 어셈블리로 표현하면 다음과 같습니다.
movl (%rdi, %rsi, 4), %eax
배열 접근과 똑같은 방식으로 주소를 계산하고 있는 게 보이시나요? 즉, 배열명 A는 사실상 &A[0]이라는 포인터 값으로 해석된다고 볼 수 있습니다.
배열은 메모리에서 어떻게 배치될까?
배열은 연속적인 메모리 공간에 할당됩니다. 따라서 A[i]의 주소는 항상 A + i * sizeof(element)로 계산이 가능합니다. 정수형 배열, 실수형 배열, 구조체 배열 모두 같은 공식을 따릅니다.
다만 메모리에 할당되는 방식은 정렬(alignment) 규칙에 따라 달라질 수 있는데, 이는 3.9절에서 자세히 다룹니다.
배열 접근 정리
C 코드 | 어셈블리 |
A[i] | (%rdi, %rsi, S) |
*(p + i) | (%rdi, %rsi, S) |
A[i][j] | A + (i * cols + j) * 4 -> 어셈블리로는 (%rdi, %rsi, 4) ※ 단, (i * cols + j)는 미리 계산되어 %rsi에 담겨 있어야 함 |
배열 접근은 어셈블리어에서는 주소 계산 공식만 정확히 이해하면 완전히 예측 가능한 구조가 됩니다.
마치면서
배열이나 포인터 접근은 어셈블리어에서는 D(Rb, Ri, S) 공식에 따라 주소를 계산해 메모리를 읽는 구조로 표현됩니다. C 코드가 다차원 배열을 하던, 포인터 연산을 하던 기계는 항상 같은 방식으로 동작한다는 걸 알 수 있죠.
다음 포스트에서는 배열 크기가 고정되지 않은 경우, 즉 동적 배열(Variable-Length Array)에서 주소 계산은 어떻게 달라지는지를 다뤄보겠습니다.
'크래프톤 정글 > 컴퓨터구조(CSAPP)' 카테고리의 다른 글
[CSAPP 3장 완전 정복] 3.6 – 조건문, 반복문, 분기 흐름을 어셈블리로 해석해보자 (0) | 2025.04.08 |
---|---|
[CSAPP 3장 완전 정복] 3.8.5 동적 배열(VLA), 주소 계산은 어떻게 달라질까? (0) | 2025.04.08 |
[CSAPP 3장 완전 정복] 3.7 함수 호출, 스택, 레지스터의 삼각관계 (0) | 2025.04.07 |
[CSAPP 3장 완전 정복] 3.4 메모리와 레지스터, 정보를 어떻게 읽고 쓸까? (0) | 2025.04.06 |
[CSAPP 3장 완전 정복] x86-64 어셈블리 필수 배경지식 핵심 정리 (0) | 2025.04.06 |