안녕하세요! 지난 포스팅을 통해 안드로이드 백그라운드에서 살아 숨 쉬는 C/C++ 사용자 정의 데몬(Daemon)의 소스코드를 함께 구현해 보았습니다. 코드를 멋지게 짜두었으니 이제 이 코드를 안드로이드 시스템이 이해할 수 있는 바이너리 파일로 구워낼(빌드) 차례입니다.
안드로이드 NDK 진영에는 두 가지 빌드 시스템 파벌(?)이 존재합니다. 예전부터 전통적으로 사용해 온 전통의 Make 기반 Android.mk 방식과, 구글이 현재 표준으로 강력하게 밀고 있는 모던한 CMakeLists.txt 방식이죠. 레거시 프로젝트를 유지보수하거나 임베디드 장비 전용 빌드 스크립트를 짤 때는 전자룰, 최신 안드로이드 스튜디오 앱 프로젝트를 개발할 때는 후자를 다룰 줄 알아야 진정한 NDK 전문가라고 할 수 있습니다. 오늘 이 두 가지 빌드 설정법을 완벽히 마스터하고, 빌드된 바이너리를 타겟 기기에 올려 LLDB로 디버깅하는 실전 팁까지 싹 풀어드릴게요!

📌 핵심 요약 3줄
- 양대 빌드 시스템 정복: 레거시 스크립트 기반의 Android.mk 파일 구조와 모던 표준인 CMakeLists.txt 파일의 핵심 문법 및 Gradle 연동법을 비교 분석합니다.
- 실전 컴파일 가이드: NDK 툴체인 환경 변수를 활용하여 터미널(CLI) 환경에서 직접 네이티브 독립 실행 파일(Executable)을 컴파일하는 명령어를 학습합니다.
- 네이티브 디버깅 셋업: ndk-gdb 및 lldb-server를 이용해 안드로이드 내부에서 도는 C 코드를 한 줄씩 들여다보며 디버깅하는 환경을 구축합니다.
1. Android.mk를 이용한 레거시 빌드 설정
Android.mk는 안드로이드의 구형 NDK 빌드 시스템에서 Make 문법을 기반으로 네이티브 코드를 컴파일할 때 사용합니다. 소스코드 단에서 공유 라이브러리(.so)가 아닌 독립 실행형 데몬 바이너리(.bin)를 만들고 싶을 때 아주 유용하게 쓰입니다.
1.1 Android.mk & Application.mk 스크립트 작성
프로젝트 내 jni/ 디렉터리를 만들고 아래 설정을 추가해 보세요.
[Android.mk]
LOCAL_PATH := $(call my-dir)
# 이전 설정 변수들을 초기화하여 충돌을 방지합니다.
include $(CLEAR_VARS)
# 빌드 결과물인 실행 파일의 이름을 지정합니다.
LOCAL_MODULE := my_daemon
# 컴파일할 C/C++ 소스 파일 목록을 지정합니다.
LOCAL_SRC_FILES := daemon.c
# 안드로이드 내장 시스템 로그 라이브러리(-llog)를 링크합니다.
LOCAL_LDLIBS := -llog
# 공유 라이브러리가 아닌 '독립 실행형 바이너리'로 빌드하라는 핵심 명령어입니다!
include $(BUILD_EXECUTABLE)
[Application.mk (선택 사항)]
Application.mk 파일을 같은 경로에 만들어 두면, 빌드 대상 CPU 아키텍처(ABI)나 최소 타겟 SDK 버전을 글로벌하게 통제할 수 있습니다.
# 지원할 CPU 아키텍처 지정 (멀티 아키텍처 대응 가능)
APP_ABI := armeabi-v7a arm64-v8a
# 최소 호환 안드로이드 플랫폼 버전 설정
APP_PLATFORM := android-21
2. CMakeLists.txt 모던 빌드 설정 방법
구글 안드로이드 스튜디오가 공식 권장하는 방식입니다. 가독성이 좋고 cross-platform 빌드가 용이하다는 막강한 장점이 있습니다.
2.1 CMakeLists.txt 기본 구조와 구성 요소
안드로이드 스튜디오 프로젝트의 src/main/cpp/ 경로에 파일을 생성하고 아래 구조를 따릅니다. 기존 코드에서 누락될 수 있는 '실행 파일(Executable)' 빌드 포인트를 완벽히 다듬었습니다.
cmake_minimum_required(VERSION 3.10)
project(my_daemon)
# 표준 C 사용할 표준 명시
set(CMAKE_C_STANDARD 99)
# [주의] 일반 앱 연동 라이브러리라면 SHARED를 쓰지만,
# 독립형 데몬 바이너리를 만드려면 add_executable을 써야 합니다!
add_executable(my_daemon daemon.c)
# 안드로이드 NDK 내장 log 라이브러리를 찾아서 log-lib 변수에 할당합니다.
find_library(log-lib log)
# 빌드 타겟에 로그 라이브러리를 바인딩(링크)해 줍니다.
target_link_libraries(my_daemon ${log-lib})
2.2 Gradle과의 연동 (app/build.gradle)
이렇게 짠 CMake 스크립트를 안드로이드 앱 빌드 파이프라인에 태우려면 app/build.gradle 파일에 경로를 매핑해 주어야 합니다.
android {
...
externalNativeBuild {
cmake {
// 프로젝트 루트 기준 CMakeLists.txt의 상대 경로 지정
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
3. NDK를 활용한 네이티브 코드 빌드 및 디버깅
3.1 실전 네이티브 코드 예제 (daemon.c)
터미널 결과와 로그캣을 동시에 확인하기 위해 가독성을 높인 표준 데몬 뼈대 C 코드입니다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <android/log.h>
#define LOG_TAG "NativeDaemon"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
int main() {
LOGI("=====================================");
LOGI("사용자 정의 데몬 프로세스가 시작되었습니다.");
LOGI("=====================================");
while (1) {
sleep(10); // 10초 대기
LOGI("데몬이 백그라운드에서 정상 동작 중입니다...");
}
return 0;
}
3.2 빌드 실행 방식 한눈에 비교하기
두 빌드 시스템의 CLI 컴파일 명령어 체계는 다음과 같이 명확히 구분됩니다. 표로 직관적으로 정리해 드립니다!
📊 빌드 시스템별 터미널(CLI) 컴파일 실행 가이드
| 빌드 시스템 종류 | 터미널 실행 명령어 (CLI) | 주요 특징 |
| Android.mk 방식 | ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=Android.mk APP_ABI=arm64-v8a | jni/ 폴더 기반으로 움직이며, 별도의 디렉터리 생성 없이 바로 결과 바이너리가 추출됨 |
| CMakeLists.txt 방식 | mkdir -p build && cd build cmake .. -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-21 make |
NDK 내부의 android.toolchain.cmake 파일을 툴체인 인자로 명시해 주어야 안드로이드 크로스 컴파일이 정상 작동함 |
🐛 네이티브 코드 실전 디버깅 전략
C/C++ 코드는 자바와 달라서 포인터 잘못 쓰면 세그멘테이션 폴트(Segmentation Fault)를 내며 소리소문없이 터집니다. 제대로 고치려면 디버거 셋업이 필수입니다.
1) ndk-gdb 코맨드 활용 (전통 방식)
전통적인 GDB 디버거 환경을 선호하신다면 프로젝트 루트에서 다음 명령어로 데몬을 실행하며 디버깅 세션을 열 수 있습니다.
ndk-gdb --launch
2) modern LLDB 원격 디버깅 (강력 추천)
최신 안드로이드 환경에서는 LLDB가 대세입니다. 스마트폰 타겟 기기 내부에 lldb 서버를 띄우고 호스트 PC와 소켓 통신으로 엮어 디버깅하는 아키텍처를 가집니다.
- 타겟 단말기에 lldb-server 푸시 및 실행 권한 부여:
-
Bash
adb push $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/lib64/clang/current/bin/arm/lldb-server /data/local/tmp/ adb shell chmod 755 /data/local/tmp/lldb-server - 단말기 내부에서 서버 대기 모드 구동:
-
Bash
adb shell /data/local/tmp/lldb-server platform --listen "*:5039" --server - 개발 PC(호스트)에서 단말기 lldb 서버로 원격 접속:
Bash
(lldb) platform select remote-android (lldb) platform connect connect://localhost:5039 - 호스트 PC 터미널에서 lldb를 실행한 후 원격 타겟 주소를 연결해 주면 소스코드 브레이크 포인트(Break Point) 처리가 가능해집니다.
🛠️ 개발을 위한 꿀팁 (Tips)
- BUILD_SHARED_LIBRARY와 BUILD_EXECUTABLE 구별하기: Android.mk 파일을 작성할 때 일반적인 JNI 사용 목적인 .so 파일을 만들고 싶다면 include $(BUILD_SHARED_LIBRARY)를 써야 하지만, 시스템 독립형 데몬을 단독 실행 파일로 만드려면 본문의 예제처럼 반드시 include $(BUILD_EXECUTABLE) 명령어를 선언해야 합니다.
- 컴파일러 최적화 플래그 제어: 디버깅 단계에서는 소스코드와 기계어 라인이 1:1 매칭되어야 변수 값을 정확히 추적할 수 있습니다. 빌드 시 컴파일 옵션에 -O0 -g (최적화 해제, 디버그 심볼 포함) 플래그를 명시해 주는 센스를 발휘하세요! 상용 빌드 시에는 다시 -O2나 -Os로 바꾸어 용량과 성능을 최적화해야 합니다.
- sysroot 지정 에러 해결: CLI에서 CMake로 빌드할 때 헤더 파일을 찾지 못하는 링킹 에러가 난다면 NDK 경로 내부의 sysroot 인자가 꼬인 것입니다. 이럴 땐 삽질하지 말고 구글 표준 크로스 빌드 툴체인 스크립트 파라미터(-DCMAKE_TOOLCHAIN_FILE) 경로를 재차 체크하는 것이 가장 빠른 지름길입니다.
⚠️ 흔히 하는 실수 (Common Mistakes)
- 타겟 아키텍처(ABI) 미스매치: 빌드 스크립트에는 APP_ABI := x86으로 잡아두고 실물 ARM64 기반 스마트폰 기기에 데몬을 강제로 밀어 넣으면 실행 시 Exec format error를 내뿜으며 아예 켜지지도 않습니다. 타겟 기기의 아키텍처(adb shell getprop ro.product.cpu.abi)를 꼭 미리 조회하세요.
- find_library 검색 실패 및 오탈자: CMakeLists.txt에서 find_library(log-lib log) 구문을 적을 때 인자 순서를 바꾸거나 오탈자를 내면 로그 함수를 호출하는 소스코드 라인마다 Undefined reference to __android_log_print 에러가 터지며 빌드가 중단됩니다.
- lldb-server 구동 시 방화벽 및 포트 포워딩 누락: USB 케이블로 단말기를 PC에 연결한 상태에서 LLDB 디버깅을 시도할 때 포트가 안 열려 커넥션 타임아웃이 나는 경우가 많습니다. 디버거 연결 전 터미널에 반드시 adb forward tcp:5039 tcp:5039 명령어를 실행하여 PC와 스마트폰 간의 네트워크 통로를 열어주어야 합니다.
4. 결론
이번 포스팅에서는 작성한 C/C++ 네이티브 데몬 코드를 세상 밖으로 나오게 해주는 두 가지 핵심 빌드 시스템(Android.mk & CMakeLists.txt)의 구체적인 명세 방법과 CLI 빌드 파이프라인을 깊이 있게 공부했습니다.
나아가 프로그램의 버그를 잡고 내부 메모리 상태를 실시간으로 스캔할 수 있는 LLDB 원격 디버깅 플랫폼 구축 방법까지 다루었으므로, 이제 여러분은 안드로이드 네이티브 단에서 발생하는 빌드 트러블슈팅과 런타임 예외 처리를 무서워하지 않고 당당히 맞설 수 있는 강력한 무기를 얻으신 셈입니다.
오늘 가이드해 드린 표와 팁들을 토대로 여러분의 프로젝트 환경에 맞는 최적의 빌드 자동화 스크립트를 짜보시길 바랍니다. 빌드 도중 이해할 수 없는 난해한 Clang 컴파일러 에러 덤프를 마주치셨다면 주저 없이 댓글로 질문을 남겨주세요. 함께 원인을 파헤쳐 봅시다! 즐거운 네이티브 빌드 하세요!
'Android System & AOSP Engineering > Native Layer & Daemons' 카테고리의 다른 글
| Android NDK 실전: JNI를 이용한 C/C++ 네이티브 데몬 프로세스 연동 및 상호 통신 가이드 (0) | 2025.06.19 |
|---|---|
| Android init.rc 데몬 등록 가이드: systemd 없는 안드로이드에서 SELinux와 Property로 서비스 제어하기 (0) | 2025.06.18 |
| Android NDK로 C/C++ 네이티브 데몬 구현하기: 더블 포크(Double Fork)와 생명주기 관리 완벽 가이드 (0) | 2025.06.16 |
| Android 데몬(Daemon) 구현 가이드: 서비스, NDK C/C++, 리눅스와의 차이점 완벽 비교 (0) | 2025.06.15 |
| Android NDK 환경 설정부터 C/C++ 네이티브 데몬(Daemon) 예제까지 완벽 가이드 (0) | 2025.06.14 |