文章目錄
OutputDebugString的適用情境:
同時可以有多個應用程式進程(生產者)發送調試資訊,只能有一個調試進程(消費者,egDebugView,VS2005等)接受。
實現方法:
生產者之間通過Mutex同步。
生產者和消費者之間通過兩個Event同步。
消費者之間通過發現一旦Event或緩衝區已經存在了,則退出以保證只有一個消費者。
參考
轉載: 譯:理解 Win32 "OutputDebugString"
http://www.cnblogs.com/xfxsworld/archive/2008/03/22/1117794.html
譯:理解 Win32 "OutputDebugString"
堅定的 Win32 開發人員可能對
OutputDebugString() API 函數比較熟悉,它可以使你的程式和調試器進行交談。它要比建立記錄檔容易,而且所有“真正的”調試器都能使用它。應用程式和調試器交談的機制相當簡單,而本文將揭示整件事情是如何工作的。
本文首先是由以下事件促使的,我們觀察到 OutputDebugString() 在管理員和非管理使用者試圖一起工作或遊戲時並不總是能可靠地工作(至少在 Win2000 上)。我們懷疑是一些相關的核心對象的許可權問題,此間涉略了相當多不得不寫下來的資訊。
請注意,儘管我們使用了“調試器”這一術語,但不是從調試 API 的意義上來使用的:並沒有“逐步執行”、“斷點”或者“附著到進程”等可以在 MS Visual C 或者一些真正的互動開發環境中找到的東西。從某種意義上來說,任何實現了協議的程式都是“調試器”。可能是一個非常小的命令列工具,或者像來自於 SysInternals 那幫聰明的傢伙們的 DebugView 那樣的進階貨。
內容目錄
- 應用程式用法
- 協議
- 許可權問題
- 實現細節
- 胡思亂想
- Unixwiz.net 工具:dbmutex
應用程式用法
<windows.h> 檔案聲明了 OutputDebugString() 函數的兩個版本 - 一個用於 ASCII,一個用於 Unicode - 不像絕大多數 Win32 API 一樣,原始版本是 ASCII。而大多數的 Win32 API 的原始版本是 Unicode。
使用一個 NULL 結尾的字串緩衝區簡單調用 OutputDebugString() 將導致資訊出現在調試器中,如果有調試器的話。構建一條資訊並發送之的通常用法是:
sprintf(msgbuf, "Cannot open file %s [err=%ld]/n", fname, GetLastError());OutputDebugString(msgbuf);
不過在實際環境中我們中的不少人會建立一個前端函數,以允許我們使用 printf 風格的格式化。下面的 odprintf() 函數格式化字串,確保結尾有一個合適的斷行符號換行(刪除原來的行結尾),並且發送資訊到調試器。
#include <stdio.h>#include <stdarg.h>#include <ctype.h>void __cdecl odprintf(const char *format, ...){char buf[4096], *p = buf;va_list args;va_start(args, format);p += _vsnprintf(p, sizeof buf - 1, format, args);va_end(args);while ( p > buf && isspace(p[-1]) )*--p = '/0';*p++ = '/r';*p++ = '/n';*p = '/0';OutputDebugString(buf);}
於是在代碼中使用它就很簡單:
...odprintf("Cannot open file %s [err=%ld]", fname, GetLastError());...
我們已經這樣使用多年了。
協議
在應用程式和調試器之間傳遞資料是通過一個 4KB 大小的共用記憶體塊完成的,並有一個互斥量和兩個事件對象用來保護對他的訪問。下面就是相關的四個核心對象:
| 對象名稱 |
物件類型 |
| DBWinMutex |
Mutex |
| DBWIN_BUFFER |
Section (共用記憶體) |
| DBWIN_BUFFER_READY |
Event |
| DBWIN_DATA_READY |
Event |
互斥量通常一直保留在系統中,其他三個對象僅當調試器要接收資訊才出現。事實上 - 如果一個調試器發現後三個對象已經存在,它會拒絕運行。
當 DBWIN_BUFFER 出現時,會被組織成以下結構。進程 ID 顯示資訊的來源,字串資料填充這 4K 的剩餘部分。按照約定,資訊的末尾總是包括一個 NULL 位元組。
struct dbwin_buffer { DWORD dwProcessId; char data[4096-sizeof(DWORD)]; };
當 OutputDebugString() 被應用調用時,它執行以下步驟。注意在任意位置的錯誤都將放棄整個事情,調試請求被認為是什麼也不做(不會發送字串)。
- 開啟 DBWinMutex 並且等待,直到我們取得了獨佔訪問。
- 映射 DBWIN_BUFFER 段到記憶體中:如果沒有發現,則沒有調試器在運行,將忽略整個請求。
- 開啟 DBWIN_BUFFER_READY 和 DBWIN_DATA_READY 事件對象。就像共用記憶體段一樣,缺少對象意味著沒有可用的調試器。
- 等待 DBWIN_BUFFER_READY 事件對象為有訊號狀態:表示記憶體緩衝區不再被佔用。大部分時候,這一事件對象一被檢查就處於有訊號狀態,但等待緩衝區就緒不會超過 10 秒(逾時將放棄請求)。
- 複製資料直到記憶體緩衝區中接近 4KB,再儲存當前進程 ID。總是放置一個 NULL 位元組到字串結尾。
- 通過設定 DBWIN_DATA_READY 事件對象告訴調試器緩衝區就緒。調試器從那兒取走它。
- 釋放互斥量。
- 關閉事件對象和段對象,但保留互斥量的控制代碼以備後用。
在調試器端會簡單一點。互斥量根本不需要,如果事件對象和/或共用記憶體對象已經存在,則假定其他調試器已經在運行。系統中任意時刻只能存在一個調試器。
- 建立共用記憶體段以及兩個事件對象。如果失敗,退出。
- 設定 DBWIN_BUFFER_READY 事件對象,由此應用程式得知緩衝區可用。
- 等待 DBWIN_DATA_READY 事件對象變為有訊號狀態。
- 從記憶體緩衝區中提取進程 ID 和 NULL 結尾的字串。
- 轉到步驟 2。
這使我們認為這決不是一種低消耗的發送資訊的方法,應用程式的運行速度會受到調試器的左右。
許可權問題
我們發現 OutputDebugString() 有時不可靠已經好幾年了,而且我們十分不解為什麼微軟這麼長時間也沒把它搞好。奇怪的是,問題總是圍繞著 DBWinMutex 對象出現,這就需要我們察看許可系統以找出為什麼會這麼麻煩。
互斥量對象會一直存活著直到使用它的最後一個程式關閉其控制代碼,故而它能在初始建立它的應用程式退出後保留相當長的時間。因為此對象被廣泛地共用,所以它必須被賦予明確的許可以允許任何人使用它。事實上,“預設”許可幾乎從不適用,這一問題被計為在 NT 3.51 和 NT 4.0 中我們觀察到的第一個問題。
當時的修正方法是使用一個廣泛開放的 DACL 建立互斥量,以此來允許任何人訪問它,但是看樣子在 Win2000 裡這些許可被加強了。表面上它看起來是正確的,就像我們在下表中看到的:
| SYSTEM |
MUTEX_ALL_ACCESS |
| Administrators |
MUTEX_ALL_ACCESS |
| Everybody |
SYNCHRONIZE | READ_CONTROL | MUTEX_QUERY_STATE |
希望發送調試資訊的應用只需要等待和擷取該互斥量的能力,也即體現為擁有 SYNCHRONIZE 許可權。上列的許可對於所有參與的使用者都是完全正確的。
不過如果有人觀察 CreateMutex() 在對象已經存在時的行為,就會發現奇怪的事情。在這種情況下,Win32 的表現就好像我們進行了如下調用:
OpenMutex(MUTEX_ALL_ACCESS, FALSE, "DBWinMutex");
儘管我們確實只需要 SYNCHRONIZE 訪問,但它還是假定調用者要做任何事情(MUTEX_ALL_ACCESS)。因為非管理員沒有這些許可權 - 僅有上列的少許 - 互斥量不能被開啟或者擷取,於是 OutputDebugString() 不做任何事情就悄悄地返回了。
甚至將所有的軟體開發都以管理員來執行也不是一個完整的修正方法:如果存在其他的使用者(例如服務)以非管理員運行而許可配置不正確,它們的調試資訊將會丟失。
我們感覺真正的修正需要微軟為 CreateMutex() 添加一個參數 - 如果對象已經存在時用於隱含的 OpenMutex() 調用的存取遮罩。也許某天我們會看到一個 CreateMutexEx(),但在此期間我們必須採用另外的方法。代之以,當對象已經存活於記憶體中時我們將硬性改變其上的許可配置。
這需要調用 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 服務來幹這件事情。
我們的 dbmutex 工具完成的就是這一工作:它在系統引導時啟動,開啟或者建立互斥量,然後設定對象的安全性以允許廣泛的訪問。然後休眠直到系統關閉,在此過程中保持互斥量的開啟狀態。它不消耗任何 CPU 時間。
實現細節
我們花了很多時間使用 IDA Pro 深入到 Windows 2000 KERNEL32.DLL 的實現中,我們認為,對於它在更精確的基礎上到底是如何工作的已經有了良好的掌握。在這兒我們給出 OutputDebugString() 函數的虛擬碼(我們沒有編譯過它),以及建立互斥量的函數。
我們故意略去了大多數的錯誤檢查:如果事情變糟了,它將釋放所有已指派的資源並退出,就像沒有調試器存在一樣。目的是展示一般行為而不是對代碼的完整的逆向工程。
“setup” 函數 - 名字是我們起的 - 建立互斥量或者在已經存在時開啟它。經過一些努力來設定互斥量對象的安全性以使任何人都能用它,儘管我們會看到其實並沒有完全正確地得到它。
• OutputDebugString.txt
胡思亂想
一些人可能會感到這是一個安全性問題,其實並不是。非管理使用者確實擁有適當使用 OutputDebugString() 的所有許可權,不過由於“請求比所需更多許可權”這一常見問題,一個合理的請求因形成了錯誤的形態而被拒絕了。
但並不像大部分的這種問題那樣,這並非是有意的。大多數的錯誤是開發人員顯式請求了更多(如“MUTEX_ALL_ACCESS”),而這次的掩碼是由 CreateMutex() 的行為隱含的。這使得如果 Win32 API 不做改動的話更加難於避免。
---
當分析 KERNEL32.DLL 中的 OutputDebugStringA() 時,非管理員如何能夠有可能去削弱系統變得明顯起來。一旦得到互斥量,一個要發送調試資訊的應用會等待 DBWIN_BUFFER_READY 事件對象就緒最多十秒鐘,如果逾時則放棄。這看起來是一個謹慎的防範措施,如果調試系統忙的話,用以避免被餓死。
但在更早的步驟裡,等待互斥量,沒有這樣的逾時設定。如果系統中的任何進程 - 包括非特權進程 - 可以以請求 SYNCHRONIZE 許可權開啟此互斥量,並且不釋放它,所有其他試圖擷取此互斥量的進程將會無限停止完蛋。
我們的研究表明,所有類型的程式都會發送隨意的調試資訊(例如,MusicMatch Jukebox 就有一個嘮嘮叨叨的鍵盤鉤子),這些線程通過很少的幾行代碼就能停止住。沒有必要停止整個程式 - 可能還有其他的線程 - 但在實際中,開發人員不計劃使用 OutputDebugString() 將會是一條拒絕服務之路(譯者註:此句沒有完全明白,請參看原文)。
---
最奇怪的是,我們發現 OutputDebugString() 並非一個天然的 Unicode 函數。大多數的 Win32 API 具有“真正的”使用了 Unicode 的函數(“W” 版本),如果調用“A”版本的函數則它們自動從 ASCII 轉換到 UNICODE。
但是,因為 OutputDebugString 把在記憶體緩衝區中的資料最終是作為 ASCII 傳遞到調試器中的,它們具有相反於常規的 A/W 配對。這就暗示了如果要在 Unicode 程式裡發送一個快捷資訊到調試器,可以通過直接調用 “A” 版本來實現:
OutputDebugStringA("Got here to place X");
一個監視User層的OutputDebugString的簡單實現
VC知識庫BLOG-Coder Jozu-簡單的debugview
http://blog.vckbase.com/jozu/archive/2005/03/23/3900.aspx
sysinternals的dbgview想必大家都用過,我寫了一個簡單的,只監視user層OutputDebugString的輸出,原理都包含在代碼裡了,希望對你有協助,沒有的話就算了:P
// DebugView.cpp : 定義控制台應用程式的進入點。<br />//</p><p>#include "stdafx.h"</p><p>// DBMntor.cpp : Little dbgview。。。。<br />// Coder Jozu<br />//<br />#include <windows.h><br />#include <tchar.h><br />#include <stdio.h><br />#include <stdlib.h></p><p>#define PAGE_SIZE 4096</p><p>#define DBWIN_MAP"DBWIN_BUFFER"<br />#define DBWIN_WRITEVENT "DBWIN_BUFFER_READY"<br />#define DBWIN_READEVENT "DBWIN_DATA_READY"</p><p>typedef struct _tag_DBMap<br />{<br />DWORD dwProcessId;<br />CHAR szString[PAGE_SIZE - sizeof(DWORD)];<br />} DB_MAP, *PDB_MAP;</p><p>int main(int argc, char* argv[])<br />{<br />HANDLE hMap;<br />PDB_MAP pDBMap;</p><p>HANDLE hEventRead;<br />HANDLE hEventWrite;<br />HANDLE hMutex;</p><p>OutputDebugString( "Hello world" );</p><p>hMutex = CreateMutex(NULL, FALSE, "DBWinMutex");<br />if ( !hMutex )<br />{<br />printf( "CreateMutex NG. Err = %d", GetLastError() );<br />system("pause");<br />return 0;<br />}</p><p>hEventRead = CreateEvent(NULL, FALSE, FALSE, DBWIN_READEVENT);<br />if(!hEventRead)<br />{<br />printf( "CreateEvent(Read) NG. Err = %d", GetLastError() );<br />system("pause");<br />return 0;<br />}<br />// 已經有調試器在運行 自己退出<br />else if( ERROR_ALREADY_EXISTS == GetLastError() )<br />{<br />printf( "CreateEvent(Read) NG. ERROR_ALREADY_EXISTS" );<br />system("pause");<br />return 0;<br />}</p><p>hEventWrite = CreateEvent(NULL, FALSE, FALSE, DBWIN_WRITEVENT);<br />if(!hEventWrite)<br />{<br />printf( "CreateEvent(Write) NG. Err = %d", GetLastError() );<br />system("pause");<br />return 0;<br />}<br />// 已經有調試器在運行 自己退出<br />else if( ERROR_ALREADY_EXISTS == GetLastError() )<br />{<br />printf( "CreateEvent(Write) NG. ERROR_ALREADY_EXISTS" );<br />system("pause");<br />return 0;<br />}</p><p>hMap = CreateFileMapping(INVALID_HANDLE_VALUE,<br />NULL,<br />PAGE_READWRITE,<br />0,<br />PAGE_SIZE,<br />DBWIN_MAP);<br />if(!hMap)<br />{<br />printf( "CreateFileMapping NG. Err = %d", GetLastError() );<br />system("pause");<br />return 0;<br />}<br />// 已經有調試器在運行 自己退出<br />else if( ERROR_ALREADY_EXISTS == GetLastError() )<br />{<br />printf( "CreateFileMapping NG. ERROR_ALREADY_EXISTS" );<br />system("pause");<br />return 0;<br />}</p><p>pDBMap = (PDB_MAP)MapViewOfFile(hMap,<br />FILE_MAP_READ,<br />0,<br />0,<br />0);<br />if(!pDBMap)<br />{<br />CloseHandle(hMap);<br />system("pause");<br />return 0;<br />}</p><p>while(TRUE)<br />{</p><p>SetEvent(hEventWrite);<br />WaitForSingleObject(hEventRead, INFINITE);<br />//ResetEvent(hEventRead);// 如果CreateEvent的第二個參數為TRUE,則需手動置為nosignel</p><p>printf("%d==>%s/n", pDBMap->dwProcessId, pDBMap->szString);</p><p>}</p><p>system("pause");<br />return 0;<br />}<br />