[开发经验] DLL注入中控制台窗口无法关闭的排查与修复

文章目录

概述

在 DLL 注入项目中,通常会通过 AllocConsole() 分配一个控制台窗口用于调试输出。当用户按下热键(如 Del)触发卸载流程时,理应调用 FreeConsole() 关闭该窗口。但实际测试中发现:FreeConsole() 调用了,控制台窗口却不关闭

本文记录了从表面现象到根因的完整排查过程,涉及 spdlog 日志框架的 sink 机制和 C 标准流(freopen_s)对控制台句柄的持有问题。

问题背景

项目结构简述:

  • DLL 注入到目标进程后,DllMain 创建线程 MyThread
  • MyThread 中调用 spdlog 初始化日志系统、内部 AllocConsolestdout重定向
  • MyThread 循环中轮询热键,按键退出循环,执行清理后 FreeLibraryAndExitThread

原始清理流程如下:

cpp 复制代码
// 退出主循环后
FreeConsole();
FreeLibraryAndExitThread(hModule, 0);

按键后,控制台窗口始终不关闭。

排查过程

第一轮:怀疑清理流程阻塞

最初的假设是 FreeConsole() 被其它逻辑 卡住,FreeConsole() 就永远执行不到。

增加日志: 代码最后增加日志,确保FreeConsoleFreeLibraryAndExitThread均执行结束。

结果: 通过日志确认清理流程实际没有阻塞,全部在毫秒级完成。排除此假设。

第二轮:spdlog console sink 持有句柄

查看代码发现 spdlog 注册了 stdout_color_sink_mt

stdout_color_sink_mt 内部持有 stdout 的文件句柄。直接调 FreeConsole() 时 sink 仍在引用该句柄,Windows 不会销毁控制台窗口。

修复尝试: 清理spdlog logger->sinks()

cpp 复制代码
auto& sinks = logger->sinks();
sinks.erase(
    std::remove(sinks.begin(), sinks.end(), console_sink),
    sinks.end());

结果: 控制台窗口仍然不关闭。

第三轮:freopen_s 打开的 C 标准流仍持有句柄

关键发现:Log::Init() 中的 freopen_s 将 C 标准流重定向到了控制台:

cpp 复制代码
FILE* fp;
freopen_s(&fp, "CONOUT$", "w", stdout);
freopen_s(&fp, "CONOUT$", "w", stderr);
freopen_s(&fp, "CONIN$", "r", stdin);

这三个 freopen_s 调用使得 stdoutstderrstdin 这三个 C 标准流各自持有一个到 CONOUT$ / CONIN$ 的文件句柄。即使 spdlog 的 sink 被移除了,即使 FreeConsole() 被调用了,这些 C 标准流的文件描述符仍然 open,Windows 内核认为仍有句柄引用该控制台对象,因此不会销毁控制台窗口。

最终修复

完整清理顺序:

  1. 移除 spdlog 的 console sink(停止向控制台写入)
  2. FreeConsole()(解除进程与控制台的关联)
  3. fclose(stdout/stderr/stdin)(关闭 freopen_s 打开的底层文件句柄)
  4. FreeLibraryAndExitThread

原理总结

Windows 控制台窗口的生命周期由引用计数控制。以下对象都会增加控制台的引用:

引用来源 创建方式 释放方式
进程与控制台的关联 AllocConsole() FreeConsole()
spdlog stdout_color_sink std::make_shared<stdout_color_sink_mt>() 从 logger sinks 中移除
C 标准流 stdout/stderr freopen_s(&fp, "CONOUT$", ...) fclose(stdout)
C 标准流 stdin freopen_s(&fp, "CONIN$", ...) fclose(stdin)

只有所有引用都释放后,Windows 才会真正销毁控制台窗口。遗漏任何一个都会导致窗口残留。

总结

这个问题的排查经历了三轮:

  1. 清理阻塞假设 --- 通过日志排除,清理流程正常完成
  2. spdlog sink 持有句柄 --- 使用 CloseConsoleIfOwned() 正确移除 sink,但不够
  3. freopen_s 的 C 标准流 --- 最终根因,fclose 关闭残留句柄后窗口正常关闭

核心教训:AllocConsole() 容易配对 FreeConsole(),但中间如果有 freopen_s 重定向标准流,这些流会成为隐性的句柄持有者。清理控制台时必须同时处理所有层级的句柄引用------系统 API 层(FreeConsole)、框架层(spdlog sink)、C 运行时层(fclose)。

相关推荐
PascalMing几秒前
从零实现一款 Windows 下的 SSH 批量运维工具:LinuxSshTools 技术详解
运维·windows·ssh
Cheng小攸16 分钟前
CTF攻防综合实战(1)
windows
汉克老师19 分钟前
GESP6级C++考试语法知识(三十九、动态规划的启蒙(四、二维DP))
c++·动态规划·dp·gesp六级·二维dp
QiLinkOS20 分钟前
QiLink 技术委员会选举实施细则
c语言·数据结构·c++·单片机·嵌入式硬件·算法·开源
不会C语言的男孩24 分钟前
C++ Primer Plus 第13章:类继承
开发语言·c++
承渊政道25 分钟前
Linux系统学习【进程控制:进程创建、终止与等待、进程程序替换、自主shell命令行解释器详解】
linux·服务器·c++·学习·ubuntu·bash·远程工作
雪度娃娃27 分钟前
转向现代C++——优先选用删除函数而非private未定义函数
java·jvm·c++
QiLinkOS34 分钟前
发明人与专利价值共生逻辑
c语言·数据结构·c++·人工智能·单片机·嵌入式硬件·算法
王老师青少年编程37 分钟前
信奥赛C++提高组csp-s之平衡树(Treap)
c++·平衡树·treap·信奥赛·csp-s·提高组
磊 子1 小时前
STL之set以及set和map区别
开发语言·c++·算法