理解 Win32API-OutputDebugString
Win32API OutputDebugString()可以使你的程序和调试器进行交谈,应用程序和调试器交谈的机制相当简单,而本文将揭示整件事情是如何工作的。
应用程序用法
<windows.h> 文件声明了 OutputDebugString() 函数的两个版本:
一个用于 ASCII;一个用于 Unicode;
如果有调试器的话,使用一个NULL结尾的字符串缓冲区简单调用 OutputDebugString() 将导致信息出现在调试器中。通常用法是:
sprintf(szMsgBuf, "Cannot open file %s [error = %ld] /n", szFileName,
GetLastError_r());
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_ACCESS, FALSE, "DBWinMutex");
甚至将所有的软件开发都以管理员来执行也不是一个完整的修正方法:如果存在其他的用户(例如服务)以非管理员运行而许可配置不正确,它们的调试信息将会丢失。
必须采用另外的方法,当对象已经存活于内存中时我们将硬性改变其上的许可配置。这需要调用 SetKernelObjectSecurity(),下列程序片断展示一个程序如何才能打开互斥量并安装一个新的 DACL。此 DACL 即使在程序退出后也仍然保持着,只要任一其他程序还维护有该互斥量的句柄。
// open the mutex that we're going to adjust
HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, "DBWinMutex");
// create SECURITY_DESCRIPTOR with an explicit,
// empty DACL that allows full access to everybody
SECURITY_DESCRIPTOR sd;
InitializeSecurityDescriptor(&sd, SECURITY_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(hMutex, DACL_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, *pSidAdmins = 0, *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;
}