구조체는 메모리에서 어떻게 저장될까?
이번 포스트에선 우리가 C에서 자주 사용하는 구조체(Struct)가 메모리에서는 어떤 방식으로 저장되고, 어셈블리에서는 어떻게 각 필드에 접근하는지를 함께 살펴보겠습니다.
구조체의 메모리 배치
예를 들어 다음과 같은 구조체가 있다고 해보겠습니다.
struct S {
char a;
int b;
char c;
};
겉으로 보기에는 1 + 4 + 1 = 6바이트 정도일 것 같지만, 실제로는 정렬(Alignment) 때문에 총 12바이트가 사용됩니다.
구조체 정렬 규칙: padding이 생기는 이유
C 컴파일러는 메모리 접근 속도를 최적화하기 위해 각 필드를 자신의 타입 크기에 맞는 주소에 정렬하려고 합니다.
- char는 아무 곳에나 저장 가능하지만,
- int는 4의 배수 주소에 저장되어야 하죠.
그래서 위의 구조체는 다음과 같이 저장됩니다.
필드 | 위치 | 이유 |
a | 0 | 시작 위치 |
(padding) | 1~3 | int b를 4의 배수 주소에 정렬하기 위해 |
b | 4~7 | 4바이트 정수 |
c | 8 | 다음 필드 |
(padding) | 9~11 | 구조체 전체를 4의 배수로 만들기 위해 |
최종적으로 이 구조체는 12바이트를 차지하게 됩니다.
이러한 정렬과 padding 처리는 모두 컴파일러가 자동으로 해주는 작업입니다. 개발자는 구조체가 어떻게 정렬되고, padding이 왜 필요한지만 이해하고 있으면 대부분의 상황에서 문제없이 사용할 수 있습니다.
구조체 필드 접근 방식, "기준 주소 + 오프셋"
구조체 포인터를 통해 필드에 접근하는 C 코드는 다음과 같습니다.
int f(struct S *p) {
return p->b;
}
이 코드는 어셈블리에서는 이렇게 번역됩니다.
movl 4(%rdi), %eax
- %rdi는 구조체 포인터 p가 저장된 레지스터이고,
- b 필드는 offset 4에 있으므로 4(%rdi)에서 값을 읽어오는 것이죠.
즉, 구조체의 각 필드는 기준 주소 + 고정 offset으로 접근됩니다. 이 offset은 필드 타입과 정렬 기준, 그리고 padding에 따라 결정됩니다.
필드 순서에 따라 구조체 크기가 바뀔 수 있다
같은 필드를 가지더라도 순서에 따라 구조체의 최종 크기는 달라질 수 있습니다.
struct A {
char a; // 1 byte
int b; // 4 bytes
char c; // 1 byte
};
// 예상 크기: 12
struct B {
int b; // 4 bytes
char a; // 1 byte
char c; // 1 byte
};
// 예상 크기: 8
같은 필드를 가지더라도 순서를 바꾸게 되면,
→ struct A는 중간과 끝에 padding이 추가되어 크기가 더 커지고,
→ struct B는 깔끔하게 정렬되어 더 작은 크기로 압축됩니다.
중첩 구조체의 내부 필드 접근
struct Inner {
int a;
char b;
};
struct Outer {
char x;
struct Inner inner;
char y;
};
구조체 안에 구조체가 들어가는 중첩 구조체 또한 존재할 수 있습니다. 이 경우 내부 구조체 또한 하나의 필드처럼 취급되며, 동일하게 offset 조합으로 접근할 수 있습니다.
Inner 자체가 Outer 구조체의 하나의 필드로 배치되기 때문에, Inner.a와 Inner.b는 내부 구조체(Inner) 기준 offset을 가지지만, 결국 Outer의 시작 주소로부터의 상대적인 거리로 재해석됩니다. 이로 인해 offset 계산 과정은 살짝 복잡해지지만, 어셈블리에서는 여전히 단순히 "base 주소 + 총 offset" 방식으로 접근할 뿐입니다.
즉, 중첩 구조체의 내부 필드도 실제 메모리에서는 단일 구조체처럼 offset을 합산하여 접근됩니다.
struct Outer outer;
int x = outer.inner.a;
이 코드는 어셈블리에서 아래와 같이 해석됩니다.
movl 4(%rdi), %eax ; inner.a는 Outer 기준 offset 4
movb 8(%rdi), %al ; inner.b는 Outer 기준 offset 8
즉, 어셈블리 관점에서는 여전히 단순히 "base 주소 + 총 offset" 방식으로 접근할 수 있으며, 내부 구조체의 구조가 바깥 구조체의 오프셋에 더해지는 형태로 반영됩니다.
구조체의 크기 계산 요령
구조체 크기를 계산할 때는 다음과 같은 순서를 따릅니다.
- 각 필드의 타입 크기와 정렬 기준을 확인한다.
- 필드 사이 필요한 padding을 계산한다.
- 구조체 전체 크기를 가장 큰 필드의 정렬 기준의 배수가 되도록 맞춘다.
구조체 배열에서는 왜 마지막에도 padding이 들어갈까?
구조체가 배열로 선언될 경우, 각 구조체 인스턴스는 동일한 정렬 기준을 따라야 합니다. 따라서 구조체의 마지막 필드 이후에도 padding이 추가되어 전체 구조체의 크기가 정렬 기준의 배수가 되도록 맞춰집니다.
예를 들어, struct S { char a; int b; }의 크기는 겉보기엔 5바이트지만, 다음 구조체 인스턴스의 b필드가 올바르게 정렬되도록 하기 위해 전체 크기를 8바이트로 맞춥니다.
정리
구조체 필드 접근 방식 | 어셈블리 표현 |
p->x | offset(%reg) |
필드 위치 계산 | 정렬 기준 + padding 포함 |
전체 크기 결정 | 최대 정렬 기준의 배수로 padding 포함 |
마치면서
C 구조체는 메모리에서 연속적으로 저장되지만, 각 필드의 정렬 기준과 padding으로 인해 실제 크기와 구조가 달라질 수 있습니다. 어셈블리어에서는 이 구조를 바탕으로 필드에 offset으로 접근하기 때문에, 정렬 원칙과 구조체의 메모리 배치를 정확히 이해하는 것이 중요합니다.
다음 포스트에서는 이러한 정렬 구조와 배열, 포인터가 함께 쓰일 때 어떤 일이 벌어지는지 더 살펴보겠습니다.
'크래프톤 정글 > 컴퓨터구조(CSAPP)' 카테고리의 다른 글
[CSAPP 3장 완전 정복] 3.11 부동소수점 연산은 어떻게 이루어질까? (0) | 2025.04.09 |
---|---|
[CSAPP 3장 완전 정복] 3.10 기계 수준 프로그램에서 제어와 데이터는 어떻게 결합될까? (0) | 2025.04.09 |
[CSAPP 3장 완전 정복] 3.6 – 조건문, 반복문, 분기 흐름을 어셈블리로 해석해보자 (0) | 2025.04.08 |
[CSAPP 3장 완전 정복] 3.8.5 동적 배열(VLA), 주소 계산은 어떻게 달라질까? (0) | 2025.04.08 |
[CSAPP 3장 완전 정복] 3.8 배열, 포인터, 주소 계산의 모든 것 (0) | 2025.04.07 |