visual-studio-2005 - x64 - visual studio redistributable 2013
Windows 8上的FreeConsole行为 (2)
在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);
}
问题是由于以前的Windows 8标准(不是重定向的)控制台句柄(由GetStdHandle返回的)实际上是伪手柄,其值不会与其他内核对象句柄干涉,所以在由FreeConsole“关闭”之后写入该伪句柄总是失败。 在Win8中,MS改变了一些东西,所以GetStdHandle返回了引用控制台子系统驱动程序对象的正常内核对象句柄(实际上这个驱动程序也只出现在Win8中)。 所以FreeConsole关闭那个句柄。 CRT在启动时执行GetStdHandle最有趣的事情,并将返回值保存在某处,并使用称为C函数的任何地方访问std :: in / out / err。 由于FreeConsole关闭了该句柄,并且它不再是一个特殊的伪句柄值 - 同样的句柄值可以被任何其他打开的内核对象句柄重复使用,如果在这种情况下它不是文件,管道或套接字原因,你的调试outpout将去那里:)
在不同的Windows版本上解散FreeConsole的代码后,我找出了问题的原因。
FreeConsole是一个非常不起眼的功能! 我真的关闭了一大堆句柄,即使它没有“拥有”这些句柄(例如,由stdio函数拥有的句柄)。
而且,在Windows 7和Windows 8中的行为是不同的,并且在10中又改变了。
提出解决方案时遇到以下困难:
- 一旦stdio有控制台设备的句柄,没有记录的方式让它放弃那个句柄,没有调用CloseHandle。 你可以调用
close(1)
或freopen(stdout)
或任何你喜欢的,但是如果有一个打开的文件描述符指向控制台,CloseHandle将被调用,如果你想要切换到新的NUL句柄之后FreeConsole。 - 另一方面,从Windows 10开始,也无法避免FreeConsole调用CloseHandle。
- Visual Studio的调试器和Application Verifier会标记应用程序在无效的句柄上调用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);
}