• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2025-06-30 16:35 Aet 隐藏边栏 |   抢沙发  4 
文章评分 1 次,平均分 5.0

概述

现象

  1. 有个DLL是给JavaScript调用的,发现它加载了这个库后,给Init函数传入了两个参数

  1. 实际上,该动态库导出的函数中,Init是别名,为InitDefrag

  1. 而项目中名为InitDefrag的函数只有一个,且只有一个参数

结论

  1. 是正确的用法
    1. 因为上述的InitDefrag是类的public成员函数
    2. 传入的第一个参数作为了隐式的 this指针 (DefragTest*)
    3. 这种调用方式是有效的标准技术

可能存在的问题

  1. 内存管理陷阱
    1. 既然这个接口需要传入一个隐式的this,那就需要使用者最后要手动对这个this进行一定的管理,比如可能需要使用者通过这个this调用类的一些Uninit或者Destroy的逻辑

  1. 二进制接口(ABI)稳定
改动类型 影响
添加虚函数 虚表偏移改变→崩溃
调整成员顺序 成员偏移改变→数据错位
修改调用约定 堆栈管理不一致
  1. 多线程安全

问题详解

添加虚函数 虚表偏移改变→崩溃

  1. 概述
    1. 添加虚函数会导致虚函数表(vtable)偏移量改变,这是C++二进制兼容性中最危险的问题之一
  2. 虚函数表结构

  1. 现在升级版本添加新虚函数:

  1. 奔溃场景模拟:
    1. EXE使用旧版本头文件编译,如1
    2. 实际加载新版本DLL对象,如2
    3. EXE调用 [eax+4] → 实际调用了 CheckCompatibility

其他操作也会导致上述类似崩溃

  1. 调整成员顺序
  2. 改变继承关系
  3. 修改调用约定
  4. 添加新成员
  5. RTTI改动

解决方案

概述

  1. 安全的跨模块C++类设计

接口继承法

PIMPL模式(零虚表依赖)

COM组件(工业级方案)

其他

C++this指针传递规则

平台 this指针传递方式
x86 (32位) 通过ECX寄存器传递(默认thiscall)
x64 (64位) 通过RCX寄存器传递
当使用WINAPI 强制改为栈传递(类似stdcall)

关于GetProcAddress

  1. 概述
    1. Windows动态库中,普通成员函数(非静态)是无法直接通过GetProcAddress按名称获取的,因为编译器会对成员函数名进行修饰,并且调用时隐含this指针,导致函数签名与普通C函数不同
  2. 为什么之前的方式能够工作呢?
    1. 案例中,获取的是包装函数地址,不是成员函数本身
    2. 为什么?
    3. 当在DEF文件中使用别名导出时:
      编译器实际生成了一个thunk(跳板)函数

关于跳板函数背后,编译器幕后工作机制

  1. 原始类布局 (版本1)

  1. DEF导出后产生的代码
    1. 直接调用

  1. GetProcAddress获得的地址

  1. 添加虚函数后发生了什么 (版本2)
    1. InitDefrag本身仍是普通成员函数
    2. 函数签名没变
    3. DEF文件映射关系没变

  1. 致命问题:编译器生成的新thunk函数
    1. this指针处理不同
    2. 间接调用

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2025-07-05
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享