x86-64 시스템 V ABI에서 값 반환 방식 이해하기
x86-64 아키텍처에서 시스템 V ABI는 첫 번째 두 개의 정수 값을 반환할 때 rax와 rdx 레지스터를 사용합니다. 구조체의 경우, 크기에 따라 분류 기준이 적용되며, 작은 크기의 경우 INTEGER, 큰 경우 MEMORY로 분류됩니다. 이는 값이 직접 반환되는지, 아니면 rdi 레지스터를 통해 전달되는 숨겨진 포인터를 통해 반환되는지를 결정합니다.
먼저 int 값을 반환하는 기본 사례를 살펴보겠습니다:
one_plus_one():
mov DWORD PTR [rbp-4], 2
mov eax, DWORD PTR [rbp-4]
ret
값은 eax (즉, rax의 하위 32비트)에 로드됩니다. 마찬가지로 64비트 타입의 경우 전체 rax 레지스터를 사용합니다.
16바이트 이하의 소형 구조체: 레지스터를 통한 반환
소형 구조체는 rax와 rdx 레지스터를 통해 직접 반환됩니다. 예를 들어 16바이트 크기의 구조체를 고려해 보겠습니다:
struct nums {
std::int64_t first{};
std::int64_t second{};
};
nums construct() {
nums ret{10, 120};
return ret;
}
생성된 어셈블리 코드는 다음과 같습니다:
construct():
mov QWORD PTR [rbp-16], 10
mov QWORD PTR [rbp-8], 120
mov rax, QWORD PTR [rbp-16]
mov rdx, QWORD PTR [rbp-8]
ret
필드는 순차적으로 배치되며, first는 rax, second는 rdx에 저장됩니다. 메모리 레이아웃은 리틀엔디안 순서를 따르며 스택은 아래로 성장합니다.
- INTEGER 분류: 최대 두 개의 64비트 필드까지 레지스터에 맞습니다.
- 정렬: 필드는 8바이트 경계에 맞춰 정렬됩니다.
- 제한 조건: 직접 반환을 위해 총 크기는 ≤16바이트여야 합니다.
대용량 구조체: rdi에 숨겨진 인자 전달
16바이트를 초과하는 구조체는 MEMORY로 분류됩니다. 호출자는 메모리를 할당하고 그 주소를 rdi에 전달하여 숨겨진 첫 번째 인자로 전달합니다.
예시: 32바이트 크기의 구조체입니다:
struct many_nums {
std::int64_t first{};
std::int64_t second{};
std::int64_t third{};
std::int64_t fourth{};
};
many_nums construct_scary() {
many_nums temp{10, 20, 30, 40};
return temp;
}
construct_scary 함수 내부에서는 다음과 같이 동작합니다:
construct_scary():
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax], 10
mov QWORD PTR [rax+8], 20
mov QWORD PTR [rax+16], 30
mov QWORD PTR [rax+24], 40
ret
mov rax, [rbp-8]가 반복되는 것은 -O0 옵션의 산물입니다. 최적화된 코드에서는 rdi를 직접 사용합니다.
main 함수에서는 다음과 같습니다:
main:
sub rsp, 32
lea rax, [rbp-32]
mov rdi, rax
call construct_scary
ret
- 메모리 할당:
sub rsp, size또는 힙/스택을 통해 수행됩니다. - 주소 전달: 함수 호출 전에
rdi에 주소를 전달합니다. - MEMORY 분류: 시스템 V ABI에서 16바이트 이상이거나 비-INTEGER 타입인 경우 사용됩니다.
반환 값 분류 규칙
시스템 V ABI는 세 가지 범주를 정의합니다:
- INTEGER: 최대 16바이트, 스칼라 또는 집합형 필드가 ≤2×64비트 →
rax/rdx에 반환. - MEMORY: 16바이트 이상, SSE 패킹 타입이 >2×128비트, 또는 비-INTEGER 타입 → 숨겨진
rdi인자 사용. - SSE: 부동소수점 값 최대 128비트 →
xmm0에 반환 (본 문서에서는 다루지 않음).
GCC 및 Clang과 같은 컴파일러는 이러한 규칙에 따라 코드를 생성합니다. 디버깅 시 중간 단계를 명확히 보기 위해 -O0 옵션을 사용하세요.
핵심 요약
- 16바이트 이하의 정수 반환에는
rax/rdx를 사용하세요. MEMORY분류 타입의 경우, 호출자가rdi를 통해 메모리를 제공해야 합니다.- 시스템 V ABI는 리눅스와 맥OS 간 일관된 동작을 보장합니다.
- 최적화(
-O2+)는 불필요한mov명령어를 제거합니다. - 생성된 코드를 확인하려면
objdump -d또는 Godbolt Compiler Explorer를 활용하세요.
— Editorial Team
아직 댓글이 없습니다.