Android System & AOSP Engineering/Native Layer & Daemons

Android NDK 빌드 가이드: Android.mk vs CMakeLists.txt 설정부터 네이티브 디버깅까지

임베디드 친구 2025. 6. 17. 21:51
반응형

안녕하세요! 지난 포스팅을 통해 안드로이드 백그라운드에서 살아 숨 쉬는 C/C++ 사용자 정의 데몬(Daemon)의 소스코드를 함께 구현해 보았습니다. 코드를 멋지게 짜두었으니 이제 이 코드를 안드로이드 시스템이 이해할 수 있는 바이너리 파일로 구워낼(빌드) 차례입니다.

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

Generated by Gemini AI.


📌 핵심 요약 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]

Makefile
 
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 버전을 글로벌하게 통제할 수 있습니다.

Makefile
 
# 지원할 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
 
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 파일에 경로를 매핑해 주어야 합니다.

Groovy
 
android {
    ...
    externalNativeBuild {
        cmake {
            // 프로젝트 루트 기준 CMakeLists.txt의 상대 경로 지정
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
}

3. NDK를 활용한 네이티브 코드 빌드 및 디버깅

3.1 실전 네이티브 코드 예제 (daemon.c)

터미널 결과와 로그캣을 동시에 확인하기 위해 가독성을 높인 표준 데몬 뼈대 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 디버거 환경을 선호하신다면 프로젝트 루트에서 다음 명령어로 데몬을 실행하며 디버깅 세션을 열 수 있습니다.

Bash
 
ndk-gdb --launch

2) modern LLDB 원격 디버깅 (강력 추천)

최신 안드로이드 환경에서는 LLDB가 대세입니다. 스마트폰 타겟 기기 내부에 lldb 서버를 띄우고 호스트 PC와 소켓 통신으로 엮어 디버깅하는 아키텍처를 가집니다.

  1. 타겟 단말기에 lldb-server 푸시 및 실행 권한 부여:
  2. 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
    
  3. 단말기 내부에서 서버 대기 모드 구동:
  4. Bash
     
    adb shell /data/local/tmp/lldb-server platform --listen "*:5039" --server
    
  5. 개발 PC(호스트)에서 단말기 lldb 서버로 원격 접속:
    Bash
     
    (lldb) platform select remote-android
    (lldb) platform connect connect://localhost:5039
    
  6. 호스트 PC 터미널에서 lldb를 실행한 후 원격 타겟 주소를 연결해 주면 소스코드 브레이크 포인트(Break Point) 처리가 가능해집니다.

🛠️ 개발을 위한 꿀팁 (Tips)

  1. BUILD_SHARED_LIBRARY와 BUILD_EXECUTABLE 구별하기: Android.mk 파일을 작성할 때 일반적인 JNI 사용 목적인 .so 파일을 만들고 싶다면 include $(BUILD_SHARED_LIBRARY)를 써야 하지만, 시스템 독립형 데몬을 단독 실행 파일로 만드려면 본문의 예제처럼 반드시 include $(BUILD_EXECUTABLE) 명령어를 선언해야 합니다.
  2. 컴파일러 최적화 플래그 제어: 디버깅 단계에서는 소스코드와 기계어 라인이 1:1 매칭되어야 변수 값을 정확히 추적할 수 있습니다. 빌드 시 컴파일 옵션에 -O0 -g (최적화 해제, 디버그 심볼 포함) 플래그를 명시해 주는 센스를 발휘하세요! 상용 빌드 시에는 다시 -O2나 -Os로 바꾸어 용량과 성능을 최적화해야 합니다.
  3. sysroot 지정 에러 해결: CLI에서 CMake로 빌드할 때 헤더 파일을 찾지 못하는 링킹 에러가 난다면 NDK 경로 내부의 sysroot 인자가 꼬인 것입니다. 이럴 땐 삽질하지 말고 구글 표준 크로스 빌드 툴체인 스크립트 파라미터(-DCMAKE_TOOLCHAIN_FILE) 경로를 재차 체크하는 것이 가장 빠른 지름길입니다.

⚠️ 흔히 하는 실수 (Common Mistakes)

  1. 타겟 아키텍처(ABI) 미스매치: 빌드 스크립트에는 APP_ABI := x86으로 잡아두고 실물 ARM64 기반 스마트폰 기기에 데몬을 강제로 밀어 넣으면 실행 시 Exec format error를 내뿜으며 아예 켜지지도 않습니다. 타겟 기기의 아키텍처(adb shell getprop ro.product.cpu.abi)를 꼭 미리 조회하세요.
  2. find_library 검색 실패 및 오탈자: CMakeLists.txt에서 find_library(log-lib log) 구문을 적을 때 인자 순서를 바꾸거나 오탈자를 내면 로그 함수를 호출하는 소스코드 라인마다 Undefined reference to __android_log_print 에러가 터지며 빌드가 중단됩니다.
  3. lldb-server 구동 시 방화벽 및 포트 포워딩 누락: USB 케이블로 단말기를 PC에 연결한 상태에서 LLDB 디버깅을 시도할 때 포트가 안 열려 커넥션 타임아웃이 나는 경우가 많습니다. 디버거 연결 전 터미널에 반드시 adb forward tcp:5039 tcp:5039 명령어를 실행하여 PC와 스마트폰 간의 네트워크 통로를 열어주어야 합니다.

4. 결론

이번 포스팅에서는 작성한 C/C++ 네이티브 데몬 코드를 세상 밖으로 나오게 해주는 두 가지 핵심 빌드 시스템(Android.mk & CMakeLists.txt)의 구체적인 명세 방법과 CLI 빌드 파이프라인을 깊이 있게 공부했습니다.

나아가 프로그램의 버그를 잡고 내부 메모리 상태를 실시간으로 스캔할 수 있는 LLDB 원격 디버깅 플랫폼 구축 방법까지 다루었으므로, 이제 여러분은 안드로이드 네이티브 단에서 발생하는 빌드 트러블슈팅과 런타임 예외 처리를 무서워하지 않고 당당히 맞설 수 있는 강력한 무기를 얻으신 셈입니다.

오늘 가이드해 드린 표와 팁들을 토대로 여러분의 프로젝트 환경에 맞는 최적의 빌드 자동화 스크립트를 짜보시길 바랍니다. 빌드 도중 이해할 수 없는 난해한 Clang 컴파일러 에러 덤프를 마주치셨다면 주저 없이 댓글로 질문을 남겨주세요. 함께 원인을 파헤쳐 봅시다! 즐거운 네이티브 빌드 하세요!

반응형