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

相关推荐
进化中的刀盾兽1 小时前
从零实现 C++ Json-Rpc(十):把消息交给真实连接——MuduoConnection 与 ConnectionFactory
c++·rpc
青瓦梦滋8 小时前
WebSocket协议
linux·服务器·网络·c++·websocket·网络协议·http
LannyChung12 小时前
PowerShell 下 git log 中文乱码的完整排查与解决
windows·git
CodeYp201913 小时前
第15讲:二叉搜索树
开发语言·c++·二叉搜索树
pipixia2003314 小时前
C++继承面试四连问:内存叠加、私有继承、sizeof、权限重映射
c++·面试·嵌入式·继承·面向对象·对象模型
牵猫散步的鱼儿16 小时前
C++ 内存分区
c++
choumin17 小时前
C++ 标准容器为什么不能存引用
c++·容器·引用类型
谙弆悕博士17 小时前
用 C 写了个 Windows 聊天室,踩了几个多线程和控制台的坑
c语言·开发语言·c++·windows·网络编程·聊天室
stolentime18 小时前
CF2277E Tree Guessing题解
c++·算法·贪心算法·交互·ad-hoc
syagain_zsx19 小时前
ODB 与 MySQL:C++ 里的 ORM 实践(类即表、事务铁律与查询封装)
开发语言·数据库·c++·mysql·工具库·1024程序员节