DEX File format

DEX File format

2024년 3월 21일
android, dex

참조 #

DEX (Dalvik EXecutable) Format #

Android 런타임에 최종적으로 실행되는 코드가 포함된 파일이며, 덱스파일 헤더와 실제 데이터를 담는 본문으로 구분된다.

Dex 헤더 (dex\n035\0) #

afcc6f35-a756-48a9-b78a-9b8644ce60ca

Magic [0x00-0x07] #

dex\n035\0 형식으로 되어있는데, 각 버전마다 특징이 조금씩 다르고, 손상된 경우 설치가 거절된다.

Checksum [0x08-0x0b] (adler-32) #

0x0c(시그니쳐) 부터 EOF 까지 바이트에 대한 체크섬이며, 설치할때 체크섬 검사를 해서 손상된 경우 설치가 거절된다. 파일 손상을 검증하는데 사용하는 값

Signature [0x0c-0x1f] (SHA-1) #

0x20(filesize) 부터 계산되며 멀티덱스의 경우처럼 덱스 파일을 고유하게 식별하는데 사용된다. 동일한 SHA-1 값이 최적화 되어있다면 캐시 재활용 등

File Size [0x20-0x23] #

헤더를 포함한 dex 파일 전체 바이트 사이즈이다.

Header Size [0x24-0x27] #

헤더는 항상 0x70 byte 이다.

Endian [0x28-0x2b] #

엔디안을 확인할 때 사용되는 필드이며, 0x12345678을 저장하는데 엔디안에 맞게 저장되기 때문에 dex 파일을 바이트로 읽어보면 확인하기 쉽다.
ex) 0x78563412

링크 데이터 영역을 가리키는 오프셋과 데이터 수를 의미한다. link_data section 는 dex 파일 포맷에서 가장 아래에 위치하며, 클래스 계층 정보, 클래스 초기화 정보, 메서드 인라이닝 정보, 문자열 중복 제거용 정보, 코드 재정렬 정보 등 최적화와 관련된 다양한 메타 데이터를 저장한다.

vdex를 확인했을 때에도 0인 것을 확인할 수 있는데, AOSP 소스코드에서 확인해보면 unused로 되어있고 사용하는곳도 안보인다.. https://android.googlesource.com/platform/art/+/master/libdexfile/dex/dex_file.h#147

dd58779c-35cf-49aa-8139-83ed0aeea4ab

map offset [0x34-0x37] #

map_list의 오프셋을 의미한다. map_list에는 dex 파일에서 섹션 정보를 순서대로 나열해서 헤더부터 맵리스트까지 각각의 type, Count, Offset이 순서대로 저장되는 영역이다.

대부분의 필드가 겹치지만, 헤더에는 코드 영역부터 data라고 퉁치기 때문에 map list는 중요하다.

IDS count/offset [0x38-0x6f] #

strings, type, proto, field, method, class defs, data 섹션 영역에 대한 count(4byte)/offset(4byte) 가 저장되어있다.

a2a9e3c4-5912-44ec-ac1f-cd2e84df4361


IDS (ID Section) #

헤더에서 각각의 IDS에 대한 오프셋을 확인할 수 있었다. IDS에서는 각 섹션의 아이템들의 구조체들이 저장되어 있다.

String IDs #

5a6bd195-c624-4cda-b1ef-c539ddae8d7f

실제 문자열들은 String Data 영역에 있고, 데이터 영역에서 문자열에 대한 오프셋(4byte)이 문자열 기준 사전순으로 정렬되어 저장된다.

Type IDs #

f456bf92-286f-4c9e-83d7-960e9ca031b7

타입은 데이터 타입을 나타내는 문자열 기반 식별자이고, 현재 dex의 smali 코드에서 사용하고 있는 타입들에 대한 문자열 오프셋 인덱스가 저장되어 있다.
이 인덱스로 String IDs 를 접근하면 타입 문자열의 오프셋을 얻을 수 있다.

  • 기본 타입
    한글자로 표시한다.
    ex) int=I, void=V, float=F, boolean=Z, byte=B
  • 클래스
    L<FullClassName>; 형식으로 작성한다.
    ex) java.lang.String=Ljava/lang/String;
  • 배열
    [ 문자로 시작한다.
    ex1) int[]=[I
    ex2) String[][]=[[Ljava/lang/String;

Proto IDs #

97815a58-fc23-435f-8dfd-35e0acbb9530

메소드의 프로토 타입(리턴타입, 파라미터 타입)을 식별하는데 사용되는 IDs이다.
리턴 타입은 한개이기 때문에 바로 type_ids를 가리키지만, 파라미터 타입은 여러개가 들어오기 때문에 type_list 를 가리킨다.

  • shorty_idx
    타입에서 클래스패스를 제외하고 리턴타입과 파라미터타입만 짧게 표현한 스트링을 가리키는 인덱스. 런타임 타입 체크를 빠르게 하기 위해 사용된다. 참조는 무조건 L로 지정되기 때문에 예시에서는 L(Object), I(int), L(String[]) 가 된다.
  • return_type_idx
    리턴 타입에 대한 type_ids 인덱스이다.
  • parameters_off
    파라미터의 타입을 지정하는 type_list 오프셋이다. Type IDs를 참조해서 파라미터별 타입을 지정한다.

Field IDs #

497c055a-e6a6-450a-9011-57dc58cd240a

앱 내 모든 필드(변수)에 대한 필드가 속한 클래스명, 타입, 이름을 저장하게 된다. 접근제한자나 정적멤버 여부 등에 대한 정보는 포함되지 않는다.
어떤 타입의 변수가 어떤 클래스에 있고 이름은 뭔지 알 수 있게된다.

Method IDs #

9dbab728-f54e-4182-88f5-5a9c5663460b

메소드가 선언된 클래스명, 선언한 프로토타입, 이름을 저장하게 된다.


class_defs #

 1public class ClassDefItem implements RawDexObject {
 2  public static int data_size = 0x20;
 3
 4  public int classIdx;
 5  public int accessFlags;
 6  public int superclassIdx;
 7  public Offset interfacesOff;
 8  public int sourceFileIdx;
 9  public Offset annotationsOff;
10  public Offset classDataOff;
11  public Offset staticValuesOff;
12
13  @Override
14  public void write(DexRandomAccessFile file) throws IOException {
15    file.getOffsetTracker().updatePositionOfNextOffsettable(file);
16    file.writeUInt(classIdx);
17    file.writeUInt(accessFlags);
18    file.writeUInt(superclassIdx);
19    file.getOffsetTracker().tryToWriteOffset(interfacesOff, file, false /* ULEB128 */);
20    file.writeUInt(sourceFileIdx);
21    file.getOffsetTracker().tryToWriteOffset(annotationsOff, file, false /* ULEB128 */);
22    file.getOffsetTracker().tryToWriteOffset(classDataOff, file, false /* ULEB128 */);
23    file.getOffsetTracker().tryToWriteOffset(staticValuesOff, file, false /* ULEB128 */);
24  }
25}

애플리케이션 내 모든 클래스의 정의를 리스트 형태로 담고있다.

class_idx (4byte) #

클래스의 타입을 가리키는 type_ids 의 인덱스. 클래스도 타입이다.

access_flags (4byte) #

클래스에 대한 액세스 플래그(public, final, private 등)를 담고 있다.

superclass_idx (4byte) #

슈퍼클래스의 type_ids 인덱스이다. 없으면 NO_INDEX(0xffffffff)의 상수값이 된다. 자바에서는 다중상속이 불가능하다.

interfaces_off (4byte) #

인터페이스의 type_ids 인덱스 배열들을 저장한 type_list의 오프셋이다. 인터페이스는 여러개 올 수 있기 때문에 type_list에 저장하고 그 인덱스를 사용한다.

source_file_idx (4byte) #

원본 소스를 포함하는 파일 이름의 string_ids 인덱스이다. 없다면 NO_INDEX로 세팅 된다.
디버깅이나 오류메시지, 스택트레이스에서 사용할 수 있다. 릴리즈빌드로 빌드해도 이 영역은 남아있다.

160da903-45fe-4d2d-ace3-ccb2e2f43655

annotations_off (4byte) #

클래스의 annotation 정보가 담긴 annotation_directory_item 구조체를 가리키는 오프셋이며 data영역을 가리킨다.

 1struct annotations_directory_item {
 2    uint32_t class_annotations_off;   // 클래스에 적용된 어노테이션
 3    uint32_t fields_size;             // 필드에 적용된 어노테이션 수
 4    uint32_t methods_size;
 5    uint32_t parameters_size;
 6    // size가 있는경우에만 아래 필드들이 존재함
 7    annotation_off_item field_annotations[fields_size]; // 필드에 적용된 어노테이션 리스트
 8    annotation_off_item method_annotations[methods_size];
 9    annotation_off_item parameter_annotations[parameters_size];
10};

2381cc9b-1949-44d6-8c55-a70bebf94d68

visibility에 따라 빌드시간에만 표시되고 말거나(0x00) 런타임에도 존재(0x01)하여 reflection으로도 접근 가능한 어노테이션이 될 수 있다. 0x02는 시스템용이다.
encoded_value는 타입에 따라 이후 바이트를 읽을 수 있다.

class_data_off (4byte) #

클래스의 필드, 메서드 등 구현 정보가 저장된 class_data 의 오프셋을 가리킨다.

static 필드(멤버변수)나 일반필드가 나뉘어있고, smali 함수 호출방식에 따라 direct, virtual 함수가 나뉘어 있다.

  • direct 메서드 : 생성자 <init>, static 블록 <cinit>, static 메서드, private 메서드가 있으며 오버라이드할 수 없는 메서드이다.
  • virtual 메서드 : public, protect, interface 등 오버라이드 가능하여 vtable을 통해 호출되는 모든 메서드를 말한다. (invoke-virtual 로 호출됨)
 1struct class_data_item {
 2    uleb128 static_fields_size;    // static 필드의 수
 3    uleb128 instance_fields_size;  // 일반 필드의 수
 4    uleb128 direct_methods_size;   // direct 메서드 수
 5    uleb128 virtual_methods_size;  // virtual 메서드 수
 6
 7    encoded_field static_fields[static_fields_size];
 8    encoded_field instance_fields[instance_fields_size];
 9    encoded_method direct_methods[direct_methods_size];
10    encoded_method virtual_methods[virtual_methods_size];
11}
12
13struct encoded_field {
14    uleb128 field_idx_diff;   // 필드 인덱스의 차이 (이전 값 기준)
15    uleb128 access_flags;     // 접근 플래그 (예: public, private 등)
16};
17struct encoded_method {
18    uleb128 method_idx_diff;  // 메서드 인덱스 차이
19    uleb128 access_flags;
20    uleb128 code_off;
21};

uleb128 타입으로 되어있는 것을 볼 수 있다. 이 방식은 4byte 데이터를 표현할 때 각 바이트의 맨 앞 1bit를 계속 플래그로 사용하는 가변길이 방식인데, 5byte까지 길어지는 대신 작은 수는 1byte 로 압축할 수 있는 장점이 있다.

dd07be6f-8bfa-4fc0-935f-58e4671a7948

코드영역은 첫 2byte가 메서드 안애서 사용하는 레지스터 수이다. JADX의 결과와 비교하면 잘 찾아온것을 확인할 수 있다.

static_values_off (4byte) #

static 필드들의 초기값을 담고있는 encoded_array_item의 offset 값을 가리킨다. 이 값이 설정되어있다는 것은 static 필드 중 초기값이 설정된게 있다는 의미이며, class_data 의 static_fields 와 크기와 순서가 매칭되어야한다.


Data #

string, code 등 실제 데이터들이 포함되어 있는 영역이며, ids 영역에서 오프셋으로 data 영역을 참조하고 있다.

code #

code 영역에는 하나의 code_item 이 하나의 메서드 단위로 저장되어 있고, 메서드에 대한 정보와 실제 코드가 bytecode 로 저장되어있다.

 1struct code_item {
 2    uint16_t registers_size;     // 전체 레지스터 수
 3    uint16_t ins_size;           // input 파라미터 개수
 4    uint16_t outs_size;          // 다른 메서드 호출 시 필요한 인자 공간
 5    uint16_t tries_size;         // 예외 처리 블록 개수 (없으면 0)
 6    uint32_t debug_info_off;     // 디버깅 정보 (없으면 0)
 7    uint32_t insns_size;         // Dalvik 명령어 수 (2바이트 단위)
 8    uint16_t insns[insns_size];  // Dalvik 바이트코드 배열
 9    // (선택) padding + try_item[] + catch_handler[]
10}

코드는 bytecode 테이블을 이용해서 해석할 수 있지만 가능성만 확인해보자.

230e4656-bb91-481b-996d-ab2900690049

type_list #

인터페이스 리스트나 메서드의 파라미터 타입을 지정할때 사용한다. 두 경우 모두 여러개가 들어올 수 있기 때문에 리스트 형태로 관리한다.
인터페이스 리스트도 역시 클래스 타입의 리스트로 관리

1struct type_list {
2    uint32_t size;
3    type_item list[size];  // type_ids 테이블의 인덱스를 저장한 uint16_t 배열
4};

string_data_item #

문자열의 길이를 바이트 단위 uleb128(가변길이)으로 표시하고, 이후에 길이만큼 실제 데이터가 \0 없이? 저장되어 있다. uleb는 1bit의 continue 비트와 7bit의 실제 데이터 비트로 구성되며, continue 비트가 세팅되면 다음 바이트까지 데이터가 있다는 의미이다.

b0bd2bfd-5bb3-4240-8a5e-91fe7c0f4f18

class_data #

dex에 포함된 클래스들의 정보가 위치한 영역이며, 결국 이 데이터를 파싱해서 클래스에 해당하는 메서드, 필드 등의 구조를 파악할 수 있게 된다. 데이터를 추적하는 것은 offset 정리한 내용을 보면 된다.

map_list #

dex 파일에서 각 섹션들이 어디에 얼마나 존재하는지 알려주는 목차 역할을 한다. 시작은 map_item의 개수를 4byte크기로 저장하며 map_item 은 12byte 이고 type, size, offset 순서로 저장된다.

데이터 영역은 헤더에 오프셋이 없기 때문에 빠르게 접근할때 사용하는 영역이다.

 1struct map_list {
 2    uint32_t size;        // map_item의 개수
 3    map_item list[size];  // 섹션 정의 리스트
 4}
 5
 6struct map_item {
 7  uint16_t type_;
 8  uint16_t unused_;
 9  uint32_t size_;
10  uint32_t offset_;
11};

각 영역의 타입 hex 값

 1public class MapItem implements RawDexObject {
 2  public static final int TYPE_HEADER_ITEM = 0x0;
 3  public static final int TYPE_STRING_ID_ITEM = 0x1;
 4  public static final int TYPE_TYPE_ID_ITEM = 0x2;
 5  public static final int TYPE_PROTO_ID_ITEM = 0x3;
 6  public static final int TYPE_FIELD_ID_ITEM = 0x4;
 7  public static final int TYPE_METHOD_ID_ITEM = 0x5;
 8  public static final int TYPE_CLASS_DEF_ITEM = 0x6;
 9  public static final int TYPE_MAP_LIST = 0x1000;
10  public static final int TYPE_TYPE_LIST = 0x1001;
11  public static final int TYPE_ANNOTATION_SET_REF_LIST = 0x1002;
12  public static final int TYPE_ANNOTATION_SET_ITEM = 0x1003;
13  public static final int TYPE_CLASS_DATA_ITEM = 0x2000;
14  public static final int TYPE_CODE_ITEM = 0x2001;
15  public static final int TYPE_STRING_DATA_ITEM = 0x2002;
16  public static final int TYPE_DEBUG_INFO_ITEM = 0x2003;
17  public static final int TYPE_ANNOTATION_ITEM = 0x2004;
18  public static final int TYPE_ENCODED_ARRAY_ITEM = 0x2005;
19  public static final int TYPE_ANNOTATIONS_DIRECTORY_ITEM = 0x2006;
20}

버전별 차이점 #

  • 035
    안드로이드 7.0 이전에 주로 사용되던 dex 포맷이다.

  • 036
    Dalvik 버그로 인해 공식적으로 건너 뛴 버전이다.
    Android 8.1.0의 dex_file.cc 코드 4428c584-38c6-48c8-b933-9204022b8f7f

  • 037
    안드로이드 7.0 에서 지원하며 035와의 유일한 차이점은 Java 8 에 도입된 디폴트 메서드 추가와 그것을 호출하기 위한 invoke의 문법 조정이다.
    Java 8 에서는 인터페이스에도 default 메서드를 만들 수 있게 추가됐는데, 이미 만들어진 인터페이스가 수정됐을때 그 인터페이스를 사용하는 모든 클래스에서 컴파일 에러가 발생하는 것을 방지하기 위해 추가됐다고 한다.
    헤더에서는 변한게 없지만, 데이터영역에서 타입 코드가 추가되거나

  • 038
    안드로이드 8.0 에서 invoke-polymorphic, invoke-custom 문법 추가

  • 039
    안드로이드 9.0 에서 const-method-handle, const-method-type 문법 추가

  • 040 안드로이드 10.0 부터 40으로 넘어갔다.

 1public static int mapApiToDexVersion(int api) {
 2    if (api <= 23) {  // Android M/6
 3        return 35;
 4    }
 5    switch (api) {
 6        case 24:  // Android N/7
 7        case 25:  // Android N/7.1
 8            return 37;
 9        case 26:  // Android O/8
10        case 27:  // Android O/8.1
11            return 38;
12        case 28:  // Android P/9
13            return 39;
14        case 29:  // Android Q/10
15        case 30:  // Android R/11
16        case 31:  // Android S/12
17        case 32:  // Android S/12.1
18        case 33:  // Android T/13
19        case 34:  // Android U/14
20            return 40;
21        case 35:  // Android V/15
22            return 41;
23    }
24    return NO_VERSION;
25}
comments powered by Disqus