classes.dex
其中包含 APK 的可执行代码,是分析 Android 软件时最常见的目标
DEX 文件结构
在 Android 源码文件 dalvik/libdex/DexFile.h 中,有 DEX 文件可能用到的所有数据结构和常量定义
先了解 DEX 文件使用的数据类型:
自定义类型
原类型
含义
s1
int8_t
8 位有符号整型
u1
uint8_t
8 位无符号整型
s2
int16_t
16 位有符号整型,小端字节序
u2
uint16_t
16 位无符号整型,小端字节序
s4
int32_t
32 位有符号整型,小端字节序
u4
uint32_t
32 位无符号整型,小端字节序
s8
int64_t
64 位有符号整型,小端字节序
u8
uint64_t
64 位无符号整型,小端字节序
sleb128
无
有符号 LEB128,可变长度
uleb128
无
无符号 LEB128,可变长度
uleb128p1
无
无符号 LEB128 加 1,可变长度
u1 ~ u8 好理解,表示 1 ~ 8 字节的无符号数;s1 ~ s8 表示 1 ~ 8 字节的有符号数;sleb128、uleb128、uleb128p1 则是 DEX 文件中特有的 LEB128 数据类型
每个 LEB128 由 1 ~ 5 字节组成,所有的字节组合在一起表示一个 32 位的数据,如下图。每个字节只有 7 位为有效位,若第一个字节的最高位为 1,表示 LEB128 要使用第二个字节;若第二个字节的最高位为 1,表示 LEB128 要使用第三个字节;依此类推,直到最后一个字节的最高位为 0。当然,LEB128 最多使用 5 字节,若读取 5 字节后下一个字节的最高位仍为 1,则表示该 DEX 文件无效,Dalvik 虚拟机在验证 DEX 文件时会失败并返回
在 Android 系统源码 dalvik/libdex/Leb128.h 中可找到 LEB128 的实现。读取无符号 LEB128(uleb128)数据的代码:
DEX_INLINE int readUnsignedLeb128(const u1** pStream) {
const u1* ptr = *pStream;
int result = *(ptr++);
// 大于 0x7f,表示第一个字节最高位为 1
if (result > 0x7f) {
// 第二个字节
int cur = *(ptr++);
// 前两个字节的组合
result = (result & 0x7f) | ((cur & 0x7f) << 7);
// 大于 0x7f,表示第二个字节最高位仍为 1
if (cur > 0x7f) {
// 第三个字节
cur = *(ptr++);
// 前三个字节的组合
result |= (cur & 0x7f) << 14;
if (cur > 0x7f) {
// 第四个字节
cur = *(ptr++);
// 前四个字节的组合
result |= (cur & 0x7f) << 21;
if (cur > 0x7f) {
// 第五个字节
cur = *(ptr++);
// 前五个字节的组合
result |= cur << 28;
}
}
}
}
*pStream = ptr;
return result;
}
有符号的 LEB128(sleb128)与无符号的 LEB128 的计算方法大致相同,区别是无符号的 LEB128 的最后一个字节的最高有效位进行了符号扩展。读取有符号的 LEB128 数据的代码:
DEX_INLINE int readSignedLeb128(const u1** pStream) {
const u1* = *pStream;
int result = *(ptr++);
// 小于 0x7f,表示第一个字节的最高位不为 1
if (result <= 0x7f) {
// 对第一个字节的最高有效位进行符号扩展
result = (result << 25) >> 25;
}
else {
// 第二个字节
int cur = *(ptr++);
// 前两个字节的组合
result = (result & 0x7f) | ((cur & 0x7f) << 7);
if (cur <= 0x7f) {
// 对结果进行符号位扩展
result = (result << 18) >> 18;
}
else {
// 第三个字节
cur = *(ptr++);
// 前三个字节的组合
result |= (cur & 0x7f) << 14;
if (cur <= 0x7f) {
// 对结果进行符号位扩展
result = (result << 11) >> 11;
}
else {
// 第四个字节
cur = *(ptr++);
// 前四个字节的组合
result |= (cur & 0x7f) << 21;
if (cur <= 0x7f) {
// 对结果进行符号位扩展
result = (result << 4) >> 4;
}
else {
// 第五个字节
cur = *(ptr++);
// 前五个字节的组合
result |= cur << 28;
}
}
}
}
*pStream = ptr;
return result;
}
uleb128p1 类型很简单,其值为 uleb128 的值加 1
计算字符序列“c0 83 92 25”的 uleb128 值:
第一个字节 0xc0 大于 0x7f,表示要使用第二个字节,即 result1 = 0xc0 & 0x7f
第二个字节 0x83 大于 0x7f,要使用第三个字节,即 result2 = result1 + (0x83 & 0x7f) << 7
第三个字节 0x92 大于 0x7f,要使用第四个字节,即 result3 = result2 + (0x92 & 0x7f) << 14
第四个字节 0x25 小于 0x7f,表示到了结尾,即 result4 = result3 + (0x25 & 0x7f) << 21
结果为:0x40 + 0x180 + 0x1200000 + 0x4a00000000 = 0x4a012001c0
计算字符序列“d1 c2 b3 40”的 sleb128 值:
第一个字节 0xd1 大于 0x7f,要使用第二个字节,即 result1 = 0xd1 & 0x7f
第二个字节 0xc2 大于 0x7f,要使用第三个字节,即 result2 = result1 + (0xc2 & 0x7f) << 7
第三个字节 0xb3 大于 0x7f,要使用第四个字节,即 result3 = result2 + (0xb3 & 0x7f) << 14
第四个字节 0x40 小于 0x7f,表示到了结尾,即 result4 = ((result3 + (0x40 & 7f) << 21) << 4) >> 4
结果为:((0x51 + 0x2100 + 0x3300000 + 0x8000000000) << 4) >> 4 = 0x8003302151
DEX 文件由多个结构体组合而成。如下图,一个 DEX 文件由七部分组成:dex header 为 DEX 文件头,指定了 DEX 文件的一些属性并记录了其他数据结构在 DEX 文件中的物理偏移;string_ids 到class_def 部分可理解为“索引结构区”;真实的数据存放在 data 数据区;link_data 为静态链接数据区
dex header
string_ids
type_ids
proto_ids
field_ids
method_ids
class_def
data
link_data
DEX 文件由 DexFile 结构体表示,定义如下:
struct DexFile {
// directly-mapped "opt" header
const DexOptHeader* pOptHeader;
// pointers to directly-mapped structs and arrays in base DEX
const DexHeader* pHeader;
const DexStringId* pStringIds;
const DexTypeId* pTypeIds;
const DexFileId* pFileIds;
const DexMethodId* pMethodIds;
const DexProtoId* pProtoIds;
const DexClassDef* pClassDefs;
const DexLink* pLinkData;
// These are mapped out of the "auxillary" section,
// and may not be included in the file
const DexClassLookup* pClassLookup;
const void* pRegisterMapPool; // RegisterMapClassPool
// points to start of DEX file data
const u1* baseAddr;
// track memory overhead for auxillary structures
int overhead;
// additional app-specific data structures associated with the DEX
//void* auxData;
};
DexOptHeader 是 ODEX 的头。DexHeader 是 DEX 文件的头部信息,定义:
struct DexHeader {
u1 magic[8]; // DEX 版本标识
u4 checksum; // adler32 检验
u1 signature[kSHA1DigestLen]; // SHA-1 散列值
u4 fileSize; // 整个文件的大小
u4 headerSize; // DexHeader 结构的大小
u4 endianTag; // 字节序标记
u4 linkSize; // 链接段的大小
u4 linkOff; // 链接段的偏移量
u4 mapOff; // DexMapList 的文件偏移
u4 stringIdsSzie; // DexStringId 的个数
u4 stringIdsOff; // DexStringId 的文件偏移
u4 typeIdsSize; // DexTypeId 的个数
u4 typeIdsOff; // DexTypeId 的文件偏移
u4 protoIdsSize; // DexProtoId 的个数
u4 protoIdsOff; // DexProtoId 的文件偏移
u4 fieldIdsSize; // DexFieldId 的个数
u4 fieldIdsOff; // DexFieldId 的文件偏移
u4 methodIdsSize; // DexMethodId 的个数
u4 methodIdsOff; // DexMethodId 的文件偏移
u4 classDefsSize; // DexClassDef 的个数
u4 classDefsOff; // DexClassDef 的文件偏移
u4 dataSize; // 数据段的大小
u4 dataOff; // 数据段的文件偏移
};
magic 字段:表示这是一个有效的 DEX 文件,目前它的值固定为“64 65 78 0A 30 33 35 00”,转换为字符串格式为“dex.035.”
checksum 字段:DEX 文件的校验和,可通过它判断 DEX 文件是否已损坏或被篡改
signature 字段:用于识别未经 dexopt 优化的 DEX 文件
fileSize 字段:记录包括 DexHeader 在内的整个 DEX 文件的大小
headerSize 字段:记录 DexHeader 结构本身占用的字节数
endianTag 字段:指定 DEX 运行环境的 CPU 字节序,预设值 ENDIAN_CONSTANT 等于 0x12345678,表示默认采用小端字节序
linkSize、linkOff 字段:分别指定链接段的大小和文件偏移,大多数情况下它们的值为 0
mapOff 字段:指定 DexMapList 结构的文件偏移
剩余字段:分别表示 DexStringId、DexTypeId、DexProto