21.1.1.1.1. POSIX API 기반 독립 프로세스와 PX4 내부 스레드의 메모리 공간 공유(Flat Memory) 특성

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 플랫 메모리 환경 하의 사용자 모듈 개발 제약사항

  1. 동적 할당(Malloc / New)의 무분별한 사용 금지:
    C++ std::vectornew, malloc을 루프 문(Loop) 안에서 난사하면, 단일 메모리 공간인 전체 SRAM 시스템의 힙(Heap) 영역이 스위스 치즈처럼 구멍이 뚫리는 단편화(Fragmentation) 현상이 발생한다. 결국 메모리가 남아있음에도 크기 할당을 못 받아 다른 코어 센서 모듈이 죽어버린다.
  2. 무자비한 배열 선언 주의 (Stack Overflow):
    int buffer[1000]; (4000 Bytes) 같은 지역 변수 배열을 함수 내부에서 선언하면, 할당받은 꼬마 스택(예: 2000 Bytes) 용량을 즉시 뚫고 나가 바로 밑에 위치한 남의 태스크 스택 메모리를 짓뭉개 버린다(Stack Smashing).
  3. 전역 변수(Global Variables) 오염:
    모든 프로세스가 같은 데이터 영역(Data Segment, BSS)을 쳐다보고 있으므로, static이나 전역 변수 하나를 잘못 디자인하여 여러 커스텀 태스크에서 동시에 읽고 쓰면 데이터 경쟁 조건(Data Race) 버그가 작렬한다.

우리가 아무리 최신 C++14 패턴을 동원해 고상하게 커스텀 모듈을 설계한들, 하드웨어 바닥에서 입을 벌리고 있는 이 **‘플랫 메모리의 살얼음판’**을 인지하지 못하면 펌웨어는 단 10분을 버티지 못하고 다운(Hard Fault)된다.

그렇다면 플랫 메모리의 이 끔찍한 연쇄 붕괴 위험성을 막아줄 방법은 아예 없는 것일까? 이 질문에 답하기 위해, 21.1.1.1.2 단원에서는 NuttX가 필사적으로 도입하고 있는 ’보호(Protected) 빌드 모드’와 플랫(Flat) 모드의 아키텍처 전쟁에 대해 다룰 것이다.