摘要服务端不停机热更的两种 C++ 实现方案:函数指针 + DLL 动态加载、以及汇编层 HotPatch 字节替换。前者实现简单、但对代码结构有侵入;后者对业务透明、但依赖编译期参数配合与汇编改写。

服务端要做到不停机热更,最直接的思路是引入脚本语言,利用动态特性替换函数实现。C++ 项目里也可以借用这个思路:把函数调用改为函数指针调用,通过加载新的 DLL / SO 并替换指针指向,实现不停机切换逻辑。下文以 Windows 平台为例,介绍两种落地方案。

方案一 动态加载 DLL,替换函数指针

核心思路:exe 主映像通过运行时加载 DLL 获取新版本函数地址,原调用点改为经由函数指针转发,切换指针即切换实现。

实现流程

  1. exe 把要热更的逻辑抽成函数指针形式的调用点。
  2. 新版本逻辑编译为独立 DLL。
  3. 运行时 LoadLibrary 加载 DLL,GetProcAddress 取到新函数地址。
  4. 原子写入函数指针,后续调用自动走新逻辑。

示例代码

exe 主体:

DLL 新逻辑:

运行结果:

方案要点

要点说明
调用方式必须以函数指针调用,整个机制依赖指针转发。
内存关系DLL 与 exe 内存空间相互独立,类似不同线程的栈——不能直接访问,需通过地址指针互相引用。
数据共享通过函数参数传递上下文。示例以 ProcessMemoryContex 传递堆内存信息,也可额外维护一个全局指针避免重复参数传递。
脏数据风险每次加载 DLL,内部全局变量会重新初始化,多次热更后容易产生脏数据、混淆内存状态。
ABI 约束替换函数时必须保证堆栈平衡——参数与返回值字节数、调用约定都不能变。
编译器陷阱注意 inline 展开带来的替换失效,以及闭包捕获参数的内存生命周期问题。

适用场景

  • 无状态进程 几乎不持有长期内存数据,主数据通过 NoSQL 存储,运行时临时状态仅在闭包 / 协程上下文中维护。流程结束即释放,天然适合本方案。
  • 函数式 / 入口式工作流 典型如 RPC 调用:网络层把上下文通过参数传入,RPC 函数内部构成独立子环境,替换入口函数即相当于替换整段流程。
  • 不适合面向对象模型 OOP 中大量的内部函数调用与隐式 this 指针让数据迁移变得棘手。并非完全无解,但通常要引入反射等机制做大面积替换,成本偏高。

方案二 汇编层 HotPatch 替换

除了在 C++ 层经由函数指针跳转,还可以直接在汇编层改动——修改可执行映像在内存中的字节,实现"偷梁换柱"。

原理剖析

观察函数开头 2 字节:66 90(即 xchg ax, ax),这是编译器在开启 /hotpatch 选项后自动填入的占位指令。

既然是占位,就可以把这 2 字节替换为跳转指令。由于只有 2 字节,只能使用短跳 EB(跳转范围 ±128 字节)。所以还要在链接期于原函数上方预留足够空间,用来安放真正要跳到的自定义指令。

关键编译参数

参数作用
/hotpatch函数首部填入 2 字节 xchg ax, ax,为短跳预留占位。
/FUNCTIONPADMIN:5函数前填充 5 字节,用来安放自定义的长跳指令。

Win32 下这些地址受只读保护,需要先通过 VirtualProtect 开启写入权限才能改写。

实施步骤

  1. 编译期开启 /hotpatch/FUNCTIONPADMIN,生成可打补丁的映像。
  2. 运行期调用 VirtualProtect 解除目标内存的写保护。
  3. 在函数前填充区写入跳转到新函数的 5 字节长跳指令。
  4. 将函数首部 66 90 改写为 2 字节短跳,指向上一步填入的长跳指令。

执行效果:

延伸阅读:/hotpatch (Create Hotpatchable Image)/FUNCTIONPADMIN (Create Hotpatchable Image)

方案要点

要点说明
性能开销每次调用多出 2 次跳转,每个参与编译的函数额外多出 2 字节占位,存在轻微性能损耗。
侵入性C++ 代码零改动,业务层完全无感知。OOP 同样适用,this 指针按原规则传递。
原子性打补丁分两步:先在填充区写 5 字节长跳(不影响运行),再改函数首部 2 字节短跳。第二步必须原子,否则会有竞态。
体积由于函数前有填充字节,可执行文件与运行时内存占用略有增大。

两种方案对比

维度函数指针HotPatch
代码侵入高,需改调用方式低,业务代码零改动
OOP 适配
运行时开销一次指针解引用两次跳转 + 2 字节占位
原子性保障指针写入天然原子需要保证 2 字节写入原子
实现难度简单较复杂,依赖编译参数与汇编改写
脏数据风险高(全局变量重复初始化)

实施建议

  • 可以通过反射遍历热更函数地址列表批量更新;追求最小更新范围时,则手动维护替换清单。
  • GamePlay 层通常单线程运行,热更过程的线程安全有天然保障。
  • Linux 下实现类似机制需要编译器支持;不同平台的填充字节数与跳转指令需重新评估。