6

在 Windows 8 上,我们遇到了 FreeConsole 的问题。它似乎关闭了 stdio 句柄,而不关闭文件流。

这可能是 Windows 8 的问题,也可能是我根本不理解 Windows 控制台/GUI 应用程序子系统做事的(完全荒谬的)方式。

这是怎么回事?

下面的最小示例。使用编译器测试:VS2005、VS2013、VS2017,使用静态链接 CRT。

#include <windows.h>
#include <io.h>
#include <stdio.h>

static void testHandle(FILE* file) {
  HANDLE h = (HANDLE)_get_osfhandle(fileno(file));
  DWORD flags;
  if (!GetHandleInformation(h, &flags)) {
    MessageBoxA(0, "Bogus handle!!", "TITLE", MB_OK);
  }
}

int main(int argc, char** argv)
{
  freopen("NUL", "wb", stdout); // Demonstrate the issue with NUL
  // Leave stderr as it is, to demonstrate the issue with handles
  // to the console device.

  FreeConsole();

  testHandle(stdout);
  testHandle(stderr);
}
4

2 回答 2

5

由于以前的 Windows 8 标准(未重定向)控制台句柄(由 GetStdHandle 返回)实际上是伪句柄,其值不与其他内核对象句柄相交,因此在被 FreeConsole“关闭”后写入该伪句柄总是失败。 . 在 Win8 MS 里面改变了一些东西,所以 GetStdHandle 返回正常的内核对象句柄,它引用控制台子系统驱动程序对象(实际上该驱动程序也只出现在 Win8 中)。所以 FreeConsole 关闭了那个句柄。最有趣的是 CRT 在启动时执行 GetStdHandle 并将返回值保存在内部某处,并在任何地方使用称为访问 std::in/out/err 的 C 函数。由于 FreeConsole 关闭了该句柄,并且它不再是特殊的伪句柄值 - 相同的句柄值可以被任何其他打开的内核对象句柄重用,

于 2013-05-30T12:57:04.927 回答
1

在不同的 Windows 版本上拆解 FreeConsole 的代码后,我找出了问题的原因。

FreeConsole 是一个非常简单的功能!我确实为您关闭了大量句柄,即使它不“拥有”这些句柄(例如,stdio 函数拥有的句柄)。

而且,Windows 7 和 8 中的行为有所不同,并在 10 中再次更改。

这是提出修复程序时的困境:

  • 一旦 stdio 有一个到控制台设备的句柄,没有记录的方法可以让它放弃该句柄,而无需调用 CloseHandle。你可以调用close(1)orfreopen(stdout)或者你喜欢的任何东西,但是如果有一个打开的文件描述符指向控制台,如果你想在 FreeConsole 之后将 stdout 切换到新的 NUL 句柄,则会调用 CloseHandle。
  • 另一方面,由于 Windows 10 也无法避免 FreeConsole 调用 CloseHandle。
  • Visual Studio 的调试器和应用程序验证程序将应用标记为在无效 HANDLE 上调用 CloseHandle。而且,他们是对的,这真的不好。
  • 因此,如果您在调用 FreeConsole 之前尝试“修复”stdio,那么 FreeConsole 将执行无效的 CloseHandle(使用其缓存的句柄,并且无法告诉它句柄已消失 - FreeConsole 不再检查GetStdHandle(STD_OUTPUT_HANDLE))。而且,如果您先调用 FreeConsole,则无法修复 stdio 对象而不导致它们对 CloseHandle 进行无效调用。

通过消除,我得出结论,唯一的解决方案是使用未记录的函数,如果公共函数不起作用。

// The undocumented bit!
extern "C" int __cdecl _free_osfhnd(int const fh);
static HANDLE closeFdButNotHandle(int fd) {
  HANDLE h = (HANDLE)_get_osfhandle(fd);
  _free_osfhnd(fd); // Prevent CloseHandle happening in close()
  close(fd);
  return h;
}

static bool valid(HANDLE h) {
  SetLastError(0);
  return GetFileType(h) != FILE_TYPE_UNKNOWN || GetLastError() == 0;
}

static void openNull(int fd, DWORD flags) {
  int newFd;
  // Yet another Microsoft bug! (I've reported four in this code...)
  // They have confirmed a bug in dup2 in Visual Studio 2013, fixed
  // in Visual Studio 2017.  If dup2 is called with fd == newFd, the
  // CRT lock is corrupted, hence the check here before calling dup2.
  if (!_tsopen_s(&newFd, _T("NUL"), flags, _SH_DENYNO, 0) &&
      fd != newFd)
    dup2(newFd, fd);
  if (fd != newFd) close(newFd);
}

void doFreeConsole() {
  // stderr, stdin are similar - left to the reader.  You probably
  // also want to add code (as we have) to detect when the handle
  // is FILE_TYPE_DISK/FILE_TYPE_PIPE and leave the stdio FILE
  // alone if it's actually pointing to disk/pipe.
  HANDLE stdoutHandle = closeFdButNotHandle(fileno(stdout)); 

  FreeConsole(); // error checking left to the reader

  // If FreeConsole *didn't* close the handle then do so now.
  // Has a race condition, but all of this code does so hey.
  if (valid(stdoutHandle)) CloseHandle(stdoutHandle);

  openNull(stdoutRestore, _O_BINARY | _O_RDONLY);
}
于 2017-12-15T13:47:51.607 回答