스마트 디바이스 개발에서 다양한 Android 생태계를 대상으로 Rust로 네이티브 서비스를 구축하는 방법을 다룹니다. 자동차 환경에서의 크로스 컴파일 실무, LTO 심볼 충돌 이슈, Android에서의 backtrace 문제와 해결, 바이너리 사이즈 최적화 및 스택 트레이스 복구 기법까지 실전 팁을 공유합니다.
스마트 디바이스 개발 세계에서 다양한 Android 생태계는 개발자에게 독특한 도전 과제를 제시합니다. 이 글은 특히 자동차 환경에서 마주치는 크로스 컴파일 기법에 초점을 맞춰, 네이티브 Android 서비스 개발을 위한 Rust 활용법을 다룹니다. 실제 현장에서의 경험과 해결책을 바탕으로 실무 노하우를 공유합니다.
임베디드 시스템—특히 자동차 및 산업 분야—에서는 GreptimeDB Edge가 Android에서 시스템 서비스로 배포되는 경우가 많습니다. 이를 위해서는 GreptimeDB Edge를 Android 플랫폼에 적합한 실행 파일로 컴파일해야 합니다.
이론적으로는 Android 개발 보드에 Rust 툴체인을 설치해 직접 컴파일할 수 있지만, 다음과 같은 제약이 있습니다:
크로스 컴파일은 보다 효율적이고 확장 가능한 해법을 제공합니다. 즉, 한 플랫폼(x86_64 Linux/macOS)에서 다른 플랫폼(ARM Android)을 타깃으로 코드를 컴파일할 수 있게 해주므로, 타깃 디바이스에서 직접 컴파일하기 어려운 상황에서 특히 유용합니다.
Rust는 뛰어난 크로스 컴파일 지원을 갖추고 있으며, Android NDK는 필요한 툴체인과 라이브러리를 제공하여 크로스 컴파일을 한층 단순화합니다.
Rust가 코드를 어떻게 컴파일하는지 간단히 살펴보면, rustc는 먼저 Rust 코드를 LLVM-IR로 컴파일하고, 이후 LLVM이 해당 LLVM-IR을 대상 플랫폼의 바이너리로 컴파일합니다. 마지막으로 링커가 이를 연결해 최종 실행 파일을 생성합니다.
Rustc는 Rust용 컴파일러이며, 백엔드는 LLVM입니다(혹은 rustc가 LLVM의 프런트엔드라고도 할 수 있습니다).
다음은 Rust 컴파일러 아키텍처의 단순화된 다이어그램입니다.

(그림 1: Rust의 컴파일 아키텍처)
x86 Linux 머신에서 오픈소스 GreptimeDB를 aarch64-linux-android 아키텍처용 실행 파일로 크로스 컴파일하는 과정을 살펴보겠습니다.
NDK 다운로드 링크는 여기에서 확인하세요. 이후 과정을 편하게 하기 위해 환경 변수를 설정합니다. 아래와 같습니다:
bash
export ANDROID_NDK_HOME=<YOUR_NDK_ROOT>
# example
# export ANDROID_NDK_HOME=/home/fys/soft/ndk/android-ndk-r25c
bash
git clone https://github.com/GreptimeTeam/greptimedb.git --depth 1
Rust 툴체인에 타깃을 추가하는 것은 크로스 플랫폼 컴파일을 가능하게 하는 핵심 단계입니다. 이를 통해 rustc가 중간 표현(LLVM-IR) 코드를 대상 플랫폼의 기계어로 컴파일할 수 있게 됩니다. 이 예시에서 대상 플랫폼 아키텍처는 aarch64-linux-android입니다. GreptimeDB 프로젝트 루트 디렉터리에서 다음 명령을 실행합니다:
bash
rustup target add aarch64-linux-android
지원되는 전체 타깃 목록은 여기에서 확인하세요.
이 시점에서 컴파일을 시도하면 -lgcc not found 오류가 발생할 수 있습니다. 이는 Android NDK의 libgcc.a가 libunwind.a로 대체되었기 때문입니다. 해결책은 libunwind.a를 복사하여 이름을 libgcc.a로 바꾸는 것입니다. 자세한 내용은 Rust 공식 블로그를 참고하세요.
bash
cd $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/aarch64/
cp libunwind.a libgcc.a
Rust 프로젝트가 build.rs를 통해 C 또는 C++ 코드와 연동될 때는 상황이 까다로워질 수 있습니다. 보통 cc나 cmake 같은 빌드 도구에 컴파일러 경로(CC, CXX), 라이브러리 및 헤더 파일 경로 등 필요한 정보를 제공해야 하기 때문입니다. 다행히 cargo-ndk가 이러한 복잡성을 상당 부분 감춰줍니다:
bash
cargo ndk --platform 30 -t aarch64-linux-android build --bin greptime --release
타깃 아키텍처와 호환되지 않는 크레이트가 있다면 다음과 같이 대응할 수 있습니다:
만약 protobuf 같은 라이브러리가 없다는 에러가 나타나면, 타깃 환경에 맞춰 올바르게 설치되어 있는지 확인하세요.
우리는 LTO(Link Time Optimization)을 활성화한 빌드에서 한 차례 실패를 겪었습니다. 에러 메시지는 다음과 같습니다:
plaintext
ld.lld: error: duplicate symbol: pthread_atfork
이는 Android의 네이티브 라이브러리와 tikv-jemallocator가 모두 pthread_atfork를 strong 심볼로 정의하기 때문입니다. LTO는 이를 병합하면서 충돌을 유발합니다.
해결: tikv-jemallocator에서 pthread_atfork를 weak 심볼로 사용하세요. 최신 버전에서는 이미 해결되었습니다 — 자세한 내용은 여기를 참고하세요.
GreptimeDB Edge 작업 중, Android에서 Rust의 backtrace 지원이 신뢰할 수 없다는 점을 발견했습니다. 특히 panic 스택 트레이스가 unknown으로 표시되어 디버깅이 어려웠습니다.
문제를 재현하기 위해 최소 예제를 작성했습니다:
main 함수에서 패닉을 발생시켜 예외를 시뮬레이션합니다:shell
fn main() {
panic!("Panic here.");
}
rust-toolchain을 stable 1.81 또는 그 이전 버전으로 설정합니다:shell
[toolchain]
channel = "1.81"
bash
export ANDROID_NDK_HOME=<YOUR_NDK_ROOT>
rustup target add aarch64-linux-android
cargo ndk --platform 28 -t aarch64-linux-android build --release
bash
RUST_BACKTRACE=full ./<path-to-binary>
backtrace 정보가 누락된 결과를 확인합니다. 문제 재현에 성공했습니다:bash
thread 'main' panicked at src/main.rs:2:5:
Panic here
stack backtrace:
0: 0x5d908f7a7535 - <unknown>
1: 0x5d908f7b336b - <unknown>
...
원인을 알고 싶지 않다면, 먼저 빠른 해결책부터 공유합니다.
Rust 툴체인 업그레이드:
rust-toolchain을 Rust 1.82 이상으로 업그레이드하세요. 이 버전에서 문제가 이미 수정되었습니다(다음 섹션에서 자세히 설명합니다).
커스텀 Panic Hook:
업그레이드가 어렵다면, backtrace-rs 크레이트를 이용해 커스텀 panic hook을 등록할 수 있습니다.
Rust의 기본
panic hook은 stderr로 메시지를 출력하는데, Android와 같은 환경에서는 파일이나logcat으로 로깅하는 방식이 더 적합합니다. 이런 경우 panic hook을 커스터마이징하는 것이 유용합니다.
간단한 구현 예시는 다음과 같습니다:
rust
pub fn set_panic_hook() {
#[cfg(windows)]
const LINE_ENDING: &str = "\r\n";
#[cfg(not(windows))]
const LINE_ENDING: &str = "\n";
std::panic::set_hook(Box::new(move |panic| {
let backtrace = backtrace::Backtrace::new();
let Some(l) = panic.location() else {
log::error!(
"Panic: {:?}, backtrace: {}{:#?}",
panic, LINE_ENDING, backtrace
);
return;
};
log::error!(
"Panic: {:?}, file: {}, line: {}, col: {}, backtrace: {}{:#?}",
panic,
l.file(),
l.line(),
l.column(),
LINE_ENDING,
backtrace,
);
}));
}
출력되는 스택 정보는 다음과 같습니다(컴파일 옵션에서 debug info를 제거하고 심볼 테이블은 보존한 상태):
rust
pub fn set_panic_hook() {
#[cfg(windows)]
const LINE_ENDING: &str = "\r\n";
#[cfg(not(windows))]
const LINE_ENDING: &str = "\n";
std::panic::set_hook(Box::new(move |panic| {
let backtrace = backtrace::Backtrace::new();
let Some(l) = panic.location() else {
log::error!(
"Panic: {:?}, backtrace: {}{:#?}",
panic, LINE_ENDING, backtrace
);
return;
};
log::error!(
"Panic: {:?}, file: {}, line: {}, col: {}, backtrace: {}{:#?}",
panic,
l.file(),
l.line(),
l.column(),
LINE_ENDING,
backtrace,
);
}));
}
⚠️ 주의:
backtrace정보의 가용성과 상세도는 컴파일 설정에도 좌우됩니다. 심볼 테이블과debug info를 모두 스트립하면<unknown>으로만 표시되는 스택 트레이스를 얻게 됩니다.debug info를 유지하면 더 상세한 트레이스를 얻을 수 있지만 바이너리 크기가 증가합니다.
이제 Rust 1.81을 기준으로 근본 원인을 파고들어 보겠습니다.
배경 지식:
dl_iterate_phdr 기능을 활성화합니다. 그러나 Rust는 backtrace-rs를 서브모듈로 통합하면서 그 build.rs를 실행하지 않기 때문에 dl_iterate_phdr 기능이 활성화되지 않습니다. 그 결과 std::backtrace가 Android에서 제대로 동작하지 않습니다.수수께끼 해결!
표준 라이브러리에 포함된 backtrace-rs에서 dl_iterate_phdr 기능을 활성화해야 합니다. 다행히 #120593부터 최소 Android API 레벨이 19에서 21로 상향되었습니다. Android 21은 dl_iterate_phdr을 지원하므로, 이제 backtrace-rs에서 이 기능을 기본으로 활성화할 수 있게 되었습니다. Rust 1.82가 바로 이 접근을 채택해 문제를 해결했습니다.
🔗 관련 PR:
Android 플랫폼에서 기반 서비스 개발 시 바이너리 크기는 매우 중요합니다. 바이너리가 커지면 OTA 업데이트가 느려지고, 핫 업데이트 효율도 떨어지며, 저장 공간을 더 많이 차지하게 됩니다. 그렇다면 Rust 프로그램의 크기를 효과적으로 줄이려면 어떻게 해야 할까요?
다행히 GitHub의 Minimizing Rust Binary Size 프로젝트가 이 목적을 위한 거의 모든 효과적인 기법을 포괄적으로 정리해두었습니다. 관심 있는 독자는 해당 프로젝트에서 자세한 정보를 살펴보시길 권합니다 — 여기서는 반복 설명을 생략합니다.
이 중 특히 주목할 기법이 하나 있습니다. 바로 바이너리에서 심볼 테이블과 디버그 정보를 스트립하는 것입니다. 이는 바이너리 크기를 크게 줄여주지만, 대가가 따릅니다. 예를 들어 프로그램이 패닉을 일으키면 스택 트레이스가 읽기 어려워져 디버깅이 상당히 까다로워집니다. 그렇다면 바이너리 크기를 줄이면서도 패닉 시 읽을 수 있는 스택 트레이스를 유지하려면 어떻게 균형을 맞출 수 있을까요?
차량 내 시스템에서 구동되는 GreptimeDB Edge는 다음과 같은 해법을 사용합니다. 실행 파일을 두 가지 버전으로 빌드합니다:
차량 시스템에서 오류(예: 패닉 또는 크래시)가 발생하면, 클라우드에서 해당 버전의 바이너리를 가져와 그 심볼과 디버그 정보를 활용해 사람이 읽을 수 있는 스택 트레이스를 복구할 수 있습니다. 이는 문제 진단과 해결에 큰 도움을 줍니다.

(그림 2: GreptimeDB Edge를 예로 든 바이너리 크기 최적화)
그렇다면 스택 트레이스 복구는 정확히 어떻게 동작할까요? 간단한 데모 프로그램으로 설명해보겠습니다.
아래 프로그램은 런타임에 패닉이 발생하도록 설계되어 있습니다. 로드된 오브젝트의 메모리 주소를 출력하고 스택 트레이스를 캡처하는 커스텀 panic hook을 설정합니다.
main.rs
rust
use std::panic;
use backtrace::Backtrace;
use tracing::error;
fn main() {
tracing_subscriber::fmt::init();
set_panic_hook();
a();
}
#[inline(never)]
fn a() {
b();
}
#[inline(never)]
fn b() {
// panic here!!!
panic!("Oh no!");
}
pub fn set_panic_hook() {
log_base_addr();
#[cfg(windows)]
const LINE_ENDING: &str = "\r\n";
#[cfg(not(windows))]
const LINE_ENDING: &str = "\n";
panic::set_hook(Box::new(move |panic| {
let backtrace = Backtrace::new();
let Some(l) = panic.location() else {
error!(
"Panic: {:?}, backtrace: {}{:#?}",
panic, LINE_ENDING, backtrace
);
return;
};
error!(
"Panic: {:?}, file: {}, line: {}, col: {}, backtrace: {}{:#?}",
panic,
l.file(),
l.line(),
l.column(),
LINE_ENDING,
backtrace,
);
}));
}
fn log_base_addr() {
for o in phdrs::objects() {
error!("Object name: {:?}, base addr: {:#x?}", o.name(), o.addr());
}
}
Cargo.toml
rust
[package]
name = "panic_demo"
version = "0.1.0"
edition = "2021"
[dependencies]
backtrace = "0.3"
phdrs = { git = "https://github.com/softdevteam/phdrs.git", rev = "86992b1e8e896a495387d931072c6088086eeabd" }
tracing = "0.1"
tracing-subscriber = "0.3"
[profile.release]
debug = true
opt-level = "z"
lto = true
codegen-units = 1
먼저, 심볼과 디버그 정보를 포함해 바이너리를 빌드합니다:
yaml
cargo build --release
참고: Cargo.toml에서 기본 release 프로파일 설정을 오버라이드했습니다.
빌드 후 target/release 디렉터리로 이동해 바이너리를 panic_demo_with_strip이라는 이름으로 복사한 뒤, strip 도구로 심볼과 debug info를 제거합니다:
yaml
cd target/release/
cp panic_demo panic_demo_with_strip
strip -s panic_demo_with_strip
이제 target/release에는 두 개의 바이너리가 있습니다:
panic_demo — 심볼과 디버그 정보가 모두 포함된 버전panic_demo_with_strip — 스트립된 버전panic_demo를 실행하면 디버그 심볼이 포함되어 있어 읽을 수 있는 스택 트레이스를 볼 수 있습니다:
rust
./panic_demo
rust
➜ ./panic_demo
2025-03-10T03:05:27.843007Z ERROR panic_demo: Object name: "", base addr: 0x5621ce28b000
2025-03-10T03:05:27.843075Z ERROR panic_demo: Object name: "linux-vdso.so.1", base addr: 0x738d45df3000
2025-03-10T03:05:27.843094Z ERROR panic_demo: Object name: "/usr/lib/libc.so.6", base addr: 0x738d45bd8000
2025-03-10T03:05:27.843107Z ERROR panic_demo: Object name: "/usr/lib/libgcc_s.so.1", base addr: 0x738d45baa000
2025-03-10T03:05:27.843120Z ERROR panic_demo: Object name: "/lib64/ld-linux-x86-64.so.2", base addr: 0x738d45df5000
2025-03-10T03:05:27.850986Z ERROR panic_demo: Panic: PanicHookInfo { payload: Any { .. }, location: Location { file: "src/main.rs", line: 20, col: 5 }, can_unwind: true, force_no_backtrace: false }, file: src/main.rs, line: 20, col: 5, backtrace:
0: 0x5621ce2b4ba5 - panic_demo::set_panic_hook::{{closure}}::hfc3c6c380e6b0be5
at /home/fys/rust_target/projects/panic_demo/src/main.rs:33:25
1: 0x5621ce2f0312 - <alloc::boxed::Box<F,A> as core::ops::function::Fn<Args>>::call::ha3fed88e6e913722
at /rustc/a4cb3c831823d9baa56c3d90514b75b2660116fa/library/alloc/src/boxed.rs:1984:9
std::panicking::rust_panic_with_hook::h1df75c095a4f3488
at /rustc/a4cb3c831823d9baa56c3d90514b75b2660116fa/library/std/src/panicking.rs:825:13
2: 0x5621ce31b5a5 - std::panicking::begin_panic_handler::{{closure}}::hf3afa20cd541c11f
at /rustc/a4cb3c831823d9baa56c3d90514b75b2660116fa/library/std/src/panicking.rs:683:13
3: 0x5621ce31b539 - std::sys::backtrace::__rust_end_short_backtrace::h3ced788cfddd85e3
at /rustc/a4cb3c831823d9baa56c3d90514b75b2660116fa/library/std/src/sys/backtrace.rs:168:18
4: 0x5621ce31bebc - rust_begin_unwind
at /rustc/a4cb3c831823d9baa56c3d90514b75b2660116fa/library/std/src/panicking.rs:681:5
5: 0x5621ce2d41ef - core::panicking::panic_fmt::ha07a50819406191f
at /rustc/a4cb3c831823d9baa56c3d90514b75b2660116fa/library/core/src/panicking.rs:75:14
6: 0x5621ce2b4b81 - panic_demo::b::h9ebbf8c80464f859
at /home/fys/rust_target/projects/panic_demo/src/main.rs:20:5
7: 0x5621ce2b4b4b - panic_demo::a::he4eee93f5289646a
at /home/fys/rust_target/projects/panic_demo/src/main.rs:14:5
8: 0x5621ce2b47dd - panic_demo::main::he3522539407f83f5
at /home/fys/rust_target/projects/panic_demo/src/main.rs:9:5
9: 0x5621ce2b2d4d - core::ops::function::FnOnce::call_once::h3e6e811b791b14aa
at /home/fys/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
std::sys::backtrace::__rust_begin_short_backtrace::h00639e0e41301441
at /home/fys/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/backtrace.rs:152:18
10: 0x5621ce2b5286 - main
11: 0x738d45bff488 - <unknown>
12: 0x738d45bff54c - __libc_start_main
13: 0x5621ce2b2c55 - _start
14: 0x0 - <unknown>
반면 panic_demo_with_strip을 실행하면 심볼과 debug info가 없기 때문에 읽을 수 없는 스택 트레이스가 출력됩니다:
rust
./panic_demo_with_strip
yaml
❯ ./panic_demo_with_strip
2025-03-10T03:05:49.818147Z ERROR panic_demo: Object name: "", base addr: 0x59bba9fd8000
2025-03-10T03:05:49.818207Z ERROR panic_demo: Object name: "linux-vdso.so.1", base addr: 0x77bf1596d000
2025-03-10T03:05:49.818216Z ERROR panic_demo: Object name: "/usr/lib/libc.so.6", base addr: 0x77bf15752000
2025-03-10T03:05:49.818221Z ERROR panic_demo: Object name: "/usr/lib/libgcc_s.so.1", base addr: 0x77bf15724000
2025-03-10T03:05:49.818226Z ERROR panic_demo: Object name: "/lib64/ld-linux-x86-64.so.2", base addr: 0x77bf1596f000
2025-03-10T03:05:49.818808Z ERROR panic_demo: Panic: PanicHookInfo { payload: Any { .. }, location: Location { file: "src/main.rs", line: 20, col: 5 }, can_unwind: true, force_no_backtrace: false }, file: src/main.rs, line: 20, col: 5, backtrace:
0: 0x59bbaa001ba5 - <unknown>
1: 0x59bbaa03d312 - <unknown>
2: 0x59bbaa0685a5 - <unknown>
3: 0x59bbaa068539 - <unknown>
4: 0x59bbaa068ebc - <unknown>
5: 0x59bbaa0211ef - <unknown>
6: 0x59bbaa001b81 - <unknown>
7: 0x59bbaa001b4b - <unknown>
8: 0x59bbaa0017dd - <unknown>
9: 0x59bba9fffd4d - <unknown>
10: 0x59bbaa002286 - <unknown>
11: 0x77bf15779488 - <unknown>
12: 0x77bf1577954c - __libc_start_main
13: 0x59bba9fffc55 - <unknown>
14: 0x0 - <unknown>
스택 트레이스를 어떻게 복구할까요? 주소 0x59bbaa001ba5를 예로 들어 복구 과정을 설명하겠습니다.
먼저 panic hook에서 각 오브젝트가 로드된 메모리 범위를 출력했습니다. 이 정보를 통해 0x59bbaa001ba5가 panic_demo_with_strip 오브젝트의 메모리 범위 안에 있음을 확인할 수 있습니다. 따라서 기준(Base) 주소로부터의 오프셋을 다음과 같이 계산할 수 있습니다:
plaintext
0x59bbaa001ba5 - 0x59bba9fd8000 - 1 = 0x29ba4
다음으로 addr2line 도구를 이용해 이 주소를 사람이 읽을 수 있는 소스 파일명과 라인 번호로 변환합니다.
⚠️ 주의: 컴파일 시
lto = true옵션을 사용했다면 GNU addr2line이 제대로 동작하지 않을 수 있습니다. 이 경우 gimli-rs/addr2line을 사용해야 합니다.
rust
addr2line -e panic_demo 0x29ba4
출력:
rust
➜ addr2line -e panic_demo 0x29ba4
/home/fys/rust_target/projects/panic_demo/src/main.rs:33
같은 방식으로 다른 주소들도 소스 코드의 파일명과 라인 번호로 변환할 수 있습니다. 이렇게 해서 사람이 읽을 수 있는 스택 트레이스를 복구했습니다!
크로스 컴파일은 언제나 도전적인 작업입니다. 만능 해결책은 없으며, 과정에서 다양한 문제가 발생할 수 있습니다. 대부분의 경우 상황에 맞춰 개별적으로 문제를 처리해야 합니다. 다행히 Cargo NDK와 Android NDK 같은 도구가 편리한 해결책을 제공해, 대부분의 컴파일 관련 과제를 효과적으로 다룰 수 있습니다.
이 글을 통해 Android 환경에서 크로스 컴파일의 중요성과 Rust 빌드 시스템의 장점을 살펴보았습니다. 이상적인 컴파일 과정은 현실에서 여러 장애물에 부딪히곤 하지만, 우리의 경험이 앞으로의 개발에 실질적인 인사이트와 영감을 제공하길 바랍니다.
Greptime은 업계 선도적인 시계열 데이터베이스 제품과 솔루션을 제공하여 IoT와 Observability 시나리오를 강화하고, 더 적은 시간과 복잡성, 비용으로 기업이 데이터에서 가치 있는 인사이트를 도출할 수 있도록 돕습니다.
GreptimeDB는 메트릭, 로그, 이벤트의 통합 저장 및 분석을 제공하는 오픈소스 고성능 시계열 데이터베이스입니다. 배포 없이 즉시 사용 가능한 완전관리형 DBaaS인 GreptimeCloud에서 바로 체험해 보세요!
엣지-클라우드 통합 솔루션은 멀티모달 엣지 데이터베이스와 클라우드 기반 GreptimeDB를 결합해 IoT 엣지 시나리오를 최적화하여 비용을 낮추면서 데이터 성능을 향상시킵니다.