malloc / IOSurface / purgeable 三线闭环 —— 结论、方法论与 Instruments 对比
同一条方法论(公开符号 Interceptor hook + 分层采集 + 协议工程 + 内核源码对齐),三条互相印证的线,把"业务申请了多少内存、申请了什么、何时声明可丢弃"全部落到带调用栈的事件级证据上。
malloc(怎么申请·高频小额·churn 主导)→ IOSurface(申请什么·低频大额·持久)→ purgeable(何时声明可丢/取回·状态机迁移)
三线数据合成一张表:IOSurface = 图形/媒体面,malloc 毛申请 = 分配压力,in_use Δ / purgeable = 净持有与可丢纪律。
| 业务 | IOSurface 申请 | malloc 毛申请 / 窗口 | 净变化 | 主导路径(实抓栈) |
|---|---|---|---|---|
| 相机管线 mediaserverd |
531 MB | 865.3 MB / 50s 338 万次调用 |
in_use +66.1MB | 全系统最大瞬时内存事件:CVPixelBufferPool 515MB + FigPhoto JPEG 70MB + ANE 14MB ‖ H13ISP 逐帧元数据 176.8MB ← JasperDepth 深度引擎 112.7MB ← 点云 38.8MB |
| 快手 | 34.4 MB | 460.9 MB / 60s 690 万次调用 |
in_use +1.4MB 存活 28.4MB |
Metal IOGPUResourceListInit 命令缓冲池 19MB · NSKeyedArchiver 序列化 11MB · JSON 8MB · anon 热补丁代码 36MB |
| 美团 | 7.4 MB | 122.9 MB / 60s 58.6 万次 |
in_use +4.8MB | JSON 解析驱动:NSJSONReader 22.3MB · dispatch_data 网络 8.1MB · gzip 压缩 6.8MB |
| bilibili | 56.3 MB | 39.8 MB / 60s 15.9 万次 |
in_use +10.4MB | 自研 AVIF 解码器 13.7MB(BFCImageAVIFCoder)· CG 图像 2.9MB · boringssl TLS 2.2MB |
| 京东 | ≈0 | 3.7 MB / 60s 4.4 万次 |
in_use −0.8MB | Cronet 双向流 · 小额为主,最安静的业务 |
| 淘宝 | 118.6 MB | 不可测(慢启动) | — | IOSurface 面 = MediaToolbox FigPhoto 解码池;malloc 面冷启动 >45s 仍在闪屏 |
| 微信 | 13 MB | 不可测(反调试) | — | frida script 加载层挂死 rpc;低频 purgeable hook 也无解 |
| 网易云 / 小红书 | 27.4 MB | 不可测(永续分配流) | — | 内容流 prefetch 永不停歇:+13/25/45s 任何时点挂钩必触发看门狗 |
| 抖音 | 42 MB 稳态 | 不可测(反调试) | — | 稳态 0 新建 + 42MB 持有已知 |
| SpringBoard | ~92 MB / 次切换 | 2.0 MB / 40s 空闲 28.6k 次 |
±0(空闲) | CARenderServerCreateSnapshots 快照 + ImageIO;基线 ~715 次调用/秒 |
set_volatile(73 次),8.9MB nonvolatile purgeable 清到 0.1MB —— 第一章"后台 tag88 清零"在状态机层得到解释。hook vm_purgable_control 的 SET 调用并 onLeave 回读旧状态(内核写回),得到完整迁移矩阵:
| 业务 | 0→1 置 volatile | 1→0 取回 | 0→2 直接置空 | 2→0 取回发现被偷 | 直接调用者 |
|---|---|---|---|---|---|
| 快手 稳态 30s | 76 (4.75MB) | 75 (4.69MB) | 4 | 1 | sqlite3_db_release_memory ×295(页缓存 0↔1 高频往返)+ libnetwork nw_segment(0→2) |
| bilibili 前后台 | 45 | 8 | 1 | 0 | CA::Render::Shmem::set_volatile ×38 + sqlite ×15 |
| SpringBoard 切换 | 17 | 10 | 0 | 4 | sqlite ×24 + CA shmem ×7 —— 快照内容被内核偷过再取回的实锤 |
| mediaserverd 相机 | 0 | 0 | 0 | 0 | SET 全零:捕获管线纪律走 madvise(MADV_FREE_REUSABLE) ×1487/40s |
| 入口 | 驱动状态机? | 实证 + 源码实锤 [SRC] |
|---|---|---|
mach_vm_purgable_control SET | ✅ 唯一用户态驱动者 | SET VOLATILE → pnv/pv/footprint 字节级精确 2MB 移动 ‖ vm_object.c:5846-6166;**SET 回写旧状态 = :6162 *state = old_state**(onLeave 回读迁移矩阵的机制依据) |
mach_vm_allocate/map(VM_FLAGS_PURGABLE=0x2) | ✅ 创建期赋 NONVOLATILE | 创建后 GET_STATE=0;bilibili 25s 内 6 次 CA::Render::Shmem::new_shmem 创建层背板 ‖ vm_map.c:2837-2839 |
madvise(MADV_FREE_REUSABLE/REUSE) | ❌ 与 purgeable 完全正交 | purgeable state 纹丝不动;驱动独立 reusable 页级机制(reusable ledger 精确 +2MB)‖ kern_mman.c:1158-1163→VM_BEHAVIOR_REUSABLE→vm_map.c:15429→:15937-15943 vm_object_deactivate_pages(reusable_pages=TRUE)→:15964 vm_page_stats_reusable 计数——整条链零 VM_PURGABLE 引用 |
madvise(MADV_DONTNEED/FREE) | ❌ | state 不动 ‖ kern_mman.c:1149-1153→vm_map.c:15423-15427(VM_SYNC 页级操作) |
CABackingStoreSetVolatile | ✅ 内部走陷阱 | bilibili 38 次带 QuartzCore 栈的 SET |
IOSurfaceSetPurgeable | ⚠️ IOKit 内核侧控制 | 陷阱 hook 看不到内核调用;业务实测 0 次直接使用 ‖ vm_object.c:5883-5886:purgeable_only_by_kernel 对象拒用户态 SET → IOKit 走 SET_STATE_FROM_KERNEL(3) |
| SET 目标 EMPTY(0→2) | ✅ 立即清页 | libnetwork nw_segment ×4 实抓 ‖ vm_object.c:6107-6159 vm_object_purge(object,0)——用户态主动丢弃内容的通道 |
| EMPTY 不可逆 | 状态机约束 | vm_object.c:5901-5905 "what's been emptied must stay empty" |
| 内核偷取(VOLATILE→EMPTY) | ⚠️ 用户态 hook 天然不可见 | 2→0 ×4 间接捕获(回读旧态)‖ vm_pageout.c:1850-1866:压力分级 Warning/Urgent/Critical → vm_purgeable_object_purge_one(force_purge) |
task_vm_info ledger(phys_footprint / purgeable / media-tag 四联表)扫全部 pid,先知道"谁重"再决定去哪采。tag 88 IOSurface 记账即在此步发现。malloc_get_all_zones iOS 未导出,公开符号无双重计数);IOSurface 收敛点 IOSurfaceCreate(全部用户态路径汇聚);purgeable 7 入口(vm/mach_vm_purgable_control、madvise 过滤 advice≥5、IOSurfaceSetPurgeable、CABackingStoreSetVolatile)。设备侧先 enumerateExports 侦察存在性(malloc_create_purgeable_zone 即在侦察中确认 MISSING)。uiopen 拉起 → +13s attach → 40-60s 窗口(过早=风暴杀 App,过晚=静态零事件);死亡分类成四型(启动风暴 / 永续分配流 / 慢启动 / 反调试);流式 send() + 周期 dump 双保险(mediaserverd 两次窗口内死亡,数据保住)。malloc_zone_statistics 前后快照对账带栈覆盖率(实测 34-61%);task_vm_info ledger 前后 Δ 对账;purgeable SET 的 onLeave 回读旧状态(内核语义,源码实证)。mach_vm_region_recurse depth 参数真义(下降上限,传 64;depth++ 会自引用死循环)、ledger 字段偏移。IOSurfaceCreate 拿到全部用户态收敛点。| 维度 | Instruments (Allocations/VM Tracker) | 本套方法(越狱 + frida 抓栈) |
|---|---|---|
| malloc 用户态栈 | ✓(需 Xcode/DDI/可调试进程) | ✓ 原版 App/daemon 通用 |
| IOSurface/IOKit 申请栈 | ✗(内核分配,不可见) | ✓ IOSurfaceCreate 收敛点 |
| purgeable 状态迁移 | 仅持有量快照 | ✓ 迁移矩阵 + 调用者栈 |
| 系统 daemon | ✗ | ✓ |
| 启动风暴 | ✓ | 看门狗边界(CModule 可解) |
| 反调试 App | 基本不可用 | 部分可用(IOSurface 面) |
| 口径一致性 | 每次配置不同 | 同一 hook 面横向可比 |
frida_malloc_trace.py / frida_iosurface_trace.py / frida_purgeable_trace.py(9 入口:purge_control×2 + madvise 过滤 + IOSurface/CA 4 + 创建期 mach_vm_allocate/map ×2 + ledger 含 reusable 独立记账)purg_probe.py(进程内建 purgeable 映射逐操作对账,入口真值表的来源)malloc_batch.py / analyze_mtrace.py / census_inproc.pyREPORT.md(三章:IOSurface / malloc / purgeable)raw-data/iosurface-trace-2026-10-27/(18 文件)· raw-data/malloc-trace-2026-10-27/(14 文件)· raw-data/purgeable-trace-2026-10-27/(5 场景)