ABI
ABI 는 이미 컴파일된 프로그램 조각들이 서로 맞물리려고 지키는 기계 수준의 약속입니다. 함수를 부를 때 값을 어느 자리에 놓는지를 정합니다. 같은 약속을 따른 조각들은 소스를 다시 컴파일하지 않고도 붙습니다.
상세
전기 플러그는 나라마다 구멍 모양과 전압이 정해져 있습니다. 그 규격이 같으면 어느 회사가 만든 기기든 벽에 그냥 꽂힙니다.
ABI(Application Binary Interface, 응용 프로그램 이진 인터페이스)는 따로 컴파일된 기계어 조각이 서로를 부르고 값을 주고받을 수 있도록 정해 둔 규약입니다. 소스 코드 수준이 아니라 기계어와 메모리 배치 수준에서 정합니다. 한 묶음으로 정해지는 것은 대체로 이렇습니다.
- 호출 규약. 인자를 어느 레지스터나 스택 자리에 놓나, 반환값을 어디서 받나, 어느 레지스터를 부른 쪽이 지켜 줘야 하나
- 스택. 프레임을 어떻게 쌓나, 호출 직전에 스택 포인터가 몇 바이트 경계에 맞아 있어야 하나
- 자료형. 정수와 실수와 포인터가 각각 몇 바이트인가, 구조체 안의 필드가 어느 오프셋에 놓이나
- 심볼 이름. 소스에 적은 함수 이름을 목적 파일 안에서 어떤 문자열로 적나
- 목적 파일 형식. 코드와 데이터와 심볼 표를 파일 안에 어떻게 늘어놓나
- 시작 상태. 프로그램이 처음 제어를 받는 순간 레지스터와 스택에 무엇이 들어 있나
부르는 쪽과 불리는 쪽은 서로의 소스를 안 봅니다. 부르는 쪽은 약속된 자리에 인자를 넣고 뜁니다. 불리는 쪽은 약속된 자리에서 인자를 꺼냅니다. 돌아올 때도 약속된 자리에 결과를 둡니다.
flowchart TD
A["부르는 쪽 기계어"] -->|"약속된 자리에 인자를 넣는다"| B["레지스터와 스택"]
B -->|"약속된 자리에서 인자를 꺼낸다"| C["불리는 쪽 기계어"]
C -->|"약속된 자리에 반환값을 넣는다"| B
B -->|"약속된 자리에서 반환값을 꺼낸다"| A
이 자리들은 양쪽이 같은 약속을 알고 있을 때만 맞물립니다. 약속은 프로세서 계열과 운영체제 조합마다 따로 정해집니다. 레지스터의 개수와 이름부터 다르기 때문입니다. 그래서 ABI 는 하나가 아니라 조합마다 하나씩 있습니다. 언어 런타임이나 컴파일러가 자기 영역에서 더 얹어 정하는 몫도 따로 있습니다.
배경
한 사람이 소스를 통째로 들고 한 번에 컴파일하면 이런 약속을 따로 적어 둘 일이 없습니다. 컴파일러가 자기 규칙만 일관되게 지키면 그만입니다. 조각을 나눠 만들기 시작하자 사정이 달라집니다.
라이브러리는 만든 쪽이 이미 컴파일해서 내놓습니다. 쓰는 쪽에는 소스가 없거나, 있어도 다시 컴파일할 형편이 안 됩니다. 운영체제와 그 위에서 도는 프로그램도 서로 다른 사람이 서로 다른 컴파일러로 만듭니다. 이 조각들이 붙어서 돌려면 소스가 아니라 기계어 수준에서 같은 약속을 알고 있어야 합니다. 인자를 어디에 두는지, 이름을 어떤 문자열로 적는지, 구조체를 어떤 순서와 간격으로 늘어놓는지입니다.
필요한 약속은 소스 수준의 약속과 결이 다릅니다. 헤더 파일과 함수 시그니처는 소스를 다시 컴파일하는 쪽에서만 지켜지면 됩니다. 기계어 수준의 약속은 다시 컴파일하지 않는 쪽까지 묶습니다. 두 약속이 따로 놀기 때문에 이름도 따로 붙었습니다. 소스 쪽을 API(Application Programming Interface, 응용 프로그램 프로그래밍 인터페이스), 기계어 쪽을 ABI 라고 부릅니다.
예시
x86-64 인자 전달과 스택 정렬
System V AMD64 ABI 문서는 함수 호출 순서를 절 하나에 못 박아 두었습니다. 인자를 먼저 부류로 나눕니다. 부류가 정해지면 왼쪽에서 오른쪽 순서로 레지스터를 배정합니다.
1. If the class is MEMORY, pass the argument on the stack.
2. If the class is INTEGER, the next available register of the sequence %rdi, %rsi, %rdx,
%rcx, %r8 and %r9 is used.
3. If the class is SSE, the next available vector register is used, the registers are taken
in the order from %xmm0 to %xmm7.
정수 부류로 분류된 인자는 %rdi %rsi %rdx %rcx %r8 %r9 순서로 들어갑니다. 벡터 부류는
%xmm0 부터 %xmm7 까지를 차례로 씁니다. 메모리 부류는 스택으로 갑니다. x87 계열 부류도 메모리로
전달됩니다.
스택에는 정렬 요구가 붙습니다. 입력 인자 영역의 끝은 16바이트 경계에 정렬되어 있어야 합니다. 달리
말하면 함수 진입점으로 제어가 넘어가는 시점에 %rsp + 8 값이 언제나 16의 배수입니다. __m256 이나
__m512 가 스택으로 전달되면 그 경계가 32 또는 64바이트가 됩니다. %rsp 자체는 프로세스 시작
시점에 16바이트 정렬이 보장됩니다.
C++ 이름 맹글링
Itanium C++ ABI 문서는 맹글링된 이름의 일반 구조를 이렇게 적습니다.
<mangled-name> ::= _Z <encoding>
::= _Z <encoding> . <vendor-specific suffix>
<encoding> ::= <function name> <bare-function-type>
::= <data name>
::= <special-name>
이름 앞에 _Z 를 붙입니다. 그 뒤에 이름을 인코딩합니다. 함수라면 오버로딩을 구별하려고 타입까지 같이
인코딩합니다. 같은 문서의 예제를 보면 Foo 라는 인라인 네임스페이스 안에 있는 A f() { } 가
_ZN3Foo1fEv 로 맹글링됩니다.
파이썬 확장 모듈의 Py_LIMITED_API 와 abi3
파이썬 공식 문서는 확장 모듈이 여러 파이썬 버전에서 다시 컴파일하지 않고 돌게 하는 장치를 둡니다.
Python.h 를 포함하기 전에 Py_LIMITED_API 를 정의하면 Limited API 만 쓰겠다고 선언하는 것입니다.
동시에 어느 버전의 Limited API 를 쓸지 고르는 것이기도 합니다. 값은 지원하려는 가장 낮은 파이썬 버전에
해당하는 PY_VERSION_HEX 입니다. 그렇게 만든 확장은 그 버전부터 그 뒤의 모든 Python 3 릴리스와
ABI 호환입니다.
파일 이름에도 흔적이 남습니다. 일부 플랫폼에서 파이썬은 abi3 태그가 붙은 공유 라이브러리 파일을
찾아 적재합니다. mymodule.abi3.so 같은 이름입니다. 다만 그렇게 이름 붙은 확장이 실제로 Stable ABI
를 지키는지 검사하지는 않습니다.
안드로이드 NDK 의 ABI 이름 목록
ABI 가 배포 단계에서 문자열 값으로 굳어 있는 자리도 있습니다. 안드로이드 NDK(Native Development Kit) 문서는 지원하는 ABI 를 표로 적어 둡니다.
| ABI | 지원 명령어 집합 | 문서가 붙인 단서 |
|---|---|---|
armeabi-v7a |
armeabi, Thumb-2, Neon | ARMv5/v6 기기와 호환되지 않음 |
arm64-v8a |
AArch64 | Armv8.0 만 |
x86 |
x86(IA-32), MMX, SSE/2/3, SSSE3 | MOVBE 와 SSE4 는 지원하지 않음 |
x86_64 |
x86-64, MMX, SSE/2/3, SSSE3, SSE4.1, 4.2, POPCNT, CMPXCHG16B, LAHF-SAHF | x86-64-v2 전체 |
문서는 예전에 지원하던 것도 함께 적어 둡니다. 역사적으로 ARMv5(armeabi)와 32비트·64비트 MIPS 도
지원했습니다. 그 ABI 들의 지원은 NDK r17 에서 제거되었습니다.
경계
공개한 함수 시그니처는 하나도 안 바꿨습니다. 구조체 안에 필드만 하나 끼워 넣었습니다. 이 구조체를 쓰던 소스는 고칠 데가 없습니다. 이것도 호환이 깨진 것인가. 소스 호환은 살아 있고 바이너리 호환만 깨집니다.
가르는 선은 다시 컴파일해야 하느냐입니다. 필드를 끼워 넣으면 그 뒤에 오는 필드들의 오프셋과 구조체 전체의 크기가 바뀝니다. 헤더를 보고 새로 컴파일한 쪽은 새 오프셋으로 값을 읽고 씁니다. 이미 컴파일되어 배포된 쪽은 옛 오프셋을 기계어 안에 박아 둔 채입니다. 소스는 양쪽 다 그대로 컴파일되므로 소스 수준에서는 아무 일도 일어나지 않습니다.
파이썬 공식 문서가 이 두 가지를 나란히 놓고 가릅니다. 파이썬의 C API 는 따로 적어 두지 않는 한 하위 호환 정책인 PEP(Python Enhancement Proposal) 387 의 적용을 받습니다. 그에 대한 변경은 대부분 소스 호환입니다. 대개 새 API 를 더하는 식이기 때문입니다. 기존 API 를 바꾸거나 없애는 것은 폐기 기간을 거친 뒤이거나 심각한 문제를 고칠 때만 합니다. 그리고 CPython 의 ABI 는 마이너 릴리스 하나 안에서 앞뒤로 호환됩니다. 같은 방식으로 컴파일했을 때라는 단서가 붙습니다. Python 3.10.0 용으로 컴파일한 코드는 3.10.8 에서 돕니다. 그 반대도 됩니다. 다만 3.9.x 와 3.11.x 용으로는 따로 컴파일해야 합니다.
두 약속의 유효 범위가 여기서 갈립니다. 소스 쪽은 폐기 기간을 두는 하위 호환 정책이 받칩니다.
기계어 쪽은 파이썬 공식 문서가 마이너 릴리스 하나를 단위로 적어 둡니다. Py_LIMITED_API 로 만든
확장은 그 범위가 다릅니다.
관련 항목
ABI를 이루는 구성 요소
호출 규약 · 레지스터 · 스택 · 스택 프레임 · 스택 정렬 · 자료형 정렬 · 구조체 · 구조체 레이아웃 · 메모리 · 목적 파일 형식 · 심볼 · 심볼 버저닝 · 이름 맹글링 · 엔디언 · 프로세스
ABI 규약이 실제로 걸리는 소프트웨어 계층
컴파일러 · 링커 · 동적 링커 · 로더 · 라이브러리 · 공유 라이브러리 · 정적 라이브러리 · 시스템 콜 · 커널
약속을 적어 둔 문서
System V ABI · Itanium C++ ABI · Stable ABI · Limited API · ELF · 안드로이드 NDK · AAPCS64
ABI가 갈리는 프로세서·운영체제 조합
x86 · ARM · MIPS · 운영체제 · 명령어 집합 · 타깃 트리플 · 크로스 컴파일
호환성이 갈리는 지점과 거기서 나는 문제
API · 소스 호환 · 바이너리 호환 · PEP 387 · 헤더 파일 · 함수 시그니처 · 의존성 지옥 · 버전 스큐
ABI가 배포·전달되는 경로
언어가 자기 영역에서 더 얹는 몫
다른 이름: Application Binary Interface · 응용 프로그램 이진 인터페이스 · 바이너리 인터페이스