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

文章目录

概述

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

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

问题背景

项目结构简述:

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

原始清理流程如下:

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

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

排查过程

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

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

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

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

第二轮: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 调用使得 stdout、stderr、stdin 这三个 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)。

相关推荐
All for pursuit.1 小时前
【位运算-1】338.比特位计数
数据结构·c++·算法·leetcode
小师兄吃牛肉2 小时前
C语言中的nullptr有必要吗
c语言·c++·算法
怎么没有名字注册了啊2 小时前
Qt开发:MessageBox怎么调出标题栏那个问号按钮?
windows
WL学习笔记3 小时前
日期类(C++)
开发语言·c++·日期类实现
H.莓飛3 小时前
【C++】命名空间、缺省参数、函数重载与引用
linux·开发语言·c++·visual studio
李子红了时3 小时前
EncFS MP中文版汉化下载(免费文件加密工具)
windows·安全
辻弋2014 小时前
五年前的旅行视频糊成马赛克?Video2X用Real-ESRGAN逐帧重建细节,但只支持Windows、集显用户建议直接放弃
服务器·数据库·windows·游戏引擎·电脑
会飞的土拨鼠呀4 小时前
Windows 系统关闭虚拟内存文件pagefile.sys
windows
汉克老师5 小时前
GESP2026年9月认证C++八级( 第一部分选择题(1~7题)精讲
c++·gesp·小学生·学c++编程
郝学胜_神的一滴6 小时前
C++ Templates 06:搞懂模板代码的三种组织方式
c++·visual studio