결론 및 결정 조건
- 모든 SO 에 대해 직접 및 간접 의존성, 로딩 진입점, 초기화 타이밍, 최소 대상 API, 호출자를 기록합니다.
- 정적 JNI 이름 발견과 RegisterNatives 동적 등록은 서로 다른 제약 조건을 가지므로 이름 처리 전에 실제 바인딩 방식을 검증해야 합니다.
- arm64-v8a, armeabi-v7a, x86_64 는 각각 고유한 아티팩트와 호환성 행렬을 나타내며, 한 아키텍처에서의 성공이 다른 아키텍처로도 확장된다고 보장할 수 없습니다.
- 보호 강도 수용성과 런타임 안정성 수용성은 동일한 최종 APK, 서명, SO 세트에 연동되어야 합니다.
ELF 의존성 그래프와 실제 로딩 타임라인을 먼저 매핑하십시오
Android 애플리케이션은 메인 라이브러리, 비즈니스 로직 라이브러리, 알고리즘 라이브러리, 서드파티 SDK, 시스템 라이브러리로 구성될 수 있습니다. Java 나 Kotlin 코드가 System.loadLibrary 를 직접 호출할 수 있으며, 메인 라이브러리는 ELF 의존성이나 런타임 로딩을 통해 추가 라이브러리를 로드할 수 있습니다. 심볼 해결 및 초기화 순서는 전체 의존성 체인에 의해 결정되므로 최종 충돌 지점은 보호된 라이브러리 외부에 있을 수 있습니다.
의존성 그래프는 ABI 별 라이브러리 파일, DT_NEEDED 관계, 최소 시스템 API 레벨, 로딩 진입점, 초기화 함수, 내보낸 인터페이스, 호출자를 기록해야 합니다. 또한 라이브러리가 자체 개발인지, 오픈소스 의존성인지, 폐쇄형 SDK 인지 표시하여 수정 범위와 회귀 테스트 책임의 경계를 정의해야 합니다.
Android NDK 문서에 따르면 네이티브 심볼은 일반적으로 라이브러리 로딩 중에 해결됩니다. 타겟 시스템에 없는 API 를 코드에서 참조하면, 런타임 분기로 해당 코드 경로가 실행되지 않더라도 dlopen 이 즉시 실패할 수 있습니다. 따라서 의존성 그래프에 NDK API 최소 버전과 디바이스 시스템 버전을 반드시 포함해야 합니다.
| 필드 | 기록 항목 | 애플리케이션 강화 (Hardening) 에 미치는 영향 | 검증 방법 |
|---|---|---|---|
| 라이브러리 및 소스 | 메인 라이브러리, 전달적 의존성, 서드파티 라이브러리, 시스템 라이브러리 | 수정 가능 범위와 회귀 테스트 (Regression Testing) 책임 주체를 결정함 | 최종 APK 에서 각 ABI 별로 언팩하여 검증 |
| 로딩 진입점 | 정적 의존성, System.loadLibrary, 또는 런타임 dlopen | 가장 초기 실패 단계와 로그 위치를 결정함 | 스타트업 타임라인과 로딩 결과 기록 |
| API 최소 버전 (Floor) | 빌드 APP_PLATFORM 및 사용된 시스템 심볼 | 디바이스 API 보다 높을 경우 로딩 시점에 심볼 누락 발생 가능 | 타겟 시스템에서의 실기기 스타트업 및 심볼 검증 |
| 초기화 순서 | JNI_OnLoad, 생성자 (constructors), 비즈니스 로직 초기화 | 코드 레이아웃이나 타이밍 변경이 암시적 의존성을 증폭시킬 수 있음 | 보호 처리되지 않은 빌드와 릴리스 후보 (Release Candidate) 빌드 간 타임라인 비교 |
| 예외 및 스레드 | C++ 예외, 스레드 소유권, JNIEnv 사용 | 라이브러리 간 및 스레드 간 오류는 종종 즉시 크래시를 유발함 | CheckJNI, 심볼화된 스택 트레이스, 비즈니스 회귀 테스트 (Regression Testing) 수행 |
정적 등록 대 동적 등록은 심볼 처리 경계를 정의함
JNI 는 명명 규칙을 통해 네이티브 메서드를 발견하거나 초기화 중 RegisterNatives 를 사용하여 Java 메서드와 함수 주소 간 매핑을 설정할 수 있습니다. 정적 등록은 예측 가능한 내보낸 이름에 의존하는 반면, 동적 등록은 일반적으로 몇 개의 초기화 진입점만 노출하면 되지만 클래스 조회, 메서드 시그니처, 등록 타이밍, 로딩 스레드에 의존합니다.
강화 (Hardening) 나 심볼 처리 과정에서 정적 등록 이름이 변경되면 시스템이 네이티브 구현을 찾지 못해 실패할 수 있습니다. 마찬가지로 동적 등록 테이블에서 메서드 시그니처, 클래스 이름 유지 규칙, 초기화 순서가 변경되면 첫 번째 함수 호출 (Function Invocation) 전에 실패가 발생할 수 있습니다. 단순히 내보낸 심볼 개수만 확인해서는 JNI 바인딩이 정확히 유지되었음을 증명할 수 없습니다.
공식 Android JNI 가이드라인도 JNIEnv 에 스레드 제약이 있음을 경고합니다. 보류 중인 예외, 무효한 참조, 스레드 간 사용은 충돌을 유발할 수 있습니다. 하드닝 적용 후 회귀 테스트는 단순한 인자 없는 데모 메서드 호출이 아닌 실제 스레드와 예외 경로를 반드시 커버해야 합니다.
| 등록 방식 | 종속성 | 하드닝 민감 지점 | 최소 검증 요건 |
|---|---|---|---|
| 정적 이름 탐색 | Java 클래스명, 메서드명, 시그니처 및 내보낸 심볼 | 이름 변경, 숨김 심볼 및 시그니처 불일치 | 각 주요 진입점을 호출하여 링크 오류 확인 |
| RegisterNatives | 클래스 조회, 메서드 시그니처, 등록 테이블 및 초기화 타이밍 | 클래스명 처리, 등록 순서, 함수 주소 변경 | 등록 완료 확인 후 실제 입출력 시나리오 실행 |
| 서드파티 래퍼 | 내부 SDK 규칙 및 비공개 구현체 | 자체 검증, 암시적 내보내기 및 버전 차이 | 벤더 지원 매트릭스 및 실제 시나리오 대비 검증 |
- 주요 Java 대 네이티브 매핑 목록 작성
- 정적 또는 동적 등록 방식 확인
- 필수 클래스명 및 시그니처 규칙 유지
- 예외, 스레드 및 참조 수명 주기 검증
ABI 는 독립적인 릴리스 아티팩트로 승인되어야 함
ABI 는 단순한 디렉터리 라벨이 아닙니다. 공식 Android NDK 문서에 따르면 ABI 는 명령어 세트, 엔디안, 호출 규약, 스택 및 레지스터 사용법, 실행 파일 형식, C++ 이름 맹글링을 정의합니다. arm64-v8a 와 armeabi-v7a 는 기계어 코드와 종속성이 다르므로 x86_64 에뮬레이터 통과가 ARM 실기기에서의 성공을 보장하지 않습니다.
각 ABI 에 대해 모든 직접 및 간접 종속성이 존재하는지, 라이브러리 API 최소 버전이 대상 시스템과 호환되는지, 패키징 도구가 특정 아키텍처용 파일을 잘못 삭제하지 않았는지 확인해야 합니다. 메인 라이브러리만 arm64-v8a 를 포함하고 서드파티 종속성에 해당 아티팩트가 없다면 애플리케이션은 로딩 단계에서 여전히 실패합니다.
제품이 ABI 하위 집합만 지원할 계획이라면 릴리스 노트, 앱 스토어 구성 및 테스트 범위에 이를 명시해야 합니다. 빌드 불가, 설치 불가 또는 주요 JNI 경로가 실행되지 않은 아키텍처는 커버리지 없음으로 표시해야 하며, 컴파일 성공이 런타임 증거를 대체할 수 없습니다.
| 점검 항목 | arm64-v8a | armeabi-v7a | x86_64 | 결정 규칙 |
|---|---|---|---|---|
| 모든 종속성 존재 | 라이브러리별 검증 | 릴리스 범위별 검증 | 필요한 경우에만 검증 | 필수 종속성 중 하나라도 누락되면 차단 |
| 최소 시스템에서 로드 가능 | 대상 API 실제 디바이스 | 대상 API 실제 디바이스 | 에뮬레이터 또는 디바이스 | 최신 시스템에서만 검증하지 않음 |
| 중요 JNI 경로 | 실제 입출력 | 실제 입출력 | ARM 결과를 대체할 수 없음 | 릴리스된 각 ABI 별로 독립적으로 기록 |
| 크래시 귀속 가능성 | 일치하는 심볼 유지 | 일치하는 심볼 유지 | 일치하는 심볼 유지 | 심볼은 동일한 릴리스 후보와 일치해야 함 |
보호 조치가 영향을 미치는 네이티브 런타임 조건은 무엇인가?
심볼 난독화는 정적 단서를 줄이고, 문자열 처리는 평문 노출을 낮추며, 제어 흐름 또는 함수 가상화는 선택된 코드의 실행 형태를 변경합니다. 이러한 기법은 내보낸 심볼, 함수 경계, 예외 언와인딩, 간접 호출, 정렬 및 성능에 각각 다른 영향을 미칩니다. 후처리 후 심볼이 줄어들었다고 해서 네이티브 보호가 완료된 것은 아니며, 라이브러리를 읽을 수 있다고 해서 모든 보호 효과가 무효화되는 것도 아닙니다.
스타트업 초기화, 시그널 처리, C++ 예외, 콜백, 스레드 로컬 상태, 리플렉티브 심볼 조회 및 서드파티 SDK 자체 검증은 고감도 영역입니다. 범위를 선정할 때는 명확한 입출력, 측정 가능한 호출 빈도, 격리 가능한 장애를 가진 비즈니스 기능을 우선시하십시오. 첫 번째 PoC 에서 전체 초기화 체인을 처리하지 마십시오.
보호 구성은 각 함수 또는 함수 그룹에 대해 근거, 호출 단계, ABI, 종속성, 성능 예산 및 롤백 그룹을 기록해야 합니다. 다음은 내부 심볼이나 제품 구성 구문을 제외하고 공개적이고 안전한 검증 체크리스트의 형식만 보여줍니다.
native_group: protocol-core-v1
abis: [arm64-v8a, armeabi-v7a]
load_phase: post-authentication
jni_registration: dynamic
execution:
frequency: bounded
main_thread: false
dependencies:
- business-runtime
- system-crypto
acceptance:
- dependency-resolution
- jni-registration
- exception-path
- output-parity
- latency-budget
rollback: native-group-v0최초 차이점을 기반으로 SO 하드닝 후 크래시 위치 특정
문제 해결 시 먼저 보호되지 않은 베이스라인과 릴리스 후보 모두에 대해 파일 식별자, 서명, 디바이스, 시스템, 설치 방법 및 비즈니스 입력을 고정하십시오. 그런 다음 타임라인에서 프로세스 생성, 애플리케이션 시작, 라이브러리 로드, JNI_OnLoad, 등록 완료, 첫 번째 네이티브 호출 또는 주요 비즈니스 반환과 같은 최초 차이점을 찾으십시오. 마지막 크래시 로그 항목은 단순한 연쇄 반응일 수 있습니다.
UnsatisfiedLinkError 는 일반적으로 라이브러리 누락, ABI 불일치, 종속성 문제 또는 심볼 해결 문제를 나타냅니다. 등록 오류는 네이티브 메서드 누락으로 나타날 수 있으며, 잘못된 JNIEnv, 참조 또는 대기 중인 예외는 비즈니스 로직 호출 중 크래시를 유발할 수 있습니다. 내부 귀속 분석을 위해 릴리스 후보 버전과 일치하는 네이티브 심볼을 유지하되, 공개 보고서에는 실제 심볼이나 주소를 노출하지 마십시오.
CheckJNI 는 일부 JNI 오용을 감지하는 데 도움이 되지만 진단 도구일 뿐이며 프로덕션 런타임 상태를 대표하지 않고 모든 네이티브 메모리 및 동시성 문제를 커버하지는 못합니다. 수정 후에는 대상 릴리스 구성에서 핵심 경로를 반드시 재실행해야 합니다.
| 최초 증상 | 우선순위 점검 항목 | 필수 증거 자료 | 최초로 수행하지 말아야 할 사항 |
|---|---|---|---|
| dlopen 실패 | ABI, DT_NEEDED, 최소 API 수준 및 심볼 | 릴리스 후보 버전과 일치하는 라이브러리 목록 및 로딩 오류 | 관련 없는 보호 스위치를 반복적으로 토글하는 행위 |
| 네이티브 메서드를 찾을 수 없음 | 등록 메서드, 클래스 이름, 메서드 시그니처 및 타이밍 | 등록 테이블, 유지 규칙 및 호출 진입점 | 내보낸 심볼 개수만 확인하는 행위 |
| 첫 번째 호출 시 크래시 발생 | 매개변수, 참조, 스레드, 예외 및 함수 처리 | 심볼화 된 스택, 입력 값 및 기준선 비교 | 다른 버전의 심볼 파일로 설명하려는 시도 |
| 일부 기기에서 실패 발생 | 시스템 API, 벤더별 차이, ABI 및 서드파티 라이브러리 | 기기 시스템 매트릭스 및 최초 차이점 | 에뮬레이터 성공 사례로 실제 기기 결론을 대체하는 행위 |
최종 릴리스 결론은 강도, 호환성 및 유지보수성을 모두 포괄해야 함
정적 강도 관찰은 심볼, 문자열, 코드 구조 및 주요 진입점의 노출 면적 변화를 기록할 수 있으며, 런타임 수용 테스트는 로딩, JNI, 핵심 비즈니스 로직, 예외 및 대상 ABI 를 검증합니다. 이 두 가지 유형의 증거는 상호 보완적이며 서로를 대체할 수 없습니다.
최종 릴리스 후보 버전은 설치 업그레이드, 서명 검증, 채널 확인, 시작 테스트, 크래시 모니터링, 네이티브 심볼 아카이빙 및 롤백 훈련을 반드시 완료해야 합니다. 심볼 아카이브는 파일 식별자를 통해 일치하는 버전을 찾을 수 있어야 하며, 그렇지 않을 경우 온라인 네이티브 크래시를 올바르게 심볼화할 수 없습니다.
본 문서는 특정 SO 나 보호 구성에 대한 테스트 결론이 아닌 엔지니어링 검사 프레임워크를 제공합니다. 대상 패키지, 대상 ABI 및 실제 호출 경로가 없으면 준비 작업의 완료 여부만 평가할 수 있으며 호환성이나 성능을 보장할 수 없습니다.
- 릴리스된 모든 ABI 는 런타임 증거를 보유해야 합니다.
- 주요 JNI 진입점은 정상 경로와 예외 처리 경로를 모두 커버해야 합니다.
- 네이티브 심볼은 최종 릴리스 후보 (release candidate) 의 식별자와 일치해야 합니다.
- 로딩, 비즈니스 로직 수행 및 롤백 결과가 재현 가능해야 합니다.
- 검증되지 않은 디바이스와 시스템은 명시적으로 제한됩니다.
증거 및 적용 범위
이 섹션에서는 문서화된 플랫폼 사실, 엔지니어링 판단, 확인되지 않은 제품 주장으로 일반화할 수 없는 제한 사항을 구분합니다.
| 기사판정 | 사실 또는 공학적 근거 | 적용 범위 |
|---|---|---|
| ABI 는 독립적인 아티팩트 차원으로 승인되어야 합니다. | 공식 Android NDK 는 명령어 세트, 호출 규약, 레지스터, 스택, ELF 형식 및 이름 매angling 을 포함하는 ABI 를 정의합니다. | 문서상의 정의만으로 애플리케이션이 완전한 종속성을 포함하거나 대상 디바이스에서 성공적으로 실행됨을 증명할 수 없습니다. |
| 디바이스 시스템 버전보다 높은 네이티브 API 를 참조하면 로딩 시점에 실패할 수 있습니다. | NDK 일반 문제 문서에 따르면 심볼은 일반적으로 라이브러리 로딩 시점에 해결되며, 존재하지 않는 API 를 참조하는 경우 런타임 분기 처리로 우회할 수 없습니다. | 구체적인 실패 사례는 빌드 파라미터, 종속성 및 디바이스 로그를 통해 반드시 확인해야 합니다. |
| JNI 오류는 스레드, 참조 및 예외 수준에서 검증되어야 합니다. | Yudun의 Android JNI 가이드라인은 충돌을 일으킬 수 있는 유효하지 않은 JNIEnv, 참조, 대기 중인 예외 및 메서드 등록과 같은 문제를 나열합니다. | CheckJNI 는 문제의 일부 하위 집합을 탐지하는 데만 도움을 줄 뿐이며 포괄적인 비즈니스 로직 및 메모리 안전성 테스트를 대체할 수 없습니다. |
| 단일 x86_64 에뮬레이터에서의 결과를 ARM 기반 실기기로 확장 적용할 수 없습니다. | 서로 다른 ABI 는 서로 다른 명령어 세트와 호출 규약을 사용하며, 최종 라이브러리 및 종속성 조합도 다를 수 있습니다. | 제품이 특정 ABI 를 명시적으로 릴리스하지 않는 경우 강제 테스트 대신 해당 사항 없음으로 표시할 수 있습니다. |
| 보호 강도와 호환성은 동일한 릴리스 후보 (release candidate) 에서 확정되어야 합니다. | SO 파일을 재빌드하거나 교체하면 코드, 심볼, 종속성 및 잠재적 런타임 동작이 변경됩니다. | 이는 아티팩트 거버넌스 원칙이며 특정 보호 수준이 통과되었음을 의미하지는 않습니다. |
엔지니어링 질문
메인 SO 가 하나뿐이라도 종속성 그래프가 여전히 필요한가요?
예. 메인 SO 는 여전히 시스템 라이브러리, C++ 런타임 또는 서드파티 라이브러리에 의존할 수 있으며 JNI 를 통해 Java 계층과 상호작용합니다. 의존성 그래프는 가장 빠른 로딩 시점과 책임 범위를 확인시켜 줍니다.
arm64-v8a 테스트를 통과했다면 armeabi-v7a 도 별도로 테스트해야 합니까?
릴리스 패키지가 armeabi-v7a 를 포함하고 지원한다면 독립적인 검증이 필요합니다. 두 ABI 는 명령어 세트, 호출 규약 및 의존성 아티팩트가 서로 달라 상호 대체할 수 없습니다.
내보낸 심볼을 모두 숨기는 것이 더 안전합니까?
반드시 그렇지는 않습니다. 필수 정적 JNI 진입점, 서드파티 호출 및 시스템 규약은 내보낸 심볼에 의존할 수 있습니다. 먼저 등록 및 호출 방식을 확인한 후 노출 범위를 최소화하십시오.
CheckJNI 가 오류를 보고하지 않으면 릴리스해도 됩니까?
아니요, 이것만으로는 릴리스하기에 불충분합니다. 실제 타겟 구성, 비즈니스 입력, 예외 경로, ABI, 시스템 범위, 설치 업그레이드 및 롤백 기능을 반드시 검증해야 합니다.
SO 애플리케이션 강화 적용 후 크래시가 발생하면 모든 보호 기능을 즉시 비활성화해야 합니까?
먼저 릴리스 후보 빌드를 고정하고 earliest difference 를 찾아야 합니다. 보호 그룹을 이진 검색하거나 롤백할 수 있지만, 새로운 귀속 불가 아티팩트 생성을 방지하기 위해 한 번에 기록 가능한 변수 하나만 변경하십시오.
자신의 앱에서 이를 테스트하고 싶으신가요?
Yudun PoC 및 호환성 평가를 위한 릴리스 후보, 대상 시스템 및 중요한 비즈니스 경로를 제출하세요.