移动开发如何用好热更新?说说其中的技术原理

热更新是一种App软件开发者常用的更新方式。简单来说,就是在用户下载安装App之后,打开App时遇到的即时更新。

在2017年苹果App Store针对热更新的下架事件以后,开发者们也在不断的探索及尝试最优技术解决方案。随着技术的迭代,各种框架的发展更新,热更新的框架已经日趋成熟,各大互联网公司基本都有研发热更新框架,方案实现及优缺点各有差异,但总的来说有三大类。

ClassLoader 加载方案: 代表有Qzone的超级补丁、大众点评的nuwa、百度金融的rocooFix,、以及美团的robust。

Native层替换方案: 代表有阿里的Dexposed、AndFix与腾讯的内部方案KKFix;

H5/小程序动态加载: 代表有HTML5方案、小程序容器热更新方案。
在这里插入图片描述

接下来介绍几个极具代表性的方案,以及它们的优缺点:

1、QZone的热更新方案

QZone方案推出比较早,对热修复技术的推进很有启发意义。它是基于Android dex分包方案,最关键的技术点在于利用字节码插桩的方式绕开了预校验问题。这种方案只支持App重启之后才能修复,也就是App在运行的时候加载到了补丁包也不能及时修复,需要App重新启动的时候才会修复,这是因为QZone方案是基于类加载区需要重新加载补丁类才能实现的,所以必须进行重启才能修复。此外,QZone方案只支持到类结构本身代码层面的修复,不支持资源的修复。

原理

class加载原理:dex文件转换成dexFile对象,存入Element[]数组,findclass顺序遍历Element数组获取DexFile,然后执行DexFile的findclass。
Hook了ClassLoader.pathList.dexElements[],将补丁的dex插入到数组的最前端,所以会优先查找到修复的类,从而达到修复的效果。

优点

代码是非侵入式的,对apk体积影响不大。

缺点

需要下次启动才修复。

性能损耗大,为了避免类被加上CLASS_ISPREVERIFIED,使用插桩,单独放一个帮助类在独立的dex中让其他类调用。

2、微信Tinker

据微信内部人士介绍:微信tinker项目之初最大难点在于如何突破Qzone方案的性能问题,通过研究Instant Run的冷插拔与buck的exopackage找到灵感。它们的思想都是全量替换新的Dex

因为使用全新的dex,所以自然绕开了Art地址可能错乱的问题,在Dalvik模式下也不需要插桩,加载全新的合成dex即可。

原理

采用dex替换的方式,避免了dex插桩带来的性能损耗。原理是提供dex差量包,整体替换dex的方案。差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并成一个完整的dex,完整dex加载得到dexFile对象作为参数构建一个Element对象然后整体替换掉旧的dex-Elements数组。

Tinker自研了DexDiff/DexMerge算法,对于dex文件的处理经验老道。Tinker还支持资源和So包的更新,So补丁包使用BsDiff来生成,资源补丁包直接使用文件md5对比来生成,针对资源比较大的(默认大于100KB属于大文件)会使用BsDiff来对文件生成差量补丁。

优点

兼容性高、补丁小。
开发透明,代码非侵入式。
支持so文件、资源文件、类的增加和删除。

缺点

需要下次启动才修复。

3、阿里AndFix

AndFix采用native hook的方式,这套方案直接使用dalvik_replaceMethod替换class中方法的实现。由于它并没有整体替换class, 而field在class中的相对地址在class加载时已确定,所以AndFix无法支持新增或者删除filed的情况(通过替换init与clinit只可以修改field的数值)。

原理

直接在native层进行方法的结构体信息对换,从而实现方法的新旧替换。

优点

补丁实时生效,不需要重新启动。

缺点

存在稳定及兼容性问题。ArtMethod的结构基本参考Google开源的代码,各大厂商的ROM都可能有所改动,可能导致结构不一致,修复失败。
无法增加变量及类,只能修复方法级别的Bug,无法做到新功能的发布。

4、FinClip 小程序容器热更新方案

「Native+H5」的App,其热更新的机制大致是:把需要频繁发版的业务应用H5化,并内嵌至 App 中。当含有页面链接的App版本过审以后,这些H5 页面可以随时远程热更新,用户在不更新App版本的基础上,就能使用最新版的业务应用。

虽说 h5 与小程序均能帮助 Hybrid 应用实现热更新,但鉴于小程序优于h5的性能,这里仅为大家介绍基于小程序容器的热更新方案。

FinClip 是近几年大热的小程序容器技术,App 通过连接后台,从后台拉取小程序包,通过小程序容器运行,可以帮助「Native+小程序」混合开发应用实现热更新。

原理:

实现了小程序的容器,由逻辑层负责与 SDK 交互,渲染层负责页面的渲染,同时由 SDK 提供路由界面跳转以及其他原生功能
SDK 通过运行时检查小程序的更新,动态进行小程序包的下载,实现功能的热更新

优点:

非侵入式的代码,对apk体积影响不大。
可扩展性高,由小程序接管业务逻辑,可以扩展任意功能

缺点:

只对app中的小程序页面有效,对原生模块无能为力。

<think>嗯,用户之前让我提供通过PEB结构获取kernel32.dll基地址的代码,现在他问的是“遍历链表详细和说说”。看来他对代码中的链表遍历部分不太明白,需要更详细的解释。首先,我需要回忆一下PEB结构中的模块链表是如何组织的。 在之前的代码中,我们通过PEB_LDR_DATA访问了InMemoryOrderModuleList链表,这个链表里存放的是LDR_DATA_TABLE_ENTRY结构。用户可能对链表的结构、遍历方式以及如何通过CONTAINING_RECORD宏找到入口点有疑问。 用户可能不太清楚为什么链表遍历要用到Flink和Blink,或者为什么循环条件是pListEntry != pListHead。需要解释LIST_ENTRY的结构,以及Windows内核中双向链表的常见实现方式。 另外,CONTAINING_RECORD宏的作用可能也需要详细说明,特别是结构体成员偏移的计算方法,用户可能对如何从链表节点获取整个结构体的指针感到困惑。 还需要解释代码中的循环步骤,比如如何移动到下一个节点,如何检查模块名称,以及为什么比较字符串时要使用Unicode字符串的长度和逐个字符比较。用户可能对安全比较函数的设计和必要性感兴趣,特别是为什么需要不区分大小写。 另外,用户可能想知道这种遍历方法在x86和x64架构下是否有差异,是否需要特别注意指针的大小或者对齐问题。虽然代码中使用了条件编译,但可能需要进一步解释不同架构下的内存布局是否影响链表遍历。 还有可能用户对链表遍历的安全性有疑问,比如如果链表损坏或者被钩子(hook)了怎么办,这时候是否需要异常处理或者其他防御措施。之前的注意事项里提到了异常处理,可能需要展开说明。 可能还需要对比其他获取模块基址的方法,比如使用GetModuleHandle,但这里的方法有什么优势,比如不需要调用API,适合shellcode等场景。用户可能想了解为什么选择这种方法而不是更简单的API调用。 最后,用户的应用场景可能涉及安全研究或逆向工程,所以需要强调这种方法的实际应用和潜在问题,比如结构体偏移变化导致的问题,如何动态计算偏移等。这部分在之前的回答中有提到,但可能需要更详细的例子或说明。 总结来说,用户需要链表遍历的详细步骤、结构体成员的作用、宏的工作原理、安全性考虑以及实际应用中的注意事项。需要分步骤解释,用简单明了的语言,避免过多术语,同时结合代码示例中的具体实现来说明每个部分的作用。</think>好的,我将详细解释Windows PEB中模块链表的遍历原理和实现细节,并用中文分步骤说明: --- ### **一、链表结构基础** 在PEB结构中,模块信息通过**双向循环链表**存储,关键结构如下: #### 1. `LIST_ENTRY` 结构(链表节点) ```c typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; // 前向指针(Forward Link) struct _LIST_ENTRY *Blink; // 后向指针(Backward Link) } LIST_ENTRY, *PLIST_ENTRY; ``` - **作用**:构成双向链表的连接节点 - **特点**:每个链表节点都嵌入在更大的结构体中(如`LDR_DATA_TABLE_ENTRY`) #### 2. `LDR_DATA_TABLE_ENTRY` 结构 ```c struct _LDR_DATA_TABLE_ENTRY { // ...其他字段 LIST_ENTRY InMemoryOrderLinks; // 链表节点位置 ← 关键字段 PVOID DllBase; // 模块基地址 UNICODE_STRING BaseDllName; // 模块名称(如"kernel32.dll") // ...其他字段 }; ``` - **关键点**:链表节点`InMemoryOrderLinks`是结构体的成员,通过它定位整个结构体 --- ### **二、链表遍历步骤详解** #### 步骤1:获取链表头 ```c PLIST_ENTRY pListHead = &pLdr->InMemoryOrderModuleList; ``` - `InMemoryOrderModuleList`是链表头 - **链表特性**:链表头本身是一个`LIST_ENTRY`结构,但`Flink`指向第一个实际节点 #### 步骤2:初始化遍历指针 ```c PLIST_ENTRY pListEntry = pListHead->Flink; // 第一个有效节点 ``` #### 步骤3:循环遍历链表 ```c while (pListEntry != pListHead) { // 循环终止条件 // ① 通过链表节点获取包含它的完整结构体 PLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD( pListEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks ); // ② 处理当前模块(例如检查名称) if (CompareUnicodeString(&pEntry->BaseDllName, L"kernel32.dll")) { return pEntry->DllBase; } // ③ 移动到下一个节点 pListEntry = pListEntry->Flink; } ``` --- ### **三、关键技术解析** #### 1. `CONTAINING_RECORD`宏 - **作用**:通过结构体成员的地址反推结构体起始地址 - **计算公式**: ```c #define CONTAINING_RECORD(ptr, type, member) \ ((type*)((ULONG_PTR)(ptr) - (ULONG_PTR)&((type*)0)->member)) ``` - **示例**: ```c // 假设: // - pListEntry 是某LDR_DATA_TABLE_ENTRY中InMemoryOrderLinks的地址 // - InMemoryOrderLinks在结构体中的偏移量为0x10 // 则结构体基地址 = pListEntry - 0x10 ``` #### 2. 双向链表遍历逻辑 - **循环终止条件**:`pListEntry != pListHead` - 因为这是**循环链表**,当指针回到链表头时结束遍历 - **遍历方向**: - `Flink`向前遍历(加载顺序) - `Blink`向后遍历(可用于反向验证) --- ### **四、内存布局示意图** ``` PEB_LDR_DATA ┌───────────────────────┐ │ InMemoryOrderModuleList │ ← 链表头(LIST_ENTRY) │ (Flink)─────────────┐│ └───────────────────────┘│ │ ▼ LDR_DATA_TABLE_ENTRY 1 │ ┌───────────────────────┬┘ │ InMemoryOrderLinks │ │ Flink ──────────────┐ │ Blink │ ├───────────────────────┤ │ DllBase │ ← 目标值1 │ BaseDllName │ └───────────────────────┘ │ ▼ LDR_DATA_TABLE_ENTRY 2 ┌───────────────────────┐ │ InMemoryOrderLinks │ │ Flink ──────────────┐ │ Blink │ ├───────────────────────┤ │ DllBase │ ← 目标值2 │ BaseDllName │ └───────────────────────┘ ``` --- ### **五、防御性编程要点** #### 1. 有效性检查 ```c if (!pLdr) return NULL; // 检查PEB_LDR_DATA是否有效 if (!pEntry->DllBase) continue; // 可选:检查基地址有效性 ``` #### 2. 防崩溃措施 ```c __try { // 访问可能无效的内存区域 if (CompareUnicodeString(...)) {...} } __except(EXCEPTION_EXECUTE_HANDLER) { continue; // 跳过异常节点 } ``` #### 3. 反检测技巧 - **替代字符串比对**:使用预计算哈希值 ```c DWORD dwHash = CalcDllHash(pEntry->BaseDllName.Buffer); if (dwHash == KERNEL32_HASH) {...} ``` --- ### **六、实际调试技巧** 1. **查看链表内容**(Windbg): ``` dt _PEB_LDR_DATA @$peb->Ldr !list -x "dt _LDR_DATA_TABLE_ENTRY" @$peb->Ldr->InMemoryOrderModuleList ``` 2. **偏移验证**: ```c // 检查结构体偏移是否匹配 assert(offsetof(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks) == 0x10); ``` --- 通过这种链表遍历方式,我们可以不依赖任何API直接获取系统模块信息,这在底层开发和安全研究中非常有用。理解这个机制是掌握Windows内存结构的重要一步。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值