BianChengNan's Blog

Coding is hard, you can make it easy!


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

置顶声明

发表于 2029-03-01 | 更新于: 2026-02-16 | 分类于 原
字数统计: | 阅读时长 ≈ 分钟

实在抱歉,因为图片使用的是 http 链接,在 chrome 或者 edge 浏览器中打开本博客的时候,看不到文章中的图片。

可以在 chrome 中通过 chrome://flags (在 edge 中通过 edge://flags)启用 Insecure origins treated as secure,

并且把图床地址 http://resources.bianchengnan.tech 加入到信任列表的方式查看图片。(非常感谢群友 张帆 的提示)

整个操作如下图:

enable-show-image-in-chrome

如果还不能查看相关图片,请联系我,或者到我的公众号里查看。

我的个人微信号是 BianChengNan,公众号是 编程难。

调试实战 | 谁“锁”住了我的 SpaceSniffer?一次由 Shell 扩展引发的 Windows 死锁侦探之旅

发表于 2026-02-16 | 更新于: 2026-02-16 | 分类于 调试
字数统计: | 阅读时长 ≈ 分钟

摘要

在一次使用 SpaceSniffer 分析磁盘占用时,我遇到了一个非常容易复现的界面卡死问题。为查明原因,我首先通过 procdump 周期性保存完整转储文件,并使用 WinDbg 对调用栈进行对比分析,确认程序确实陷入阻塞而非假死。逐层追踪后,我最终将问题定位到第三方 Shell 扩展在 COM 激活过程中的异常阻塞,并由此引发死锁。

阅读全文 »

调试实战 | DllMain 的陷阱:当 Windows 10 并行加载遇上线程等待引发的死锁

发表于 2026-02-08 | 更新于: 2026-02-16 | 分类于 调试
字数统计: | 阅读时长 ≈ 分钟

摘要

这篇文章分析了在 Win10 系统上 DLL 卸载时发生死锁的根本原因。通过调试发现,死锁是由于 Win10 引入了并行加载器机制,导致线程在退出时等待全局事件 LdrpLoadCompleteEvent,而该事件又因 DllMain() 在处理 DLL_PROCESS_DETACH 时调用 WaitForSingleObject() 无限等待工作线程结束而无法被触发。

阅读全文 »

调试实战 | 从无法复现到真相大白:DLL卸载死锁背后的版本陷阱

发表于 2026-02-07 | 更新于: 2026-02-16 | 分类于 调试
字数统计: | 阅读时长 ≈ 分钟

摘要

这篇文章记录了我对 DLL 卸载时死锁问题的进一步探索。多年前,我曾在看雪论坛上发表过一篇关于 DLL 卸载死锁的分析文章,但最近有网友反馈无法复现这个问题。通过深入调试,我发现问题根源在于不同版本的 VS 运行时库对引用计数的处理方式不同。本文详细展示了如何使用 Windbg 追踪 DLL 引用计数的变化,解释了为什么在 VS2022 环境下原问题不再出现,最终定位到不同版本的 _beginthreadex 和 _endthreadex 的内部实现差异是关键所在。

阅读全文 »

调试技巧 | 从 Win7 到 Win10,DLL引用计数查看全攻略

发表于 2026-01-31 | 更新于: 2026-02-16 | 分类于 调试
字数统计: | 阅读时长 ≈ 分钟

前言

有两种常用的方式可以查看 dll 的引用计数。

  1. 在 windbg 中通过 !dlls 命令查看 dll 的引用计数。!dlls 输出结果中包含 LoadCount 字段。

    注意: 不同版本的 windbg 使用的 !exts.dlls 命令解析方式不同,低版本的 windbg 在解析 win10 上运行程序的 dll 引用计数,解析结果很可能是错的。

  2. 手动解析 PEB 中的 PEB_LDR_DATA 中的模块加载列表数据查看 dll 的引用计数。

    • Win7 及之前系统:
      找到 PEB 中的 PEB_LDR_DATA (Ldr 字段),遍历其 InLoadOrderModuleList 链表。链表项为 _LDR_DATA_TABLE_ENTRY 结构,其中的 LoadCount 字段即为引用计数。

    • Win8 及之后系统:
      结构发生变化,需在 _LDR_DATA_TABLE_ENTRY 中找到 DdagNode 字段(它是一个指向 _LDR_DDAG_NODE 的指针),该结构中的 LoadCount 字段为当前引用计数。

阅读全文 »

排错实战 | 使用 chkmatch 和 procmon 速解 MFC 调试符号加载失败

发表于 2025-12-14 | 更新于: 2026-02-16 | 分类于 排错
字数统计: | 阅读时长 ≈ 分钟

摘要

最近在调试的时候想查看 mfc140u.dll 中的函数,发现对应的调试符号没有加载,手动加载了几次都是失败。再三确认调试符号确实存在,而且设置的符号路径都对。难道又是调试符号不匹配?于是打算用 chkmatch 确认一下是否匹配。

最后,发现是调试符号放到了 system32 路径下,chkmatch 是 32 位程序,会自动到 sysWOW64 目录下找。本文简单记录了使用 procmon 快速排查定位,最后使用 chkmatch -c 使调试符号与 mfc140u.dll 强制匹配的过程。

阅读全文 »

如何关闭 ASLR

发表于 2025-12-13 | 更新于: 2026-02-16 | 分类于 调试
字数统计: | 阅读时长 ≈ 分钟

缘起

写文章的时候经常需要截图,有时候截完图程序就关了,但是发现还有另外一个地方需要截图说明。再次启动程序的时候,模块加载位置变了,与原来的截图的内容不匹配了,又要重新截图。为了解决这个问题特意查了一下如何禁用地址空间随机化。有两种方法:一种是全局禁用,一种是针对某个程序禁用。这里做个记录,方便后续查询。

阅读全文 »

MFC对话框的“双胞胎”控件:隐藏了一个,还有一个在看我

发表于 2025-12-13 | 更新于: 2026-02-16 | 分类于 调试
字数统计: | 阅读时长 ≈ 分钟

缘起

前段日子,遇到两个非常有意思的 MFC 对话框相关的 bug:

  1. 为 ComboBox 控件设置了文本,但是却显示为空
  2. 明明已经隐藏了控件,但是控件却没有隐藏

最后发现这两个问题是同一个问题,对话框中存在相同 ID 的控件。因为这两个问题是同一个问题,本文只介绍其中一个问题的排查思路。

阅读全文 »

内存都去哪了?探究 VirtualAlloc 分配背后被“浪费”的 60KB

发表于 2025-12-06 | 更新于: 2026-02-16 | 分类于 调试
字数统计: | 阅读时长 ≈ 分钟

摘要

本文记录了一次因 VirtualAlloc 分配失败引发的 OOM 问题排查过程。通过编写测试程序模拟内存分配,发现 32 位进程在未开启 Large Address Aware 时,用户空间仅 2GB 可用,且 VirtualAlloc 实际分配粒度均为 64KB——若申请 4KB,剩余 60KB 将变为不可用空间,导致地址空间碎片化与大量浪费。借助 VMMap 碎片视图与 Windbg 分析,直观展示了分配粒度对内存布局的影响,并验证了按 64KB 对齐分配可避免该问题。

阅读全文 »

排错实战 | 当编译器"吃掉"函数声明:一次由宏冲突引发的离奇编译错误

发表于 2025-12-06 | 更新于: 2026-02-16 | 分类于 排错
字数统计: | 阅读时长 ≈ 分钟

摘要

在编译 .NET Runtime 源码研究 GC 机制时,我遇到了一个离奇的编译错误:函数 __asan_handle_no_return 的声明被编译器报错为”类函数宏的调用”。通过将源文件预处理输出到中间文件,我发现这个函数声明竟然变成了 void ;——它被”吃掉”了!追踪发现,在 utils.h 中定义了一个同名宏,当 __SANITIZE_ADDRESS__ 宏未定义时,该宏被展开为空,导致函数声明被意外删除。这个案例再次证明:宏命名冲突是编译错误的常见陷阱,而预处理输出文件是诊断这类问题的利器。同时,FileLocator 这类文件搜索工具在源码分析中不可或缺。

阅读全文 »

深入.NET Runtime:一次 OOM 异常的分析与源码追踪之旅

发表于 2025-11-29 | 更新于: 2026-02-16 | 分类于 NET
字数统计: | 阅读时长 ≈ 分钟

摘要

本文记录了一次对 .NET 应用程序发生的“内存不足”(OOM)异常进行的深度源码级调查。问题始于一个看似矛盾的现象:诊断工具显示有足够大的空闲内存块(约 50MB),但垃圾回收(GC)过程却在尝试预留较小内存段(约 16MB)时失败。

为了探究根源,笔者逆向追踪了诊断工具(如SOS)输出的数据链路:从高层诊断命令(AnalyzeOOMCommand)入手,逐步深入Microsoft.Diagnostics.Runtime (CLRMD)库、托管辅助类、Dac 接口,最终直达 .NET Runtime(CoreCLR) 底层的 GC 相关 Native 代码(如 ClrDataAccess、gc_heap)。

虽然最终并没能定位问题的真实原因,但是理清了 GC 在 virtual_alloc过程中因内存限制检查、地址空间布局考量等因素导致预留失败的具体逻辑,并对 .NET 源码有了一定的认识,还是非常值得记录分享的。

阅读全文 »

调试实战 | 使用 GFlags 与 WinDbg 定位 VS2022 “重复释放” 引发的崩溃

发表于 2025-11-29 | 更新于: 2026-02-16 | 分类于 调试
字数统计: | 阅读时长 ≈ 分钟

摘要

本文记录了一次独特的调试经历:作为开发利器的 Visual Studio 2022,其在切换调用栈时频繁崩溃。面对这一问题,利用 procdump 自动捕获崩溃转储文件,并通过 WinDbg 初步排查将问题指向堆内存的异常操作,可能是堆损坏或重复释放。为了精准定位,我启用 gflags 工具开启页堆检测,最终成功捕获到首次释放操作的完整调用栈,明确问题根源在于VSDebug!treegrid::CTreeGridItemContainerGenerator::Refresh过程中的内存重复释放。虽然因缺少源码无法直接修复,但通过环境隔离(关闭特定程序)避免了问题复现。此次实战再次证明了 procdump、gflags 等工具在诊断复杂内存问题中的巨大价值,也提醒我们即使面对没有源码的“黑盒”组件,系统化的调试方法依然能指引我们找到问题的本质。

缘起

最近,用 vs2022 在调试的时候,切换调用栈,会有很大概率崩溃。一次两次就忍了,不停的崩溃就有点说不过去了。 话不多说,先放张动图看看 vs2022 是怎么崩溃的。

vs2022崩溃

阅读全文 »

调试实战 | 一个隐蔽的崩溃:当 this 指针在构造函数中“杀死”自己

发表于 2025-11-22 | 更新于: 2026-02-16 | 分类于 调试
字数统计: | 阅读时长 ≈ 分钟

摘要

在最近的项目开发中,我遇到了一个由智能指针误用导致的程序崩溃问题。问题的根源在于 SheetDataHandler 类的构造函数中,将 this 指针传递给了一个接收 SheetDataHandlerPtr(智能指针类型)参数的静态函数 HandleMissingColumn。这个看似简单的操作,却导致了对象在构造函数执行期间被意外释放,最终引发空指针访问异常。

通过深入调试和反汇编分析,我发现当 this 指针被隐式转换为智能指针时,引用计数会从 0 增加到 1,而在函数调用结束后,智能指针对象析构时引用计数又减回 0,从而触发了对象的 delete 操作。这导致构造函数尚未执行完毕,对象就已经被销毁,后续对成员变量的访问变成了访问已释放内存的非法操作。

阅读全文 »

调试实战 | 永远不要忽略编译警告:记一次由重复switch语句导致的诡异崩溃

发表于 2025-11-22 | 更新于: 2026-02-16 | 分类于 调试
字数统计: | 阅读时长 ≈ 分钟

摘要

本文记录并剖析了一次由看似低级的代码错误引发的、令人意想不到的程序崩溃。问题的根源在于 GetErrorStr 函数中一个容易被忽略的重复 switch语句。这个错误导致函数返回的 std::wstring 对象未被正确初始化,最终在构造 CResult 对象时引发了空指针访问异常 。

本文通过深入分析崩溃调用栈和反汇编代码,清晰地总结了从函数异常返回(未初始化字符串)到调用端使用无效数据(触发崩溃)的完整过程。希望这个案例能提醒各位,编译警告是发现潜在风险的第一道防线,而基础的汇编知识则是深入调试的利器。文末有可复现问题的代码,欢迎动手实践。

阅读全文 »

基础知识 | 函数基础 5 —— 实战修复虚函数导致的编译错误

发表于 2024-12-01 | 更新于: 2026-02-16 | 分类于 基础知识
字数统计: | 阅读时长 ≈ 分钟

缘起

前一阵子,同事遇到了一个奇怪的编译问题,大概情况如下:

classA 是模块 A 中的一个类, classA 没有定义构造函数,其它函数都是导出的。B 模块依赖了 A 模块,并且会调用 classA 的接口。当在 B 模块中添加了实例化 classA 对象的代码的时候,报链接错误,提示找不到 classA 类的某个虚函数。

我帮忙看过之后发现又是虚函数相关的编译问题(正好最近在总结虚函数相关的问题),这是送上门的素材啊!

阅读全文 »

基础知识 | 函数基础 4 —— 又崩溃了,原来是虚函数声明顺序不一致捣的鬼

发表于 2024-11-09 | 更新于: 2026-02-16 | 分类于 基础知识
字数统计: | 阅读时长 ≈ 分钟

缘起

前一阵子,同事遇到了一个崩溃问题,解决后发现这个崩溃是由于在公共类中加了一个虚函数接口,但是并没有编译相关模块导致的。这种崩溃问题是老朋友了。在此之前,我已经写了几篇关于虚函数的总结,感兴趣的小伙伴儿可以查看这几篇文章:

《基础知识 | 有趣的动态转换》

《基础知识 | C++ 虚函数简介》

《基础知识 | c++ 有趣的动态转换之 delete 崩溃探究兼谈基类虚析构的重要性》

《基础知识 | 函数基础 1 —— 基本概念 & 如何调用外部模块的函数》

《基础知识 | 函数基础 2 —— 如何不依赖外部模块却能调用它的函数?》

《基础知识 | 函数基础 3 —— 跨模块调用未导出虚函数的各种姿势 》

本文主要关注以下两个问题,如果你已经有了很明确的答案,可以跳过本文:

  • 如果在编译 A 模块的时候,Test 类的虚函数声明的顺序是 Test1, Test2, Test3,但是在 B 模块编译的时候,Test 类头文件中虚函数顺序变成了 Test2, Test1, Test3。在 B 模块中调用 test->Test1(),调用的是哪个函数呢?
  • 假设 A 模块代码不变,但是在编译 B 模块的时候,Test 类的头文件中又多了一个虚函数 Test4(),在 B 模块中调用 test->Test4(),代码可以正常编译吗?会有链接问题吗?如果可以正常编译链接,运行的时候会有问题吗?
阅读全文 »

基础知识 | 函数基础 3 —— 跨模块调用未导出虚函数的各种姿势

发表于 2024-09-14 | 更新于: 2026-02-16 | 分类于 基础知识
字数统计: | 阅读时长 ≈ 分钟

缘起

在上篇文章 《基础知识 | 函数基础 2 —— 如何不依赖外部模块却能调用它的函数?》中,我们明白了一个事实 —— 可以在不依赖外部模块的情况下通过类对象指针调用其虚函数。

但是遗留了几个问题,如下:

  • 问题 1:Interface1 类中没有声明构造函数,编译器生成的构造函数保存在哪里?GetInterface 模块还是 Interface 模块?

  • 问题 2:Interface1 的虚表保存在哪里?GetInterface 模块还是 Interface 模块?

  • 问题 3:如果去掉 Interface1 中虚函数的导出符号,上述代码能编译通过吗?

  • 问题 4:如果在 Interface1 中声明了未导出的构造函数,上述代码能编译通过吗?

  • 问题 5:如果 InterfaceBase::Test1() 不是纯虚函数,上述代码能编译通过吗?

  • 问题 6:如果 InterfaceBase 的析构函数不是虚函数,上述代码能编译通过吗?

本文力求把这几个问题弄清楚。如果您对以上问题已经有了答案,可以跳过本文。

阅读全文 »

基础知识 | 函数基础 2 —— 如何不依赖外部模块却能调用它的函数?

发表于 2024-07-27 | 更新于: 2026-02-16 | 分类于 基础知识
字数统计: | 阅读时长 ≈ 分钟

缘起

前一段日子,同事遇到了一个奇怪的现象 —— B 模块调用了 A 模块某个类的成员函数,没有依赖 A 模块,编译时没有报错。而 C 模块也调用了 A 模块中同一个类的成员函数,没有依赖 A 模块,编译时却报了链接错误。

简单语音沟通后觉得不太可能。用了 A 模块的函数,却不依赖 A 模块,有点儿不讲道理!

因为当时我没在电脑前,跟同事简单沟通了几个可以设置库依赖的位置,结果都没有发现对应的依赖项。

心里越发觉得不可思议,难道 B 模块是通过其它方式依赖 A 模块的?正常情况下,如果 B 模块依赖 A 模块,一定可以在 B 模块的导入表中看到 A 模块相关的记录。于是建议同事查看 B 模块的导入表,但是同事不太熟悉。因为项目比较急,遂建议同事在 C 模块中添加对 A 模块的依赖,先解决项目问题,后面有机会再调查具体原因。

直到最近才有时间调查这个问题,结果发现这个问题非常有意思 —— B 模块确实没有依赖 A 模块(B 模块的导入表中确实没发现 A 模块的相关项),但是 B 模块确实调用了 A 模块中的函数,而且不是通过 LoadLibrary() + GetProcAddress() 的方式调用的。

本文主要关注以下问题,如果你已经有了答案,可以跳过本文。

  • B 模块在什么情况下可以调用 A 模块中的函数,但是却不依赖 A 模块?
阅读全文 »

基础知识 | 函数基础 1 —— 基本概念 & 如何调用外部模块的函数

发表于 2024-07-13 | 更新于: 2026-02-16 | 分类于 基础知识
字数统计: | 阅读时长 ≈ 分钟

缘起

最近遇到了几个跟虚函数有关的问题,既有编译链接问题,又有运行问题,归根到底是基本功不扎实导致的问题。本文将会简单梳理一下函数相关的基本知识:

  • 函数是什么?
  • 函数调用约定有哪些?有什么作用?
  • 普通函数、类静态函数、类成员函数的区别是什么?
  • 什么时候会调用构造函数,什么时候会调用析构函数?
  • 调用虚函数与调用其它函数的区别是什么?
  • B 模块如何调用 A 模块的函数?

本文适合初学者,如果您已经有一定的开发经验,可以跳过本文。

阅读全文 »

调试实战 | 调试另外一个由于全局变量初始化顺序导致的 dll 加载失败问题(下)

发表于 2024-06-08 | 更新于: 2026-02-16 | 分类于 调试
字数统计: | 阅读时长 ≈ 分钟

缘起

在前面两篇文章中,应该算是彻底理清了项目中存在的两个问题。感兴趣的小伙伴儿可以参考《调试实战 | 调试另外一个由于全局变量初始化顺序导致的 dll 加载失败问题(上)》和 《调试实战 | 调试另外一个由于全局变量初始化顺序导致的 dll 加载失败问题(中)》。

在上篇文章的末尾提到一种情况

如果在 LoadDlls.exe 中也显式加载了 dll3.dll,还会不会崩溃呢?答案是可能崩溃,也可能不崩溃。

因为 RegisterInitCallback()内部更新数据时使用的是 map.insert(),这会导致一个问题 —— 如果 map 中已经存在相同的 key,那么 insert() 会失败,不会更新数据。

试想,如果显式加载 dll3.dll 成功,但是 dll3.dll 的基址变了。map 中保存的还是旧的无效地址,而不是新函数地址。

如果 dll3.dll 的基址没有发生变化,新函数地址与旧函数地址一样,程序可以非常“幸运”的正常运行。

本文通过实战来验证上文中的结论,什么情况下会崩溃,什么情况下不崩溃。

阅读全文 »
12…8
BianChengNan

BianChengNan

147 日志
34 分类
238 标签
RSS
GitHub 知乎 博客园
© 2019 — 2026 BianChengNan | 全博客共 字
0%