TECH 으로 돌아가기
TECH HACKER NEWS 오늘 7분 읽기 32 READS

C를 지키려면 ABI부터 손봐야 한다: 링커가 눈감는 조용한 재앙

C를 지키려면 ABI부터 손봐야 한다: 링커가 눈감는 조용한 재앙
SOURCE IMAGE · HACKER NEWS

ABI(Application Binary Interface, 응용 이진 인터페이스)는 소스 코드 어디에도 적히지 않지만 프로그램이 실제로 동작하려면 반드시 지켜져야 하는 계약이다. 함수 하나를 선언하고 호출하는 순간, 컴파일러는 개발자 대신 다른 컴파일러와 다른 기계를 향해 '이 값은 어느 레지스터에 담고, 반환값은 어디로 돌려준다'는 약속을 맺는다. 이 계약은 문법이나 타입 검사와 달리 프론트엔드에서 드러나지 않으며, 링크와 실행 단계에서야 그 실체가 문제로 불거진다. ABI 안정성을 강하게 비판해 온 저자가 이번에는 'C를 살리려면 ABI를 살려야 한다'는, 언뜻 모순처럼 들리는 제목을 내건 이유가 여기에 있다.

타입 하나만 바꿔도 계약이 달라진다

저자는 x86_64 환경에서 간단한 실험으로 이 계약의 실체를 드러낸다. long long(이 경우 64비트) 인자를 받는 do_stuff 함수는 rdi 레지스터 하나만 사용해 값을 넘기고 rax로 반환한다. 그런데 인자 타입을 __int128_t로 바꾸는 순간, 호출 규약은 rdi와 rsi 두 레지스터를 함께 쓰도록 달라지고, 반환값도 rax와 rdx 두 곳에 나뉘어 담긴다. 함수 본문을 들여다보지 않아도, 단지 시그니처만으로 컴파일러는 128비트를 두 레지스터에 나눠 담는다는 규약을 확정한다. 소스 코드 수준의 약속이 아니라 컴파일러가 하드웨어 레지스터 단위까지 못 박아 둔 약속이라는 점이 핵심이다.

C의 ABI가 특히 취약한 것은 이름 맹글링(name mangling)이 없기 때문이다. C++는 인자 타입을 함수 이름에 인코딩해 _Z8do_stuffn 같은 심볼을 만들어 내므로, 시그니처가 다르면 링커가 서로 다른 심볼로 인식한다. 반면 C에서 do_stuff는 그냥 do_stuff다. 링커 입장에서 필요한 정보는 오직 심볼 이름 하나뿐이며, do_stuff라는 이름의 심볼만 찾으면 타입이 무엇이든 그대로 연결한다. 저자가 C에 맹글링을 도입하자고 주장하지 않는 것은, Clang의 [[overloadable]] 속성을 빼면 어떤 구현도 그 길을 가지 않을 것이기 때문이다.

링커는 타입을 검사하지 않는다

그래서 선언은 long long인데 정의는 __int128_t인 두 파일을 함께 빌드해도, 링커는 이 둘이 완전히 호환되지 않는다는 사실에 아무런 관심이 없다. 심볼 이름만 같으면 묶어 버린다. 디버그 모드에서는 운 좋게 그럴듯한 오류 창을 볼 수도 있지만, 릴리스 빌드에서는 사정이 다르다. 잘못된 레지스터를 건드리거나, 엉뚱한 메모리를 덮어쓰거나, 스택의 다른 부분을 망가뜨린다. 문제는 실제 원인에서 멀리 떨어진 지점에서 터지기 때문에, 세그멘테이션 폴트가 나면 그나마 다행이고 코어 덤프가 힌트라도 주면 행운이다. 이 모든 가정이 일반적인 소스 코드 개발자에게는 완전히 보이지 않는 층위에서 작동한다는 점이 이 버그를 특히 고약하게 만든다.

더 무서운 대목은 헤더를 제대로 두어도 이 문제가 발생할 수 있다는 것이다. 헤더에 extern long long do_stuff(long long value);라고 정확히 적혀 있고 구현 파일도 겉보기에 멀쩡해도, 정작 실행 시점에 연결되는 공유 라이브러리의 심볼이 다른 규약을 따르면 그대로 깨진다. ABI 파손은 프론트엔드도 링커도 불평하지 않는 지점에서 일어나는 것이 본질이며, 그래서 헤더와 소스 파일만의 문제가 아니라 라이브러리라는 완전히 새로운 문제의 원천을 낳는다.

배포판을 떠받치는 소수의 사람들

이 취약함이 가장 선명하게 드러나는 곳이 리눅스 배포판이다. apt, yum, pacman 같은 저장소로 갱신되는 동적 라이브러리는 반드시 하위 호환성을 유지해야 하고, 시스템의 모든 패키지가 합의된 libc를 공유해야 한다. 저자의 표현대로, 각 배포판과 붕괴 사이에 서 있는 사람은 극소수의 패키저와 릴리스 매니저, 메인테이너뿐이다. 공유 라이브러리가 존재하는 순간 진실의 원천은 둘로 나뉜다. 애플리케이션을 컴파일할 때 참조하는 헤더와 그 인터페이스가 하나이고, 실제 심볼과 구현을 담은 공유 라이브러리가 다른 하나다. 잘못된 헤더로 컴파일하면 정의가 어긋났다는 경고조차 없이, 같은 이름의 것은 같은 방식으로 동작하리라는 가정만으로 모든 것이 굴러가다가 어긋난다.

한국의 실무자에게 이 이야기는 추상적인 언어 논쟁이 아니라 운영 환경의 안정성 문제로 직결된다. C가 워낙 방대한 소프트웨어의 바닥을 떠받치고 있고 C++가 C 표준 라이브러리를 거의 그대로 흡수하기 때문에, C의 ABI 문제는 사실상 모든 진영의 문제가 된다. MSVC의 STL이 이진 호환성 때문에 여전히 32비트 비트 연산에 묶여 성능을 손해 보는 사례처럼, 과거의 결정이 영구적 제약으로 굳어 설계 공간과 성능을 갉아먹는다. 이 글이 던지는 실무적 교훈은 명확하다. 라이브러리를 갱신할 때 헤더와 바이너리의 규약이 진짜로 일치하는지, 타입 폭이 바뀌는 변경이 조용히 섞여 들어오지 않았는지를 링커의 침묵에 기대지 말고 별도의 규율로 지켜야 한다는 것이다. 다만 이 글 자체는 문제의 구조를 해부하는 데 초점을 맞출 뿐, 어떻게 ABI를 실제로 고쳐 나갈지에 대한 구체적 처방까지는 다음 논의로 남겨 둔다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://thephd.dev/to-save-c-we-must-save-abi-fixing-c-funct...
SHARE
처리 중...