21.3.1.1.1. C++ 네임 맹글링(Name Mangling) 방지를 통한 NSH(NuttShell) 심볼 테이블 검색 기능 지원
우리가 앞서 작성한 extern "C" __EXPORT int custom_app_main(...) 지시어의 위력은, 런타임(Runtime)에 사용자가 NSH 터미널을 통해 기체와 소통할 때 비로소 그 진가를 발휘한다.
이 단원에서는 픽스호크에 USB 케이블을 꽂고 터미널 프로그램(QGroundControl의 MAVLink Console이나 Tera Term 등)을 열어 custom_app start라는 명령어를 타이핑했을 때, 펌웨어 내부에서 벌어지는 **‘텍스트 기반 심볼 검색(Symbol Lookup)’**의 극적인 드라마를 살펴보자.
1. NSH(NuttShell)의 내장 명령어 시스템 한계
NuttX 운영체제의 셸인 NSH는 리눅스의 bash 셸과 겉보기엔 비슷하지만, 구조적으로 결정적인 차이가 있다.
리눅스에서 여러분이 ls나 python 같은 명령어를 치면, bash 셸은 디스크의 /bin이나 /usr/bin 폴더를 뒤져서 독립된 실행 파일(Executable Binary)을 찾아내어 메모리에 로드(Load)한다.
하지만 플래시 메모리 용량이 극도로 부족한 픽스호크에는 그런 ‘독립된 실행 파일 시스템(File System)’ 조차가 존재하지 않거나 쓰이지 않는다. custom_app, ekf2, commander 등 수백 개의 모듈들은 이미 부팅 순간부터 하나의 거대한 펌웨어 덩어리(.px4)로 합쳐져 RAM에 통째로 올라가 있다.
따라서 NSH는 터미널로 입력된 텍스트 명령을 처리하기 위해 플래시 디스크를 뒤지는 대신, 자신의 몸통(RAM) 어딘가에 이미 로드되어 있는 수만 개의 함수 주소 보관소, 즉 ’시스템 심볼 테이블(System Symbol Table)’을 뒤져야만 한다.
2. 텍스트 매칭을 가로막는 컴파일러의 만행: Name Mangling
이 심볼 테이블 검색은 오직 100% 한 치의 오차도 없는 **‘문자열 매칭(String Matching)’**으로 일어난다. NSH 파서는 사용자가 custom_app이라고 치면, 뒤에 관습적으로 _main을 붙여 custom_app_main이라는 텍스트를 들고 테이블을 검색한다.
이때 만약 우리가 extern "C"라는 방어구를 입혀놓지 않았다면 어떤 끔찍한 일이 벌어질까?
C++ 컴파일러(GCC)는 함수 이름 오버로딩(Overloading)을 지원하기 위해, 컴파일 타임에 모든 C++ 함수의 이름을 뒤틀어버린다(Name Mangling).
- 원본 텍스트:
int custom_app_main(int argc, char *argv[]) - 맹글링된 텍스트:
_Z15custom_app_mainiPPc(의미: 함수명 길이 15자, 인자 1은int(i), 인자 2는 십자char포인터(PPc))
NSH 커널은 순수하게 custom_app_main이라는 키워드를 들고 테이블을 훑지만, 테이블에는 온통 _Z15... 같은 외계어만 적혀 있으므로 매칭에 철저하게 실패한다. 결과적으로 모듈은 컴파일되어 메모리에 멀쩡히 존재함에도 불구하고, 평생 사용자의 호출을 받지 못한 채 깊은 잠에 빠지게 된다.
3. extern "C"의 구원
우리가 메인 함수 껍데기에 extern "C"를 씌워주는 행위는, C++ 컴파일러에게 **“이 함수만큼은 C++의 객체지향적 특성(오버로딩)을 전부 포기할 테니, 제발 이름을 건드리지 말고 옛날 C 언어 시절처럼 custom_app_main이라는 순수한 문자열 원형 그대로 심볼 테이블에 박아 다오”**라고 애원하는 것과 같다.
이 선언 덕분에 링커(Linker)는 맹글링 되지 않은 순수한 텍스트를 테이블에 등재한다. 그리고 NSH 터미널에서 사용자가 엔터를 치는 순간, 문자열 매칭 알고리즘이 완벽하게 들어맞으며 함수 포인터(메모리 주소)를 성공적으로 반환해 낸다. 커널은 이 주소로 점프하여 스레드를 생성하고, 마침내 여러분의 커스텀 앱이 PX4라는 거대한 무대 위에서 첫 숨을 쉬게 되는 것이다.
모듈의 진입문(Entry Point)을 성공적으로 열었다면, 이제 이 문을 통과하면서 커널이 던져주는 택배 상자, 즉 argc와 argv[] 배열이 어떻게 전달되고 해체되는지 다음 단원(21.3.1.2)에서 뜯어볼 차례다.