1. Android进程虚拟内存空间概览
Android基于Linux内核,每个应用进程都有独立的虚拟地址空间:
64位Android进程虚拟内存布局(简化版):
0x0000000000000000 - 0x0000555555554000 [用户空间下限]
├── 0x0000000000000000 - 0x0000000000001000 [NULL页,不可访问]
└── 0x0000000000001000 - 0x0000555555554000 [用户代码段起始区域]
0x0000555555554000 - 0x00007fffffffffff [主要用户空间]
├── 代码段 (.text)
├── 数据段 (.data, .bss)
├── 堆 (Heap)
├── 内存映射区 (mmap)
├── 共享库区域
└── 栈 (Stack)
0x8000000000000000 - 0xffffffffffffffff [内核空间]
2. APK加载过程中的内存布局
2.1 应用启动时的内存分配
典型Android应用内存布局:
[低地址]
0x0000000000400000 - app_process64 (Zygote fork出来的进程)
0x0000000012c00000 - linker64 (动态链接器)
0x0000006f00000000 - [匿名内存区域]
0x0000007000000000 - [共享库加载区域开始]
├── 0x0000007000000000 - libc.so
├── 0x0000007001000000 - libm.so
├── 0x0000007002000000 - libdl.so
├── 0x0000007003000000 - libart.so (ART虚拟机)
├── 0x0000007010000000 - libandroid_runtime.so
└── 0x0000007020000000 - [应用特定的native库]
0x0000007100000000 - [ART堆区域]
├── Image Space (boot.art)
├── Zygote Space
├── Allocation Space (应用对象)
└── Large Object Space
0x0000007fff000000 - [栈区域]
[高地址]
2.2 DEX文件在内存中的布局
DEX文件内存映射:
/data/app/com.example.app/base.apk
├── classes.dex → 映射到内存
│ ├── 0x7030000000 - DEX Header
│ ├── 0x7030001000 - String Pool
│ ├── 0x7030010000 - Type Definitions
│ ├── 0x7030020000 - Method Definitions
│ └── 0x7030100000 - Code Items
├── classes2.dex → 单独映射
└── lib/arm64-v8a/
├── libnative1.so → 0x7040000000
└── libnative2.so → 0x7041000000
3. SO库的详细内存布局
3.1 ELF文件段映射
单个SO文件在内存中的布局:
libnative.so (基址: 0x7040000000)
├── ELF Header [0x7040000000 - 0x7040000040]
├── Program Headers [0x7040000040 - 0x7040000200]
├── .text (代码段) [0x7040001000 - 0x7040050000] (可执行)
├── .rodata (只读数据) [0x7040050000 - 0x7040060000] (只读)
├── .data (已初始化数据) [0x7040061000 - 0x7040062000] (读写)
├── .bss (未初始化数据) [0x7040062000 - 0x7040063000] (读写)
├── .got (全局偏移表) [0x7040063000 - 0x7040064000] (读写)
├── .plt (过程链接表) [0x7040064000 - 0x7040065000] (可执行)
└── .dynamic (动态信息) [0x7040065000 - 0x7040066000] (只读)
3.2 内存权限设置
# 通过 /proc/[pid]/maps 可以看到实际布局
cat /proc/12345/maps
7040000000-7040001000 r--p 00000000 fd:00 12345 /data/app/.../lib/arm64/libnative.so
7040001000-7040050000 r-xp 00001000 fd:00 12345 /data/app/.../lib/arm64/libnative.so
7040050000-7040060000 r--p 00050000 fd:00 12345 /data/app/.../lib/arm64/libnative.so
7040061000-7040063000 rw-p 00060000 fd:00 12345 /data/app/.../lib/arm64/libnative.so
7040063000-7040064000 rw-p 00000000 00:00 0 [anon:.bss]
4. ART虚拟机内存布局
4.1 Java堆结构
ART堆内存布局:
[Image Space] - 0x7100000000
├── boot.art (系统类)
└── boot-framework.art
[Zygote Space] - 0x7110000000
├── 预加载的系统类
└── 字符串常量池
[Allocation Space] - 0x7120000000
├── Young Generation (新生代)
│ ├── Eden Space
│ └── Survivor Space
└── Old Generation (老年代)
[Large Object Space] - 0x7200000000
└── 大对象存储区域
[Non-Moving Space] - 0x7300000000
└── 不可移动对象
4.2 方法编译和缓存
编译后的代码布局:
[OAT文件映射] - 0x7400000000
├── OAT Header
├── DEX文件数据
├── 编译后的机器码
│ ├── Method 1 机器码
│ ├── Method 2 机器码
│ └── ...
└── GC Maps
[JIT编译缓存] - 0x7500000000
├── 热点方法机器码
└── 优化后的代码
5. 内存保护和隔离机制
5.1 ASLR (地址空间布局随机化)
每次启动时基址都会变化:
启动1: libnative.so → 0x7040000000
启动2: libnative.so → 0x7f8a000000
启动3: libnative.so → 0x7c15000000
5.2 内存权限控制
权限标志说明:
r-- : 只读
r-x : 只读+可执行 (代码段)
rw- : 读写 (数据段)
--- : 不可访问 (保护页)
6. 动态加载和链接
6.1 延迟绑定机制
函数调用过程:
应用调用 native_function()
↓
PLT表中的跳转指令
↓
GOT表查找实际地址
↓
如果未解析,调用动态链接器
↓
链接器解析符号并更新GOT
↓
跳转到实际函数地址
6.2 共享库依赖关系
依赖链示例:
app_process64
├── linker64
├── libc.so
├── libm.so
├── libart.so
│ ├── libsigchain.so
│ └── libbacktrace.so
└── 应用SO库
├── libssl.so
├── libcrypto.so
└── 其他依赖库
7. 实际内存布局查看方法
你可以通过以下方式查看实际的内存布局:
# 查看进程内存映射
adb shell cat /proc/$(pidof com.shizhuang.duapp)/maps
# 查看进程状态
adb shell cat /proc/$(pidof com.shizhuang.duapp)/status
# 使用Frida查看模块信息
Process.enumerateModules().forEach(function(module) {
console.log(module.name + ": " + module.base + " - " + module.size);
});
这种内存布局设计确保了:
安全性:通过权限控制和ASLR防止攻击
效率性:共享库减少内存占用
隔离性:每个进程有独立的虚拟地址空间
灵活性:支持动态加载和卸载
这就是为什么你的dump工具能够通过Frida准确定位和提取SO文件的原理基础