Windows 8上的FreeConsole行为

时间:2012-10-01 15:29:07

标签: visual-studio-2005 windows-8 msvcrt

在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);
}

2 个答案:

答案 0 :(得分:5)

事先由Windows 8标准(未重定向)控制台处理(由GetStdHandle返回)的事实引起的问题实际上伪句柄,其值不与其他内核对象句柄相关,因此在被“关闭”之后写入该伪句柄FreeConsole总是失败。 在Win8 MS中改变了一些内容,因此GetStdHandle返回引用控制台子系统驱动程序对象的正常内核对象句柄(实际上该驱动程序也只出现在Win8中)。所以FreeConsole关闭了这个句柄。最有趣的事情是CRT在启动时执行GetStdHandle并在内部保存返回值,并在任何使用称为访问std :: in / out / err的C函数的地方使用。由于FreeConsole关闭了该句柄,并且它不再是一个特殊的伪句柄值 - 任何其他打开的内核对象句柄都可以重用相同的句柄值,如果在这种情况下它不是文件,管道或套接字原因你将会很幸运你的调试输出会去那里:)

答案 1 :(得分:1)

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

FreeConsole是一个非常不起眼的功能!我确实为你关闭了大量的手柄,即使它没有自己的#34;那些句柄(例如stdio函数拥有的HANDLEs)。

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

在提出解决方案时,这是一个两难选择:

  • 一旦stdio对控制台设备有一个HANDLE,就没有记录的方法让它放弃该句柄,而不需要调用CloseHandle。您可以调用close(1)freopen(stdout)或任何您喜欢的内容,但如果有一个打开的文件描述符引用控制台,如果您想将stdout切换到新的NUL,将调用CloseHandle处理 在FreeConsole之后。
  • 另一方面,由于Windows 10也无法避免FreeConsole调用CloseHandle。
  • Visual Studio的调试器和Application Verifier标记用于在无效HANDLE上调用CloseHandle的应用程序。并且,他们是对的,它真的不好。
  • 所以,如果你试着&#34;修复&#34;在调用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);
}