[开发经验] 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)。

相关推荐
无限进步_1 小时前
C++ 多态机制完全解析:从虚函数重写到动态绑定原理
java·c语言·jvm·数据结构·c++·windows·后端
CoderCodingNo1 小时前
【信奥业余科普】C++ 的奇妙之旅 | 20:更安全的间接访问——引用的设计动机与实战对比
开发语言·c++
天都3572 小时前
青少年ctf 日志排查 复盘
windows·网络安全·应急响应
zh_xuan3 小时前
使用命令行把安装包上传到github
c++·git·libcurl·c++工程打包
无限进步_4 小时前
二叉搜索树完全解析:从概念到实现与应用场景
c语言·开发语言·数据结构·c++·算法·github·visual studio
鱼跃厂长4 小时前
这份skill,能将你的简历提升到字节的水平!
c++·ai·ai编程
天若有情6735 小时前
逆向玩家狂喜!用C++野生写法一键破解线性加密(不规范但巨好用)
开发语言·c++·算法
咸鱼翻身小阿橙5 小时前
Qt QML调用C++注册类
java·c++·qt