理解 Win32API-OutputDebugString 原理

本文详细介绍了 Win32API 中 OutputDebugString 函数的工作原理及其实现细节,包括应用程序和调试器之间的交互过程、共享内存机制、互斥量和事件对象的使用等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

理解 Win32API-OutputDebugString

Win32API OutputDebugString()可以使你的程序和调试器进行交谈,应用程序和调试器交谈的机制相当简单,而本文将揭示整件事情是如何工作的。

 

应用程序用法

<windows.h> 文件声明了 OutputDebugString() 函数的两个版本:

一个用于 ASCII;一个用于 Unicode

如果有调试器的话,使用一个NULL结尾的字符串缓冲区简单调用 OutputDebugString() 将导致信息出现在调试器中。通常用法是:

sprintf(szMsgBuf, "Cannot open file %s [error = %ld] \n", szFileName,

    GetLastError());

OutputDebugString(szMsgBuf);

 

协议

在应用程序和调试器之间传递数据是通过一个 4KB 大小的共享内存块完成的,并有一个互斥量和两个事件对象用来保护对他的访问。下面就是相关的四个内核对象:

对象名称       对象类型

DBWinMutex      Mutex (互斥量)

DBWIN_BUFFER     Section(共享内存)

DBWIN_BUFFER_READY  Event (事件对象)

DBWIN_DATA_READY   Event (事件对象)

互斥量通常一直保留在系统中,其他三个对象仅随调试器要接收信息时才创建。

事实上如果一个调试器发现后三个对象(共享内存与两个事件对象)已经存在,它会拒绝运行。

当 DBWIN_BUFFER 出现时,会被组织成以下结构。进程 ID 显示信息的来源,字符串数据填充这 4KB 的剩余部分。按照约定,信息的末尾总是包括一个 NULL 字节。

 

struct dbwin_buffer // 4KB共享内存

{

DWORD dwProcessId               // 进程 ID 显示信息的来源

char  data[4096 - sizeof(DWORD)]; // 除去进程ID占用的四字节以外的字节空间

};

 

在 OutputDebugString() 被调用时,它执行以下步骤:

(注意任意步骤出错都将使调试信息输出失败)

1.     等待和获取该DBWinMutex;

2.     映射DBWIN_BUFFER 共享内存段到当前进程内存空间中;

3.     打开DBWIN_BUFFER_READY 和 DBWIN_DATA_READY 事件对象;

4.     等待DBWIN_BUFFER_READY 事件对象为有信号状态:表示共享内存不再被占用,但等待共享内存可写入不会超过 10 秒;

5.     复制数据到共享内存中(前4字节为进程ID),再保存当前进程 ID。必须在字符串结尾后放置一个 NULL字节;

6.     通过设置DBWIN_DATA_READY 事件对象表示调试器可从共享内存中取走数据;

7.     释放互斥量,且关闭事件对象和共享内存段对象,但保留互斥量的句柄以备后用;

对于调试器来说,互斥量根本不需要,如果事件对象或共享内存对象已经存在,则假定其他调试器已经在运行。系统中任意时刻只能存在一个调试器,以下为它的执行步骤:

1.     创建共享内存段以及两个事件对象(如果失败,则退出程序);

2.     设置DBWIN_BUFFER_READY 事件对象,由此OutputDebugString()得知缓冲区可用;

3.     等待DBWIN_DATA_READY 事件对象变为有信号状态;

4.     从共享内存段映射内存中提取进程 ID 和 NULL 结尾的字符串;

5.     转到步骤 2;

应用程序的运行速度会受到调试器的左右。

权限问题

我们发现 OutputDebugString() 有时不可靠,问题总是围绕着 DBWinMutex 对象出现,这就需要我们察看系统许可,以找出为什么会这么麻烦。

互斥量对象会一直存活着直到使用它的最后一个程序关闭其句柄,故而它能在初建它的应用程序退出后保留相当长的时间。因为此对象被广泛地共享,所以它必须被赋予明确的许可才允许任何人使用它。事实上,“缺省”许可几乎从不适用。

在 Win2000 里这些许可如下表中看到的:

系统用户身份   许可

SYSTEM      MUTEX_ALL_ACCESS

Administrators  MUTEX_ALL_ACCESS

Everybody     SYNCHRONIZE | READ_CONTROL | MUTEX_QUERY_STATE

希望发送调试信息的OutputDebugString()只需要等待和获取该互斥量的能力,也即体现为拥有SYNCHRONIZE 权限。上列的许可对于所有参与的用户都是完全正确的。

不过如果有人观察 CreateMutex() 在对象已经存在时的行为,就会发现奇怪的事情。在这种情况下,Win32的表现就好像我们进行了如下调用:

OpenMutex(MUTEX_ALL_ACCESSFALSE"DBWinMutex");

甚至将所有的软件开发都以管理员来执行也不是一个完整的修正方法:如果存在其他的用户(例如服务)以非管理员运行而许可配置不正确,它们的调试信息将会丢失。

必须采用另外的方法,当对象已经存活于内存中时我们将硬性改变其上的许可配置。这需要调用SetKernelObjectSecurity(),下列程序片断展示一个程序如何才能打开互斥量并安装一个新的 DACL。此 DACL即使在程序退出后也仍然保持着,只要任一其他程序还维护有该互斥量的句柄。

// open the mutex that we're going to adjust

HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESSFALSE"DBWinMutex");

// create SECURITY_DESCRIPTOR with an explicit,

// empty DACL that allows full access to everybody

SECURITY_DESCRIPTOR sd;

InitializeSecurityDescriptor(&sdSECURITY_DESCRIPTOR_REVISION);

SetSecurityDescriptorDacl(&sd   // addr of SD

                          TRUE  // TRUE = DACL present

                          NULL  // ... but it's empty (wide open)

                          FALSE); // DACL explicitly set, not defaulted

// plug in the new DACL

SetKernelObjectSecurity(hMutexDACL_SECURITY_INFORMATION&sd);

这一方法明确地走向了正确的道路,但我们还需要找一个地方来放置此逻辑。把它放在一个一经请求即运行的小程序中是可以的,但是看起来它有可能被中断。我们的办法是写一个 Win32 服务来干这件事情:它在系统引导时启动,打开或者创建互斥量,然后设置对象的安全性以允许广泛的访问。然后休眠直到系统关闭,在此过程中保持互斥量的打开状态。它不消耗任何 CPU 时间。

实现细节

// pseudocode for OutputDebugString from KERNEL32.DLL ver 5.0.2195.6794
void OutputDebugStringA(LPTSTR* lpString)
{
	DBWIN_buffer*	pDBBuffer = 0;
	HANDLE			hFileMap = 0;
	HANDLE			hBufferEvent = 0;
	HANDLE			hDataEvent = 0;

	// if we can't make or acquire the mutex, we're done
	if (hDbwinMutex == 0)
		hDbwinMutex = setup_mutex();
	if (hDbwinMutex == 0)
		return;
	(void)WaitForSingleObject(hDbwinMutex, INFINITE);
	hFileMap = OpenFileMapping(FILE_MAP_WRITE, FALSE, "DBWIN_BUFFER");
	pDBBuffer = (DBWIN_buffer*)MapViewOfFile(hFileMap, FILE_MAP_READ | FILE_MAP_WRITE, 0,	// file offset high
											  0,	// file offset low
											  0);	// # of bytes to map

	// (entire file)
	hBufferEvent = OpenEvent(SYNCHRONIZE, FALSE, "DBWIN_BUFFER_READY");
	hDataEvent = OpenEvent(EVENT_MODIFY_STATE, FALSE, "DBWIN_DATA_READY");

	const char*		p = lpString;
	int				len = strlen(lpString);
	while (len > 0)
	{
		if (WaitForSingleObject(hBufferEvent, 10 * 1000) != WAIT_OBJECT_0)
		{
			  break;	// ERROR: give up
		}

		// populate the shared memory segment. The string
		// is limited to 4k or so.
		pBuffer->dwProcessId = GetCurrentProcessId();

		int n = min(len, sizeof(pBuffer->data) - 1);
		memcpy(pBuffer->data, p, n);
		pBuffer->data[n] = '\0';
		len -= n;
		p += n;
		SetEvent(hDataEvent);
	}

	// cleanup after ourselves
	CloseHandle(hBufferEvent);
	CloseHandle(hDataEvent);
	UnmapViewOfFile(pDBBuffer);
	CloseHandle(hFileMap);
}

HANDLE setup_mutex(void)
{
	SID_IDENTIFIER_AUTHORITY	SIAWindowsNT = SECURITY_NT_AUTHORITY;
	SID_IDENTIFIER_AUTHORITY	SIAWorld = SECURITY_WORLD_SID_AUTHORITY;
	SID*						pSidSYSTEM = 0;
	SID *pSidAdmins = 0;
	SID *pSidEveryone = 0;
	AllocateAndInitializeSid(&SIAWindowsNT, 1, SECURITY_LOCAL_SYSTEM_RID, 0, 0, 0, 0, 0, 0, 0, &pSidSYSTEM);
	AllocateAndInitializeSid(&SIAWindowsNT, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0,
							 &pSidAdmins);
	AllocateAndInitializeSid(&SIAWorld, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &pSidEveryone);

	const DWORD dwACLSize = GetLengthSid(pSidSYSTEM) + GetLengthSid(pSidAdmins) + GetLengthSid(pSidEveryone) + 32;
	ACL*		pACL = GlobalAlloc(0, dwACLSize);
	InitializeAcl(pACL, dwACLsize, ACL_REVISION);
	AddAccessAllowedAce(pACL, ACL_REVISION, SYNCHRONIZE | READ_CONTROL | MUTEX_QUERY_STATE, pSidEveryone);
	AddAccessAllowedAce(pACL, ACL_REVISION, MUTEX_ALL_ACCESS, pSidSYSTEM);
	AddAccessAllowedAce(pACL, ACL_REVISION, MUTEX_ALL_ACCESS, pSidAdmins);

	SECURITY_DESCRIPTOR sd;
	InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
	SetSecurityDescriptorDacl(&sd, TRUE, pACL, FALSE);

	SECURITY_ATTRIBUTES sa;
	ZeroMemory(&sa, sizeof(sa));
	sa.bInheritHandle = FALSE;
	sa.nLength = sizeof sa;
	sa.lpSecurityDescriptor = &sd;

	HANDLE	hMutex = CreateMutex(&sa, FALSE, "DBWinMutex");
	FreeSid(pSidAdmins);
	FreeSid(pSidSYSTEM);
	FreeSid(pSidEveryone);
	GlobalFree(pACL);
	return hMutex;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值