21.3.1.1. extern "C" __EXPORT 지시자의 바이너리 레벨 역할
PX4 커스텀 모듈의 메인 함수 맨 앞자리를 굳건히 지키고 있는 두 개의 수식어, extern "C"와 __EXPORT는 단순한 장식품이 아니다. 이들은 C++ 컴파일러(GCC/Clang)와 링커(Linker)의 기본 동작 방식을 강제로 비틀어, NuttX 커널이 여러분의 함수를 무사히 찾아낼 수 있게 만드는 **‘바이너리 레벨의 나침반’**이다.
1. extern "C": 객체지향의 이름을 버리다
C++ 컴파일러는 기본적으로 오버로딩(Overloading)을 지원한다. 즉, 소스 코드 안에 void print(int a)와 void print(double a)라는 똑같은 이름의 함수가 동시에 존재할 수 있다.
컴파일러가 이 둘을 기계어 객체 파일(.obj) 안에서 구분하기 위해 쓰는 꼼수가 바로 **‘네임 맹글링(Name Mangling)’**이다. 컴파일 과정에서 함수명 뒤에 매개변수 타입들을 암호처럼 덧붙여 함수 이름 구성을 기괴하게 비틀어버리는 것이다.
예를 들어, 여러분이 순수하게 int custom_app_main(int argc, char *argv[]) 라고 코드를 짰다고 해보자. C++ 컴파일러를 거치고 나면, 바이너리 심볼 테이블에는 _Z15custom_app_mainiPPc 와 같이 도무지 인간이 읽을 수 없는 이름으로 등록되어 버린다.
이때 멍청하지만 정직한 NuttX 커널(C 언어 기반)이 custom_app_main이라는 정확한 텍스트 덩어리를 들고 링커 테이블을 찾아오면 कैसे(어떻게) 될까? 커널은 맹글링된 _Z15... 코드를 알아보지 못하고 영원히 함수를 찾지 못한 채 “Command not found” 에러를 내뱉을 것이다.
바로 이 비극을 막는 마법의 주문이 extern "C"이다.
이 지시어는 C++ 컴파일러의 멱살을 잡고 “이 함수의 이름만큼은 절대로 네임 맹글링을 하지 말고, C 언어 시절의 순수한 문자열 원형 그대로 심볼 테이블에 박아 넣어라!” 라고 강제하는 역할을 한다. (이 현상에 대한 자세한 메커니즘은 21.3.1.1.1 단원에서 추가로 다룬다.)
2. __EXPORT: 은둔형 외톨이를 광장으로 끌어내다
extern "C"로 이름을 순수하게 지켰다고 해서 안심하기엔 이르다. 최신 링커(Linker)들은 펌웨어 바이너리 크기를 극한으로 줄이기 위해, 소스 파일 안에서만 쓰이고 외부 파일에서 명시적으로 호출(Call)되지 않은 함수들은 가차 없이 ’데드 코드(Dead Code)’로 간주하여 심볼 테이블에서 삭제해 버리는 최적화(LTO 등)를 공격적으로 수행한다.
여러분의 custom_app_main 함수는 작성된 .cpp 파일 내부 그 어디에서도 자기 자신을 호출하지 않는다. 오직 NSH 터미널을 통해 런타임(Runtime)에 커널이 동적으로 찾아올 뿐이다. 링커 입장에서는 “이 함수는 아무 데서도 안 쓰이네? 용량도 없는데 날려버리자“라고 판단해버리기 딱 좋은 먹잇감이다.
이때 __EXPORT (주로 __attribute__((visibility("default"))) 로 치환됨) 지시자가 링커의 가위에 방패가 되어준다.
이 지시어는 링커에게 “이 함수는 당장 이 소스 코드 안에서 쓰이진 않지만, 실행 중에 누군가 외부(운영체제)에서 동적으로 이름을 검색해 호출할 함수이니 절대 지우지 말고 펌웨어 공용(Global) 심볼 테이블에 눈에 띄게 등재해 놓아라!” 라고 선언하는 강력한 보증 수표이다.
이 두 지시자가 합쳐져 빚어내는 extern "C" __EXPORT라는 방탄조끼 덕분에, 여러분의 메인 함수는 최적화의 칼날을 피해 살아남아 NSH(NuttShell) 심볼 검색 기능에 무사히 연동될 수 있다. 이어지는 단원에서는 이 NSH 환경에서의 동적 이름 검색 처리 메커니즘을 보다 생생하게 분석해 보자.