20.5.2 C/C++ 기반 Zenoh 애플리케이션 트러블슈팅
MCU(Micro Controller Unit)나 운영체제 자원이 극도로 제한된 레거시 산업용 컴퓨터, 빠른 카메라 비전 처리를 필요로 하는 임베디드 장비에서는 예외 없이 C 또는 C++이 사용된다. zenoh-c API 기반으로 된 시스템을 돌리다 보면 통신 문제보다 ‘메모리 포인터(Pointer)’ 때문에 머리가 비게 된다.
C언어는 개발자에게 무소불위의 권한을 주지만, 그 대가로 단 하나의 문자 오타, 단 1바이트의 범람만으로 OS 커널이 즉각 프로세스를 킬(Kill)해버리는 지옥도를 선물한다.
세그멘테이션 폴트(Segfault/Core Dumped)가 터졌을 때 라우터를 탓하지 마라. 당신이 선언한 포인터가 가리키는 곳에, 이미 죽어버린 Zenoh 메모리 주소(Use-after-free)가 도사리고 있는 것이다.
1. 세그멘테이션 폴트(Segmentation Fault) 및 포인터 에러 디버깅
zenoh-c 라이브러리를 사용하다 발생하는 모든 Segfault의 원인은 결국 메모리 소유권(Ownership) 위반과 수명(Lifetime) 착각에서 기인한다.
1.0.1 콜백(Callback) 파라미터의 수명 주기 착각 (Dangling Pointer)
가장 흔하게 터지는 초보적인 버그다. C언어로 Subscriber를 선언하고 콜백 함수를 연결했다. 데이터가 도착하면 콜백에 const z_sample_t *sample이 주입된다.
개발자는 이 sample 안의 페이로드 바이트 배열(sample->payload.start) 주소값을 전역 변수에 슬쩍 캐싱(저장)해 두고 함수를 빠져나간다. 그리고 다른 스레드에서 그 전역 변수를 읽으력 시도한다.
결과는 100% 확률의 크래시 폭발이다!
Zenoh C API 내부 구조 상, 콜백으로 주입된 메모리 공간은 콜백 함수가 소멸(return)하는 즉시 백그라운드에서 메모리가 파괴(Deallocated)된다. 당신이 저장한 전역 변수는 완전히 허공을 가리키는 길 잃은 포인터(Dangling Pointer)가 되었다.
해법: 콜백 외부에서 데이터를 소모하려면, 콜백 안에서 무조건 새로운 영역을 malloc()으로 할당받거나 스택 배열을 파서, 원본 바이트를 1 대 1로 전부 통째로 복사(Deep Copy memcpy)하여 넘겨야만 애플리케이션의 장수를 보장할 수 있다.
1.0.2 Use-after-Free 현상 (해제된 세션 재사용)
애플리케이션이 종료되거나 에러 복구를 수행하면서 개발자가 정성스레 z_close(session)을 호출해 자원을 반환했다.
그런데 옆에 있던 독립된 로그 스레드가 이 사실을 모르고 해제된 session 변수 주소를 사용해 z_put을 시도한다면, C 런타임은 주저 없이 메인 메모리 위반(Segfault) 철퇴를 내리친다.
디버깅 방책 (GDB 활용):
이런 현상이 터지면, 크래시를 뚫어져라 쳐다보기 전에 컴파일 단계에 반드시 -g 옵션을 넣어 심볼(Symbol)을 보존하라. 그리고 GDB를 돌려라.
gcc -g -o my_robot robot_app.c -lzenohc
gdb ./my_robot
(gdb) run
(gdb) backtrace # 터진 라인 넘버를 정확히 타격
스레드 안전성(Safety) 장치가 없는 C에서는 공유 포인터 관리에 대한 락(Mutex)이나 참조 카운트 메커니즘을 여러분이 직접 코딩의 뼈대부터 정립해야 함을 명심하라.
2. 메모리 할당/해제 오류(Valgrind 활용) 및 ABI 호환성 문제
프로그램이 크래시는 나지 않지만, 로봇을 켜두고 다음 날 와보면 메모리가 99% 꽉 차 있어 시스템 전체가 느려진 채 죽기만을 기다리고 있다. 이것은 할당(Allocation)은 했으나 해제(Free/Drop)를 잊어버린 전형적인 메모리 릭(Leak)이다.
2.0.1 뼈를 깎는 소멸자 검증 - Valgrind 프로파일링
zenoh-c API를 쓰면서 선언한 모든 세션, 퍼블리셔 선언(z_declare_publisher), 키 익스프레션(z_keyexpr)은 힙(Heap) 메모리를 갉아먹는다. 이 자원은 z_undeclare 나 z_drop을 호출해주지 않으면 무한정 증식한다.
개발자가 작성한 C 애플리케이션을 리눅스 메모리 프로파일러의 거장인 valgrind 위에서 무자비하게 돌려라.
valgrind --leak-check=full --show-leak-kinds=all ./my_sensor_app
스크립트 구동 직후 나오는 리포트 중에 definitely lost: 10,204 bytes in 50 blocks라는 무지막지한 붉은 글씨가 보인다면, 그 블록을 생성한 함수 호출 스택 넘버 라인을 따라가 해당 변수에 쌍을 이루는 z_drop() 로직 누락 부위를 땜질하라. 당신의 코드 라인마다 할당과 소멸의 완벽한 1:1 대칭을 이루는 것이 유일한 구원의 길이다.
2.0.2 Shared Library 버전 불일치 및 ABI 호환성 마찰
libzenohc.so 동적 라이브러리 파일을 시스템 /usr/lib 계층에 둔 채로, 개발팀의 A 모듈은 0.6 버전 헤더(/usr/include/zenoh.h)를, B 모듈은 최근 변경된 0.7 버전의 바이너리 구현체를 물고 링크 컴파일을 진행했다.
이 C 코드 바이너리를 돌리는 도중, 특정 API 함수(z_put)가 불리는 순간 영문 모를 쓰레기(Garbage)값이 파라미터로 넘어가거나 코어 덤프가 터진다.
ABI(Application Binary Interface) 구조체 크기나 배열 오프셋 크기가 라이브러리 메이저 버전 업데이트로 달라졌음에도 강제로 결합되어 벌어지는 파국이다.
이런 재난을 피하기 위해 ldd 파편화 추적을 생활화하고, 컴파일링 호환성이 무너졌다면 시스템상의 C 메이저 헤더 파일을 모조리 클리어하고 완전히 클린 빌드(Clean Build)하는 정공법을 택해라.