[내용요약]
- 문제: 안드로이드 시스템 장기 구동 시 Native Daemon 및 HAL 프로세스의 가상 메모리 크기(VSS)와 상주 메모리 크기(RSS)가 지속적으로 증가하다가 결국 커널 OOM(Out of Memory) Killer에 의해 프로세스가 강제 종료(SIGKILL)되는 현상 발생.
- 원인: C/C++ 기반 네이티브 레이어에서 dynamic allocation 후 free() 또는 delete 연산자가 누락되거나, 스마트 포인터의 순환 참조(Circular Reference)로 인해 힙 heap 메모리 해제가 누락되는 현상.
- 해결책: AOSP 환경에서 Malloc Debug를 활성화하여 동적 할당 스택을 추적하고, libmemunreachable을 통해 런타임에 메모리 가비지 컬렉션(GC) 스타일의 누수 검사를 수행하며, 빌드 타임에 ASan(AddressSanitizer)을 적용하여 비정상 메모리 접근 및 누수를 원천 차단.
안드로이드 Native Daemon 및 HAL 레이어의 메모리 누수(Memory Leak) 발생 증상
안드로이드 임베디드 기기나 시스템을 수일 이상 롱런(Long-run) 테스트할 때, 특정 Native Daemon이나 HAL(Hardware Abstraction Layer) 프로세스가 예기치 않게 재시작되는 현상이 발생합니다. adb logcat을 모니터링하면 LowMemoryKiller 혹은 Out of memory: Kill process 메시지와 함께 프로세스가 SIGKILL(Signal 9)을 받고 강제 종료되는 패턴을 보입니다.
글로벌 개발자들이 Google 검색이나 Stack Overflow에 주로 제기하는 키워드는 AOSP native memory leak, HAL process virtual memory continuous increase, libc malloc leak trace, asan heap-use-after-free android 등입니다. 프로세스가 종료되기 직전 procrank나 dumpsys meminfo를 확인해 보면, 특정 데몬의 RSS(Resident Set Size)와 VSS(Virtual Set Size)가 해제 없이 증가하는 메모리 Leakage 증상이 보입니다.
네이티브 동적 할당 아키텍처와 힙 오염(Heap Contamination)의 근본 원인분석
안드로이드 프레임워크 하단의 네이티브 레이어는 Bionic libc 라이브러리의 힙 메모리 관리자(기본적으로 jemalloc 또는 scudo)를 사용합니다. 애플리케이션 레이어와 달리 Java/Kotlin의 Garbage Collector(GC)가 개입하지 않으므로, C/C++ 소스 코드 레벨에서 명시적으로 제어해야 합니다.
메모리 누수가 발생하는 하드웨어/소프트웨어적 메커니즘은 다음과 같습니다:
- Unfreed Heap Blocks: malloc(), calloc(), realloc() 또는 C++의 new 연산자로 할당된 힙 영역의 가상 주소 포인터가 함수 스코프를 벗어나면서 유실(Lost)되어, 할당된 페이지(Page)가 커널에 반환되지 못합니다.
- Circular References in Smart Pointers: std::shared_ptr을 남용하여 오브젝트 간 참조 그래프가 순환 구조를 형성하는 경우, 참조 카운트(Reference Count)가 절대 0에 도달하지 않아 데스트럭터(Destructor)가 호출되지 않습니다.
- Hardware Driver Callback Accumulation: HAL 레이어에서 하드웨어 인터럽트나 커널 이벤트에 대응하기 위해 할당한 컨텍스트 버퍼 구조체가 콜백 함수가 종료된 이후 정상적으로 해제(Free)되지 않고 누적되면서 물리 RAM 영역에 남아있습니다.
메모리 누수를 유발하는 잘못된 네이티브 C++ HAL 코드 예시 (Bad Case)
아래 코드는 HAL 레이어에서 주기적으로 센서 데이터를 패킷화하여 처리하는 과정에서, 에러 브랜치 진입 시 할당된 메모리를 해제하지 않고 누수를 일으키는 패턴입니다.
#include <iostream>
#include <cstdlib>
#include <unistd.h>
// Simulated hardware data structure
struct SensorDataPacket {
uint32_t timestamp;
double values[3];
char* raw_payload;
};
void process_sensor_event(int status_code) {
// Allocation of the main packet structure
SensorDataPacket* packet = new SensorDataPacket();
// Dynamic allocation for internal buffer
packet->raw_payload = (char*)malloc(1024);
packet->timestamp = 1680000000;
if (status_code < 0) {
// BAD CASE: Premature return without freeing allocated resources.
// Both 'packet' pointer and 'packet->raw_payload' are leaked into the heap.
std::cerr << "[HAL Error] Invalid status code. Aborting process." << std::endl;
return;
}
// Normal processing path
std::cout << "[HAL] Processing packet at: " << packet->timestamp << std::endl;
// Incomplete clean-up: missing free(packet->raw_payload)
delete packet;
}
int main() {
while (true) {
// Simulating continuous hardware event loops with error triggers
process_sensor_event(-1);
usleep(100000); // 100ms interval
}
return 0;
}
자원 자동 해제(RAII) 아키텍처를 적용한 안전한 네이티브 C++ 코드 (Good Case)
메모리 누수를 방지하기 위해 스마트 포인터(std::unique_ptr)와 RAII(Resource Acquisition Is Initialization) 패턴을 적용하여, 예기치 못한 리턴 상황에서도 스코프를 벗어나면 컨텍스트가 100% 자동 해제되도록 수정한 코드입니다.
#include <iostream>
#include <cstdlib>
#include <memory>
#include <vector>
#include <unistd.h>
// Secure hardware data structure utilizing RAII elements
struct SecureSensorDataPacket {
uint32_t timestamp;
double values[3];
std::vector<char> raw_payload; // Replaced raw char* with std::vector for automatic management
};
void process_sensor_event_secure(int status_code) {
// GOOD CASE: Using std::unique_ptr to guarantee resource deallocation
auto packet = std::make_unique<SecureSensorDataPacket>();
// Safe sizing without manual malloc()
packet->raw_payload.resize(1024);
packet->timestamp = 1680000000;
if (status_code < 0) {
// Even if a premature return happens here, std::unique_ptr
// automatically destroys the object and frees the memory.
std::cerr << "[HAL Secure] Invalid status code. Safe stack unwinding." << std::endl;
return;
}
std::cout << "[HAL Secure] Processing packet at: " << packet->timestamp << std::endl;
// No explicit delete or free required. Resource management is handled by RAII scope.
}
int main() {
while (true) {
process_sensor_event_secure(-1);
usleep(100000); // 100ms interval
}
return 0;
}
핵심 수정 포인트 (Key Implementation Details)
- std::unique_ptr 도입: 동적 오브젝트 소유권을 스택 기반 스마트 포인터에 바인딩하여, 함수 내부의 조기 리턴(return) 구문이 실행되더라도 컴파일러 수준에서 안전하게 디스트럭터를 호출합니다.
- std::vector 대체: 원시 포인터(char* raw_payload) 대신 내부 컨테이너 버퍼를 사용하여, 구조체 파괴 시 2차 내부 동적 할당 영역까지 수동 free() 없이 연쇄적으로 메모리가 반환되도록 구조를 격리했습니다.
엔지니어를 위한 안드로이드 네이티브 메모리 디버깅 및 트러블슈팅 가이드 (Debugging Tips)
안드로이드 소스 트리 전체를 다시 빌드하지 않고도 필드나 개발 환경에서 네이티브 누수를 신속하게 격리할 수 있는 3가지 핵심 실무 기법입니다.
1) 런타임 Malloc Debug 활성화 및 스택 백트레이스 추출
AOSP 가 런타임에 제공하는 Malloc Debug 기능을 켜면, 특정 네이티브 데몬 프로세스가 메모리를 할당할 때마다 호출 스택(Call Stack)을 기록할 수 있습니다.
# Target 프로세스에 대해 malloc debug 및 leak 검출 프로퍼티 주입
adb shell setprop wrap.libhal_service_name '"LIBC_DEBUG_MALLOC_OPTIONS=backtrace leak_track"'
adb shell stop
adb shell start
프로세스가 동작하는 와중에 kill -45 신호를 인젝션하거나 로그캣을 조회하면, 해제되지 않은 메모리 블록이 어떤 네이티브 함수 스택에서 생성되었는지 기호화(Symbolized)된 로그 파일 형태로 확인할 수 있습니다.
2) CLI 기반 libmemunreachable 실시간 스캔 실행
libmemunreachable은 가벼운 가비지 컬렉터와 유사한 알고리즘을 사용하여, 프로세스의 전체 메모리 공간을 스캔한 뒤 도달 가능한 포인터 주소가 없는 힙 블록(Dangling / Unreachable memory)을 실시간으로 추적합니다.
# 디버깅 대상 네이티브 데몬의 누수 리포트 실시간 조회
adb shell dumpsys meminfo --unreachable <PID>
이 명령은 실행 중인 데몬의 중단 시간을 최소화하면서, 누수된 바이트 수와 누수를 유발한 오브젝트의 메모리 할당 시점 백트레이스를 즉시 터미널에 표시해 줍니다.
3) 빌드 타임 ASan(AddressSanitizer) 하드웨어 어시스트 검증
가장 강력한 검증 수단으로, Android.bp 파일 내 대상 컴파일 모듈 속성에 AddressSanitizer 플래그를 심어 빌드하는 방법입니다. 힙 오버플로우와 동시에 Memory Leak Detection 기능을 완전 활성화합니다.
// Android.bp 컴파일 설정 예시
cc_binary {
name: "android.hardware.foo@1.0-service",
defaults: ["hidl_defaults"],
srcs: ["main.cpp", "Foo.cpp"],
sanitize: {
address: true,
},
}
ASan 기법이 적용된 바이너리는 타깃 보드에서 구동 중 프로세스가 종료되거나 특정 시점에 도달했을 때, 해제되지 않은 블록 리스트와 오염된 메모리 맵 주소를 상세히 덤프하므로 컴파일 타임에 버그를 완전히 소거할 수 있습니다.
'Advanced Debugging & Observability' 카테고리의 다른 글
| 안드로이드 오디오 버퍼 언더런 및 UI Jank 분석: Perfetto 기반 컨텍스트 스위칭 지연 추적 가이드 (0) | 2026.07.05 |
|---|---|
| AOSP 시스템 프로파일링 최적화: Systrace에서 Perfetto 전환 및 SQL 쿼리 분석 가이드 (0) | 2026.07.04 |
| eBPF 기반 리눅스 커널 메모리 누수(Memory Leak) 및 좀비 프로세스(Zombie Process) 추적 가이드 (1) | 2026.07.03 |
| eBPF 기반 파일 I/O 및 네트워크 지연(Latency) 추적기 구현 가이드 (bcc / bpftrace 실습) (0) | 2026.07.02 |
| Linux eBPF 커널 트레이싱 가이드: ftrace 및 perf 대비 오버헤드와 패러다임 비교 (0) | 2026.07.01 |