C 확장, 이식성, 대체 컴파일러에 관하여
2 days ago
3
- ISO C 표준만 따르는 코드는 드물고, 실제 C 코드베이스는 기능 추가와 컴파일러·라이브러리별 공백 우회를 위해 비표준 확장에 의존함
- 유용한 C 컴파일러는 <stdio.h> 같은 시스템 헤더부터 처리해야 하지만, glibc는 __attribute__((packed)), #include_next 같은 GNU 확장과 가정으로 장벽을 만듦
- SDL의 byteswapping 로직은 ISA 매크로가 있으면 inline assembly를 택할 수 있어, GCC·clang이 아닌 컴파일러도 GCC식 확장을 요구받을 수 있음
- OpenBSD와 Gnulib의 extern inline 처리는 C99와 GCC 의미 차이, 플랫폼별 분기, _FORTIFY_SOURCE 조건 때문에 inline 의미론 호환을 복잡하게 만듦
- 작은 C 컴파일러는 upstream 패치, downstream 패치, 전용 가드 확보, GCC 호환성 흉내 중 선택해야 하며 기능 테스트 매크로 확대가 더 나은 방향으로 보임
glibc 헤더가 만드는 첫 장벽
- 유용한 C 컴파일러가 되려면 시스템 C 라이브러리 헤더를 전처리하고 파싱할 수 있어야 하며, <stdio.h>를 처리하지 못하면 hello world도 통과하기 어려움
- GNU/Linux 환경에서는 이 장벽이 glibc로 이어짐
- glibc는 거의 모든 libc 헤더가 간접적으로 포함하는 sys/cdefs.h에서 컴파일러가 미리 정의한 매크로를 검사해 지원되는 확장을 판별함
- 지원되지 않는 확장은 관련 정의를 없애는 식으로 처리하지만, 이 호환성 로직도 실제로 깨질 수 있음
-
struct epoll_event와 __attribute__((packed))
- Linux의 sys/epoll.h에 있는 struct epoll_event는 GNU __attribute__((packed))를 쓰는 packed struct임
- 이 속성은 64비트에서 구조체 레이아웃을 바꾸므로, 무시하면 ABI가 깨짐
- 컴파일러가 __attribute__((packed))를 구현해도 충분하지 않음
- sys/cdefs.h에는 GCC, clang, tcc가 아니면 __attribute__(xyz)를 빈 매크로로 정의하는 코드가 있음
- 그 결과 다른 컴파일러는 packed 속성을 지원하더라도 glibc 헤더에서 해당 속성이 제거될 수 있음
- epoll 헤더는 Linux 전용이므로 C 표준 이식성 기준을 그대로 적용하기 어렵다는 반론도 가능함
-
limits.h와 #include_next
- stddef.h, stdint.h, limits.h, float.h 같은 일부 C 헤더는 freestanding 구현에도 필요해 컴파일러가 제공해야 함
- POSIX는 표준 C 상수 외에도 POSIX 전용 상수를 limits.h에 정의하도록 요구하므로, 컴파일러의 limits.h 위에 플랫폼별 limits.h가 필요함
- glibc의 <limits.h>는 GNU C가 아니면 ANSI limits.h 값을 직접 정의하고, GCC 환경에서는 #include_next <limits.h>로 컴파일러 헤더를 가져옴
- 이 구조는 GCC 전용 builtin limits.h가 특정 매크로를 정의한다고 가정하며, #include_next 확장에도 의존함
- clang도 이 구조를 우회 처리해야 함
SDL의 기능 감지와 inline assembly 문제
- SDL_endian.h의 byteswapping 함수는 가능한 경우 컴파일러 builtin이나 inline assembly를 쓰고, 마지막 수단으로 일반 비트 연산 구현으로 대체함
- 감지 로직은 대략 다음 순서로 동작함
- GCC 또는 clang이고 __has_builtin(__builtin_bswapX)가 있으면 builtin 사용
- MSVC 8.0 이상이면 MSVC intrinsic #pragma 사용
- __x86_64__ 같은 ISA별 매크로가 정의되어 있으면 inline assembly 사용
- 그 외에는 일반 비트 연산 구현 사용
- GCC나 clang이 아닌 컴파일러가 합리적인 이유로 ISA별 predefined macro를 정의하면 이 순서가 문제가 됨
- 해당 컴파일러가 bswap builtin과 __has_builtin 특수 연산자를 제공해도, 로직상 GCC식 inline assembly를 쓰려고 할 수 있음
- 결과적으로 알 수 없는 컴파일러도 GCC 스타일 inline assembly를 지원한다고 기대하는 구조가 됨
OpenBSD libc와 extern inline의 혼란
- OpenBSD의 일부 헤더는 최적화 시 컴파일러가 선택적으로 사용할 inline 함수 정의를 포함함
- 이 함수들은 __only_inline 매크로로 정의되며, 컴파일러가 실제로 inline하지 않으면 외부 심볼로 대체해야 함
- 즉, extern linkage를 가진 inline 함수가 필요함
-
C99 inline과 GCC inline 의미 차이
- inline은 C99에 명시되어 있지만, 표준 동작은 C99 이전의 비표준 GCC 동작과 충돌함
- 헤더 안 inline 정의는 함수 본문과 함께 extern inline을 써야 하며, 이 경우 실제 exported function을 emit하지 않음
- translation unit에서는 함수 정의를 export하기 위해 inline만 붙여 선언해야 함
- inline의 의미는 C++과 C에서도 다름
- 이 차이는 Youtao Guo의 글에서 자세히 다뤄짐
-
OpenBSD의 __only_inline
- OpenBSD는 GCC inline semantics에 의존함
- GCC 버전 차이를 덮기 위해 sys/cdefs.h의 __only_inline 매크로는 최신 GCC에서 명시적 __attribute__로 예전 gnu89 inline semantics를 지정함
- 비-GNU 컴파일러에서는 __only_inline이 static linkage로 정의됨
- 그 결과 함수가 서로 충돌하는 linkage로 선언·정의되어 깨질 수 있음
-
_ANSI_LIBRARY 우회
- OpenBSD는 _ANSI_LIBRARY 매크로를 존중함
- 이 매크로를 정의하면 signal.h 같은 표준 헤더에서 깨지는 __only_inline 정의 사용을 완전히 생략함
- 최적화된 버전은 얻지 못하지만, 최소한 빌드는 동작함
-
Gnulib의 extern inline 호환성 코드
- Gnulib의 extern inline 호환성 코드는 Guile과 nano를 빌드할 때도 등장함
- extern-inline.m4는 이 C corner case의 깨지고 이상한 구현들을 처리하기 위해 복잡한 조건 분기를 포함함
- 조건에는 Apple, DragonFly, FreeBSD, GCC, clang, PCC, HP cc, PGI, SunPro C, _FORTIFY_SOURCE, __GNUC_STDC_INLINE__, __GNUC_GNU_INLINE__ 같은 환경 차이가 반영됨
Android bionic의 clang 가정
- bionic은 Android의 libc이며, 헤더가 GCC보다 clang을 강하게 가정함
- bionic 헤더는 nullability checks를 위해 _Nonnull, _Null_unspecified 같은 clang 전용 확장을 많이 사용함
- 이런 매크로는 명령줄 플래그로 #define해 없애기 어렵지 않음
- Termux를 통해 Android 휴대폰을 네이티브 aarch64 개발 환경으로 사용할 때 bionic 헤더에서 이 문제가 드러남
- _Null_unspecified는 __BIONIC_COMPLICATED_NULLNESS로도 불리며, 관련 정의는 bionic의 sys/cdefs.h에 있음
작은 C 컴파일러가 마주하는 선택지
- ISO C 표준만 따르는 코드는 현실에서 드물고, 많은 C 코드베이스가 비표준 동작과 언어 확장에 의존함
- 이런 의존은 추가 기능뿐 아니라 컴파일러와 라이브러리마다 다른 버그·공백을 우회하는 과정에서도 생김
- 여러 환경을 지원하려는 코드베이스는 전처리기 검사와 가드에 의존하지만, 이 방식은 쉽게 깨지고 다루기 까다로움
- antcc 같은 C 컴파일러를 만들 때 이런 호환성 문제가 반복해서 드러남
- 많은 오픈소스 프로젝트가 필수적이지 않은 일에도 컴파일러별 비표준 확장과 동작에 의존하면, 대체 컴파일러의 대응 부담이 커짐
- 동시에 모든 개발자가 작고 덜 알려진 컴파일러까지 포함해 여러 컴파일러에서 C 코드를 테스트해야 한다고 요구하기도 어려움
- C 이식성은 그 자체로 충분히 어려움
- 컴파일러 작성자 입장에서 가능한 선택지는 네 가지임
- 비호환성을 upstream에 패치하려고 시도함
- 충분히 유명해져서 개발자들이 전용 #ifdef 검사와 기본 테스트를 추가하게 만듦
- downstream에서 처리하고, 패치나 별도 패치를 배포함
- 특정 버전의 GCC인 척하고 해당 확장을 구현함
- upstream 패치는 이기기 어려운 싸움처럼 보이고, downstream 패치가 가장 쉬운 방법임
- 많은 코드베이스를 사용자와 개발자에게 최소한의 혼란으로 지원하려면 GCC 호환성 흉내가 현실적이지만 구현 부담이 큼
- clang은 GCC 4.2.1 호환을 주장하기 위해 __GNUC__=4, __GNUC_MINOR__=2, __GNUC_PATCHLEVEL__=1을 정의함
- clang은 지금은 별도 지원 대상에 가깝지만, Linux kernel을 clang으로 컴파일하게 만들기 위해 두 프로젝트 모두에 패치가 필요했을 정도로 큰 노력이 들어감
GCC 매크로와 따라잡기 문제
- GCC인 척하는 방식에도 문제가 있음
- 많은 코드베이스가 #ifdef __GNUC__만 검사하고 버전 확인 없이 최신 GCC 확장을 사용할 수 있음
- 이 경우 대체 컴파일러는 계속 따라잡기를 해야 함
- clang이 4.2.1보다 최신 GNU 확장을 지원하면서도 __GNUC__ 매크로 값을 올리지 않는 이유 중 하나가 여기에 있음
- 관련 배경은 LLVM의 __GNUC__ minor 버전 상향 논의에 있음
더 나은 방향과 현재 상태
- 이상적으로는 컴파일러별 가드와 버전 검사 대신 기능 테스트 매크로가 더 널리 쓰여야 함
- 유용한 기능 테스트 매크로에는 __has_builtin, __has_feature, __has_attribute가 있음
- 표준 매크로인 __STDC_NO_VLA__ 같은 방식도 더 많이 쓰일 수 있음
- 현재 *NIX 세계에서는 좋든 나쁘든 GCC/clang 준-양강 체제가 기본 상태임
- 독립적인 작은 C 컴파일러 개발도 이어지고 있음
-
Homepage
-
개발자
- C 확장, 이식성, 대체 컴파일러에 관하여