이전 글
2026.02.19 - [안드로이드] - 안드로이드 컴파일 과정 완전정리 ④ - D8/R8 란?
안드로이드 컴파일 과정 완전정리 ⑤ - DEX 란?
이전 글에서 D8/R8이 .class → .dex 변환과(및 desugaring), 릴리즈 최적화까지 담당한다고 정리했다. 이번 글은 그 결과물인 DEX가 정확히 어떤 포맷이고, 내부가 어떻게 생겼고, 어떤 실행 모델(레지스터/명령 포맷)을 가지는지까지 DEX 스펙 레벨로 내려가서 정리한다.
핵심 결론부터 잡는다.
- DEX는 Android 런타임(ART)이 실행하는 바이트코드 포맷이다. (Android Open Source Project)
- JVM 바이트코드는 스택 기반 모델로 설명되는 경우가 많은 반면, DEX 바이트코드는 가상 레지스터(v0~)를 직접 명령 피연산자로 쓰는 모델로 스펙 문서에 드러난다(예: v0–v65535 언급, 명령 포맷에 레지스터 필드가 내장). (Android Open Source Project)
- 따라서 sum(a, b) 같은 정수 덧셈은 DEX(스마일리/smali 표기)에서 보통 add-int vA, vB, vC 형태로 표현된다(레지스터 3개를 명시).
1) D8/R8과 DEX의 연결: “JVM 산출물을 Android 실행 포맷으로 내리는 마지막 관문”
2~4편 흐름을 다시 붙이면 이렇게 된다.
- Kotlin 컴파일러: .kt → .class
- .class: JVM 규격 클래스 파일(바이트코드)
- D8/R8: .class → .dex (+ 필요 시 desugar, 릴리즈 최적화)
- ART: .dex 실행 (Android Open Source Project)
ART 문서가 “ART가 DEX 포맷과 DEX 바이트코드 스펙을 실행한다”고 명시한다. (Android Open Source Project)
▶ 더보기 — “포맷(format)”이란?
여기서 포맷은 “파일이 어떤 구조/필드/인코딩으로 저장되는지”를 뜻한다.
DEX는 바이트코드뿐 아니라, 문자열 테이블/타입 테이블/메서드 목록/디버그 정보 같은 부가 데이터까지 같이 담는 “컨테이너”다. (Android Open Source Project)
2) DEX는 “바이트코드 + 메타데이터” 묶음이다
AOSP DEX 포맷 문서는 .dex가 “클래스 정의 집합과 그에 딸린 보조 데이터(adjunct data)를 담는 파일”이라고 정의한다. (Android Open Source Project)
즉 .dex는 단순히 명령어 스트림만 있는 게 아니라, 최소한 다음 계층을 같이 가진다.
- 식별자 테이블(IDs): 문자열, 타입, 프로토타입(시그니처), 필드, 메서드
- 클래스 정의 목록(class definitions)
- 코드(code items): 각 메서드 바이트코드와 예외 처리/디버그 정보 등
- 여러 인코딩/정렬 규칙(LEB128, 정렬, 체크섬/해시 등)
또한 .dex는 “유효한 파일이 되기 위한 제약(constraints)”이 있고, 런타임은 “유효한 dex만 지원하면 된다”라고 문서가 말한다. (Android Open Source Project)
▶ 더보기 — LEB128이란?
DEX는 일부 정수 값을 **가변 길이 인코딩(LEB128)**으로 저장한다. DEX 포맷 문서는 LEB128을 “Little-Endian Base 128”이라고 설명하고, .dex에서는 32비트 수량 인코딩에만 사용된다고 명시한다. (Android Open Source Project)
3) DEX 파일 무결성: “아무 바이트나 넣으면 로드가 안 된다”
DEX 제약 문서에는 .dex가 유효하려면 만족해야 하는 조건들이 예시로 나열돼 있다. 대표적으로:
- magic 값(버전 표기)이 특정 패턴이어야 한다
- 체크섬(Adler-32), 시그니처(SHA-1) 규칙
- 섹션 오프셋/정렬(대부분 4바이트 정렬), 섹션 간 오버랩 금지
(Android Open Source Project)
이게 중요한 이유는, D8/R8이 “컴파일러”인 동시에 유효한 dex를 생성하는 포맷 생성기이기 때문이다.
▶ 더보기 — Adler-32 / SHA-1이 왜 나오나?
DEX 파일은 설치/로드 과정에서 “파일이 깨지지 않았는지” 빠르게 확인할 필요가 있다.
그래서 포맷 레벨에서 체크섬/해시 필드를 요구한다. (정확한 검증 시점/정책은 런타임/플랫폼 구현에 따라 달라질 수 있어 여기서 단정하지 않는다.)
4) DEX 바이트코드는 “16비트 코드 유닛(code unit)”로 인코딩된다
AOSP의 “DEX 명령 포맷(instruction formats)” 문서는 DEX 명령이 16비트 code unit 단위로 레이아웃이 정의된다고 설명한다(포맷 표의 각 “word”가 16비트 code unit을 의미). (Android Open Source Project)
또한 DEX 바이트코드 문서는 몇몇 가변 길이 payload(예: fill-array-data)가 있고, 이런 payload는 정렬 제약(예: 4바이트 정렬, 필요 시 nop 삽입)이 있음을 설명한다. (Android Open Source Project)
요약하면:
- DEX “명령 1개”는 보통 1개 이상의 16비트 유닛으로 구성된다. (Android Open Source Project)
- 어떤 payload는 실행 흐름 중 “직접 실행되면 안 되는” 특수 영역으로 취급되며, 정렬 규칙이 있다. (Android Open Source Project)
▶ 더보기 — code unit이란?
기계어에서 “바이트/워드”가 있듯, DEX 명령 인코딩에서 최소 단위를 “16비트 code unit”으로 정의해 둔 것이다.
“명령이 몇 바이트냐”가 아니라 “몇 개의 16비트 유닛이냐”로 설명되는 이유가 여기서 나온다. (Android Open Source Project)
5) DEX 실행 모델 핵심: 레지스터(가상 레지스터) 중심
DEX 바이트코드 문서에는 레지스터가 v0부터 v65535까지 언급되고, 어떤 명령은 더 큰 레지스터 범위를 다루기 위한 변형이 있으며, 변형이 없으면 “낮은 레지스터로 move해서 처리”하는 식의 기대 동작까지 적혀 있다. (Android Open Source Project)
또한 “instruction formats” 문서는 각 명령 포맷이 “최대 몇 개의 레지스터를 인코딩하는지” 같은 정의를 제공한다. (Android Open Source Project)
이 두 문서만으로도 DEX 모델이 “레지스터 인자(vA, vB, vC…)를 명령에 직접 박는 구조”라는 점은 확인 가능하다. (Android Open Source Project)
▶ 더보기 — 가상 레지스터(virtual register)란?
CPU 레지스터가 아니라 “바이트코드 수준에서 쓰는 논리적 슬롯”이다.
실제 하드웨어 레지스터로 매핑되는지는 런타임 컴파일러(예: ART의 JIT/AOT)가 결정한다. (어떤 방식으로 매핑되는지는 환경/버전/최적화에 따라 달라져서 여기서 확정 서술은 하지 않는다.)
6) 예시: sum(a, b)가 DEX에서 보이는 모양
Kotlin 코드:
fun sum(a: Int, b: Int) = a + b
이를 “DEX(스마일리) 관점”으로 보면 핵심 연산은 이런 형태로 표현될 수 있다.
- add-int vA, vB, vC : vB + vC 결과를 vA에 저장
즉 JVM 바이트코드가 “스택에 push → add → return” 같은 흐름으로 설명되는 경우가 많다면, DEX는 “어느 레지스터들을 더해서 어느 레지스터에 넣는지”가 명령 자체에 포함된다.
네가 예시로 든 add-int v0, v1, v2는 “v1+v2 결과를 v0에 넣는다”라는 의미로 쓰인다(스마일리 표기).
실제로 파라미터가 어느 레지스터(p0/p1 또는 v1/v2 등)에 잡히는지는 메서드 형태(정적/인스턴스), 레지스터 배치, 컴파일러 선택에 따라 달라질 수 있다. 이 글에서는 “add-int가 3-레지스터 형태”라는 구조만 확정적으로 말한다. (Android Open Source Project)
▶ 더보기 — v레지스터와 p레지스터
스마일리 문법에서는 흔히:
- vN: 로컬/임시 레지스터
- pN: 파라미터 레지스터(매개변수 영역을 별칭처럼 부르는 관례)
로 설명하는 자료가 많다. 다만 “p가 물리적으로 다른 레지스터 파일이다” 같은 의미로 이해하면 틀린 경우가 생긴다. 보통은 같은 레지스터 파일에서 “파라미터로 할당된 구간”을 가독성 때문에 p로 표기하는 관례에 가깝다. (이건 스마일리 표현 관례이고, DEX 파일 포맷 자체가 p/v를 별도로 저장한다고 이 글에서 단정하지 않는다.)
7) “DEX가 왜 이런 구조냐”를 포맷 관점에서 다시 보면
DEX 포맷 문서는 .dex가 클래스 정의 집합을 담고, 여러 타입/정수 인코딩(리틀엔디언, LEB128)을 사용한다고 설명한다. (Android Open Source Project)
DEX 제약 문서는 유효성(무결성/정렬/섹션 구조) 요구사항을 상세히 나열한다. (Android Open Source Project)
DEX 바이트코드/명령 포맷 문서는 레지스터 범위, 정렬, 16비트 code unit 기반 인코딩을 설명한다. (Android Open Source Project)
즉 “DEX는 Android 런타임이 빠르게 로드/검증/실행할 수 있도록 설계된, 구조화된 바이너리 포맷”이라는 결론이 자연스럽게 나온다(이 문장은 문서들의 내용을 종합한 요약이다). (Android Open Source Project)
8) 실무에서 DEX를 알아야 하는 지점
D8/R8을 다루다 보면 결국 DEX로 내려앉는 구간에서 문제를 만난다. 예를 들면:
- “왜 특정 Java 8 기능이 desugar에 걸려서 코드 형태가 바뀌는지”
- “왜 멀티덱스가 생기고, 어떤 클래스가 어디 dex에 들어가야 하는지”
- “왜 R8 난독화 후 스택트레이스가 바뀌는지(= dex 결과가 바뀜)”
이걸 이해하려면 DEX를 “그냥 변환 결과물”로 보지 말고, 독자적인 실행 포맷/명령 집합으로 봐야 한다.
참고(검증 가능한 1차 문서 위주)
- ART가 DEX 포맷과 DEX 바이트코드 스펙을 실행한다고 명시: (Android Open Source Project)
- DEX 파일 레이아웃/인코딩(LEB128 포함) 설명: (Android Open Source Project)
- DEX 유효성 제약(무결성/정렬/섹션 규칙 등): (Android Open Source Project)
- DEX 바이트코드의 레지스터 범위/정렬/특수 payload 설명: (Android Open Source Project)
- Dalvik/DEX 명령 포맷이 16비트 code unit 기반임을 설명: (Android Open Source Project)
'안드로이드' 카테고리의 다른 글
| 안드로이드 컴파일 과정 완전정리 ⑥ - ART란? (0) | 2026.02.24 |
|---|---|
| 안드로이드 컴파일 과정 완전정리 ⑤ 부록 - DEX 는 알겠고 APK 는요? (0) | 2026.02.23 |
| 안드로이드 컴파일 과정 완전정리 ④ - D8/R8 란? (0) | 2026.02.21 |
| 안드로이드 컴파일 과정 완전정리 ③ - JVM 이란? (0) | 2026.02.20 |
| 안드로이드 컴파일 과정 완전정리 ② - 코틀린 컴파일러란? (0) | 2026.02.19 |
댓글