同样的global,不同的audioLibPath——记一次诡异的内存错位

0 阅读3分钟

最近在做 keyBonk 这个项目时,遇到了一个卡了我将近六小时的 bug。回头想想还挺有意思的,于是打算写篇博客记录一下。

这是一个 C++ 的 Win32 项目,因为我写它的时候刚学 C++,参考 Win32 文档的风格较多,所以名义上是 C++,实际上更接近 C 风格。最近我打算引入一些 C++ 特性来重写部分模块,比如用 struct 统一管理资源:

namespace keybonk
{
    struct resource_manager
    {
    public:
        ULONG_PTR gdiplusToken = 0;
        bool comInitialized = false;
        wchar_t *fullIniFilePath = nullptr;
        wchar_t *fullDebugFilePath = nullptr;
        HWND hwnd = NULL;
        HWND hwndAbout = NULL;
        HWND hwndSetting = NULL;
        bool Mute = false;
        bool MuteMouse = false;
        bool WindowPenetrate = false;
        NOTIFYICONDATA nid = {};
        wchar_t audioLibPath[MAX_PATH];
        bool minimum = false;
        HINSTANCE hInstance;
        HHOOK KeyboardHook = nullptr;
        HHOOK MouseHook = nullptr;
        HBITMAP hBmp = nullptr;
        HDC hdcScreen = nullptr;
        HDC memDC = nullptr;
        HBITMAP hOldBmp = nullptr;
        int nCmdShow;
        HRESULT hrMain;
        std::optional<keybonk::background> bg_opt;

        ~resource_manager();
    };
    inline resource_manager global;
}

这个结构体集中管理了大量全局资源,并负责在析构时自动释放,从而解决中途抛异常或返回错误时资源泄漏的问题。global 是它的全局实例,为了在多个 .cpp 文件中避免重复定义,我使用了 inline 关键字来保证单例。

代码能跑起来,但以前正常的音频播放突然没声了。查看日志,我定位到 audioPlay.cpp 里的这段代码:

using keybonk::global;
debug::logOutPut("音频库路径:", global.audioLibPath);

输出却是:

音频库路径:

而把同样的代码放到 main.cpp 中,输出就正常了:

音频库路径:./bin/default/

也就是说,在 main.cppglobal.audioLibPath 能正常访问,在 audioPlay.cpp 里却不行。为什么?

我排查了很久,各种逻辑看起来都对,实在没招了,于是打开了 Trea。

当前项目遇到一个问题:main.cpp 可以正常使用 global 对象的 audioLibPathaudioPlay.cpp 却不行,可能是什么原因?

Trae 初步分析后猜测是 inline 的编译器 bug,建议改成 extern 形式。但改完之后问题依旧,说明这不是主因。

接着 Trae 继续分析,我也开始大量查资料。说实话,这个问题我甚至找不到一个准确的描述,只能写一大段提示词反复喂给 DeepSeek、豆包、Kimi……经过多轮分析后,我确信不能太指望 AI 了。

不过 Trae 有一句话倒是点醒了我:看地址

于是我加了一段调试输出:

using keybonk::global;
debug::logOutPut("global:", &global, "audioLibPath:", &global.audioLibPath);

结果如下:

[程序启动] 2026-4-19 3:16:39
[main.cpp] keybonk::global 地址 = 0x7ff73bfa0040, audioLibPath 地址 = 0x7ff73bfa0498
[audioPlay.cpp] keybonk::global 地址 = 0x7ff73bfa0040, audioLibPath 地址 = 0x7ff73bfa02d8

global 地址相同,但 audioLibPath 的地址却不同——这很奇怪。我一时没想通,但 Trae 很快反应过来:问题出在 NOTIFYICONDAT 上!

注:当然它也不是那么聪明,我中途还让它查了一次预处理结果它才看出来,不过已经比我强多了

回顾资源管理类的成员排布(简化):

NOTIFYICONDATA nid = {};   // 任务栏通知区域图标状态
wchar_t audioLibPath[MAX_PATH];
bool minimum = false;

audioLibPath 紧跟在 nid 之后。而 nid的类型NOTIFYICONDATA实际上是个宏定义,它的大小取决于是否定义了 UNICODE 宏:

  • 定义了 UNICODE,它实际上会变为NOTIFYICONDATAW ,数据使用宽字符
  • 未定义则变为NOTIFYICONDATAA,使用窄字符

main.cpp 中在初期开发时就定义了 UNICODE,因此 nid 是宽字符版本,占用较大空间。

audioPlay.cpp 缺少 UNICODE,于是 nid 被当作 ANSI 版本,比宽版本小了 448 字节

由于两个 .cpp 文件对同一个结构体的内存布局理解不一致,导致 audioLibPathaudioPlay.cpp 中的偏移量计算错误,从而访问到了错误的内存地址。日志里看不到路径内容,自然也就无法播放音频了。

又是宏定义不一致惹的祸,说实话,这还挺简单的,但真的很难排查。前一阵子做 desktopDanmaku 做习惯了,默认自己在 Makefile 里写过 -DUNICODE,结果这次就栽在这个细节上了。