21.1.1.1.1. POSIX API 기반 독립 프로세스와 PX4 내부 스레드의 메모리 공간 공유(Flat Memory) 특성
사용자 정의 앱(Custom Application)이 응용 계층에 머무르든 미들웨어 계층에 머무르든, 우리가 작성하는 C++ 소스 코드는 겉보기에는 전형적인 리눅스(Linux) 프로그래밍과 매우 흡사하게 생겼다. 메인 함수(custom_module_main(int argc, char *argv[]))가 존재하고, printf를 사용하며, POSIX 호환 API인 pthread, sleep, open, write 등을 거침없이 호출한다.
이러한 형태적 유사성 때문에 많은 개발자들이 **“아하, 내 커스텀 모듈은 리눅스의 데몬 프로세스(Daemon Process)처럼 격리된 가상 메모리를 통째로 할당받는구나!”**라고 착각하곤 한다. 이 치명적인 오해가 훗날 PX4 기체 추락(Crash)을 유발하는 메모리 침범 버그(Segmentation Fault가 아닌 Hard Fault)의 씨앗이 된다.
1. 리눅스 프로세스(Linux Process)와 전혀 다른 ’플랫 메모리(Flat Memory)’의 늪
리눅스나 Windows 같은 거대한 데스크톱 운영체제는 하드웨어 MMU(Memory Management Unit)를 이용해 각 프로세스마다 독립적이고 거대한 가상 메모리 공간(Virtual Memory Space)을 선물한다. 프로세스 A의 코드가 미쳐 날뛰어 포인터 주소 0x1234를 마음대로 오염시킨다고 해도 운영체제가 그 즉시 프로세스 A만 암살(Segfault)시켜 버릴 뿐, 프로세스 B나 커널에는 아무런 피해가 가지 않는다.
그러나 대부분의 픽스호크(Pixhawk) 시리즈 보드에 탑재되는 STM32 계열 마이크로컨트롤러(MCU)와 그 위에서 구동되는 NuttX RTOS 기반의 PX4 펌웨어는 기본적으로 **‘플랫 메모리 모델(Flat Memory Model)’**로 컴파일되고 동작한다.
- 가상 메모리의 부재: 통째로 단일한 물리적 주소 공간(SRAM)만을 사용한다. 즉, PX4 시스템 내에 띄워진 수십 개의 프로세스(태스크)와 스레드들은 정확히 동일한 주소 공간 지도를 공유하고 있다.
- 격리벽(Isolation) 붕괴: 사용자가 만든 커스텀 모듈(
custom_app) 안에서 실수로 어떤 엉뚱한 포인터(*ptr) 1바이트의 값을0으로 덮어썼는데, 하필 그 물리적 메모리 번지가 자세 제어기(Attitude Controller)의 모터 출력 변수였다면? 보호 장치(MMU Page Fault) 따위는 발동하지 않는다. 제어기는 그 오염된 값을 그대로 읽고 드론을 땅으로 내리꽂게 된다.
2. 독립 프로세스(Task)의 껍데기를 쓴 스레드(Thread)
따라서 NSH(NuttShell) 콘솔에서 custom_app start를 쳤을 때 NuttX RTOS가 만들어주는 것은, 리눅스와 같은 철통 보안의 ’독립 프로세스’가 아니다. 겉모양만 POSIX의 외피를 두르고 환경 변수(Environment Variables)와 파일 디스크립터(File Descriptor)를 개별적으로 조금씩 가지고 있을 뿐, 실제 물리적 메모리 헌법하에서는 **거대한 하나의 프로그램 안에서 돌아가는 커다란 스레드(Thread)**에 불과하다.
이 거대한 단일 플랫 메모리 풀 안에서 각 태스크(사용자 앱 포함)는 펌웨어 부팅 시 또는 실행 시점에 각자의 스택(Stack) 영역 메모리 덩어리를 아주 쪼잔하게(예: 1200 Bytes, 2000 Bytes) 떼어받아 살아가게 된다.
2.1 플랫 메모리 환경 하의 사용자 모듈 개발 제약사항
- 동적 할당(Malloc / New)의 무분별한 사용 금지:
C++std::vector나new,malloc을 루프 문(Loop) 안에서 난사하면, 단일 메모리 공간인 전체 SRAM 시스템의 힙(Heap) 영역이 스위스 치즈처럼 구멍이 뚫리는 단편화(Fragmentation) 현상이 발생한다. 결국 메모리가 남아있음에도 크기 할당을 못 받아 다른 코어 센서 모듈이 죽어버린다. - 무자비한 배열 선언 주의 (Stack Overflow):
int buffer[1000];(4000 Bytes) 같은 지역 변수 배열을 함수 내부에서 선언하면, 할당받은 꼬마 스택(예: 2000 Bytes) 용량을 즉시 뚫고 나가 바로 밑에 위치한 남의 태스크 스택 메모리를 짓뭉개 버린다(Stack Smashing). - 전역 변수(Global Variables) 오염:
모든 프로세스가 같은 데이터 영역(Data Segment, BSS)을 쳐다보고 있으므로,static이나 전역 변수 하나를 잘못 디자인하여 여러 커스텀 태스크에서 동시에 읽고 쓰면 데이터 경쟁 조건(Data Race) 버그가 작렬한다.
우리가 아무리 최신 C++14 패턴을 동원해 고상하게 커스텀 모듈을 설계한들, 하드웨어 바닥에서 입을 벌리고 있는 이 **‘플랫 메모리의 살얼음판’**을 인지하지 못하면 펌웨어는 단 10분을 버티지 못하고 다운(Hard Fault)된다.
그렇다면 플랫 메모리의 이 끔찍한 연쇄 붕괴 위험성을 막아줄 방법은 아예 없는 것일까? 이 질문에 답하기 위해, 21.1.1.1.2 단원에서는 NuttX가 필사적으로 도입하고 있는 ’보호(Protected) 빌드 모드’와 플랫(Flat) 모드의 아키텍처 전쟁에 대해 다룰 것이다.