VC調試器進階應用程式----進階斷點篇
一.進階斷點文法
進階斷點文法由兩部分組成:1.上下文部分.2.位置,運算式,變數或Windows訊息條件.
用函數,源檔案和二進位模組來指定上下文,內容相關的表示方法:
{[函數],[源檔案],[二進位模組]}
必須指定唯一的,足夠的上下文資訊才能擷取斷點位置.如在TEST.CPP的20行設一位置斷點,文法為:{,TEST.CPP,}.20,如A.DLL或B.DLL都使用了該行,又只想在B.DLL的調用中觸發,則必須使用:{,TEST.CPP,B.DLL}.20.
VC調試器中可直接輸入上下文文法:Breakpoints對話方塊的Location選項卡BreakAt編輯框中.更容易的方法是使用BreatAt框右的箭頭開啟菜單,選擇Advanced項,然後在Context框中輸入斷點的相應資訊.
如想在一個絕對位址上中斷,直接在BreakAt框中輸入地址就行.
二.任何函數上快速中斷
將函數名輸入BreadAt框中.如果是C++代碼,同時還需要類限定符.支援重載了的函數,調試器會列出所有滿足條件的函數供選擇,如輸入時提供足夠的資訊,完全可略過選擇過程.如輸入: "CString::operator=(const char *) "可唯一確定要中斷的函數.
三.在系統或DLL輸出的函數中設定斷點
在程式中從DLL輸入的函數中設定一個斷點可能是毫無作用的,調試器需要知道在何處可以找到該函數上下文資訊,同時,函數名取決於是否載入了DLL的符號.只有在W2K以上版本中才能在系統DLL中設定斷點--原因在於其它系統沒有提供邊寫入邊禁止複製的功能,若一定要啟用這種方法,必須要有COFF(Common Object File Format),並在調試器中輸出啟動的裝載----在Options對話方塊的Debug頁,將Load COFF & Exports選中.
VC調試器用分級的符號資訊法,完整的符號的層級高於不太完整的.PDB(Program Database)檔案具有所有可能的源碼行,函數,變數和類型資訊,優先順序便高於COFF/DBG檔案,後者只有公用函數符號,而COFF/DBG檔案高於輸出名稱,輸入的名稱是一種偽符號.
調試時,如DEBUG視窗輸出:裝載DLL的符號,則說明符號已被裝入;否則說明沒有裝載DLL的符號.
沒有裝入符號時,使用的位置字串是DLL輸出的名稱,可能用DUMPBIN程式查看這個名稱:DUMPBIN /EXPORTS DLLname.例:在LoadLibraryA中設定中斷: "{,,Kernel32.dll}LoadLibraryA ".
如裝入了符號,則要根據輸出函數和調用協議來計算函數名.如上例,LoadLibraryA使用__stdcall調用協議,據該協議,函數名以底線為首碼,所跟有進棧的位元組數為尾碼的@號.一般說來,參數個數*4,就是參數佔用棧空間的總位元組數,LoadLibary的名稱便是:_LoadLibraryA@4,故最後的文法是:或 "{,,Kernel32.dll}_LoadLibraryA@4 "
附:常用的調用協議
1、__stdcall呼叫慣例相當於16位動態庫中經常使用的PASCAL呼叫慣例。在32位的VC++5.0中PASCAL呼叫慣例不再被支援(實際上它已被定義為__stdcall。除了__pascal外,__fortran和__syscall也不被支援),取而代之的是__stdcall呼叫慣例。兩者實質上是一致的,即函數的參數自右向左通過棧傳遞,被調用的函數在返回前清理傳送參數的記憶體棧,但不同的是函數名的修飾部分(關於函數名的修飾部分在後面將詳細說明)。
_stdcall是Pascal程式的預設調用方式,通常用於Win32 Api中,函數採用從右至左的壓棧方式,自己在退出時清空堆棧。VC將函數編譯後會在函數名前面加上底線首碼,在函數名後加上 "@ "和參數的位元組數。
2、C呼叫慣例(即用__cdecl關鍵字說明)按從右至左的順序壓參數入棧,由調用者把參數彈出棧。對於傳送參數的記憶體棧是由調用者來維護的(正因為如此,實現可變參數的函數只能使用該呼叫慣例)。另外,在函數名修飾約定方面也有所不同。
_cdecl是C和C++程式的預設調用方式。每一個調用它的函數都包含清空堆棧的代碼,所以產生的可執行檔大小會比調用_stdcall函數的大。函數採用從右至左的壓棧方式。VC將函數編譯後會在函數名前面加上底線首碼。是MFC預設呼叫慣例。
3、__fastcall呼叫慣例是“人”如其名,它的主要特點就是快,因為它是通過寄存器來傳送參數的(實際上,它用ECX和EDX傳送前兩個雙字(DWORD)或更小的參數,剩下的參數仍舊自右向左壓棧傳送,被調用的函數在返回前清理傳送參數的記憶體棧),在函數名修飾約定方面,它和前兩者均不同。
_fastcall方式的函數採用寄存器傳遞參數,VC將函數編譯後會在函數名前面加上 "@ "首碼,在函數名後加上 "@ "和參數的位元組數。
4、thiscall僅僅應用於“C++”成員函數。this指標存放於CX寄存器,參數從右至左壓。thiscall不是關鍵詞,因此不能被程式員指定。
5、naked call採用1-4的呼叫慣例時,如果必要的話,進入函數時編譯器會產生代碼來儲存ESI,EDI,EBX,EBP寄存器,退出函數時則產生代碼恢複這些寄存器的內容。naked call不產生這樣的代碼。naked call不是類型修飾符,故必須和_declspec共同使用。
關鍵字 __stdcall、__cdecl和__fastcall可以直接加在要輸出的函數前,也可以在編譯環境的Setting.../C/C++ /Code Generation項選擇。當加在輸出函數前的關鍵字與編譯環境中的選擇不同時,直接加在輸出函數前的關鍵字有效。它們對應的命令列參數分別為/Gz、/Gd和/Gr。預設狀態為/Gd,即__cdecl。
要完全模仿PASCAL呼叫慣例首先必須使用__stdcall呼叫慣例,至於函數名修飾約定,可以通過其它方法模仿。還有一個值得一提的是WINAPI宏,Windows.h支援該宏,它可以將出函數翻譯成適當的呼叫慣例,在WIN32中,它被定義為__stdcall。使用WINAPI宏可以建立自己的APIs。
四.位置斷點修飾符
1.跳躍計數.
功能是執行斷點但不在斷點處停止,直到執行完了一個特定的次數為止.
使用中首先設定一個標準的位置斷點,開啟BreadPoint對話方塊,選中該斷點,單擊Condition,然後在彈出的對話方塊最下面的編輯控制項中輸入次數.
只有當程式全速運行時,未執行的迴圈次數才有用.逐步執行跨過斷點時不會更新跳躍計數.
例:已知迴圈可能崩潰,但不清楚在哪次迴圈時,輸入遠遠大於總迴圈次數的跳躍計數修飾符,則在崩潰時可開啟Breakpoint框,其中將列出還未執行的迴圈次數,與總次數相減就可得已執行的次數.
2.條件運算式.
只有運算式為真時觸發.Breakpoint框Condition按鈕,選第一個編輯框,輸入運算式即可.規則:
.只可使用C類型比較子.
.運算式中不能調用任何函數.
.運算式中不能包含任何宏值.
運算式為@TIB=Thread Infomation Block Linear Address,則程式只在該特定線程中才會中斷.例:線程@TIB地址值為0E000,則輸入 "@TIB==0xE000 ",則在切換到該線程時中斷.對W98,可用@FS=thread specific value.
如在某特定錯誤後中斷,則可用@ERR,如 "@ERR=2 "表示在最後錯誤為ERROR_FILE_NOT_FOUND.除@CLK外,所有可在WATCH視窗中使用的偽寄存器均可用於條件運算式.
條件運算式可與跳躍斷點組合使用.
3.變數更改
在變數更改時中斷程式.只有當位置斷點執行時才能檢查變數.常用用調用棧高層的函數中發現出錯,需要深入調用棧,壓縮範圍找出根源時.
添加時在Breakpoint框第一個編輯框中輸入變數名(可以是指標指向聽對象:*p),在第二個編輯框中輸入要查看的項目數量.
五.全域運算式和條件斷點.
調試器可監控某一地址和該地址上的1,2或4位元組的內容.如可用硬體調試寄存器,則不影響速度;否則程式將逐步執行ASM指令並在每一步中檢查條件,這將嚴重影響程式運行速度.
總共有4個調試寄存器.硬體調試寄存器不能處理超過1個雙字長的引用.確保利用硬體調試寄存器的最好方法是使用運算式和資料已變更位元置的實際地址值.例如:g_szGlobal是全域數組指標,地址為0x5000,則在Breakpoint對話方塊中DATA選項卡中將運算式斷點設為 "*(char*))0x5000== 'G ' ",但如果寫為 "WO(0x5000)== 'G ',則用不到硬體調試寄存器,會逐步執行每條指令.
與全域運算式斷點類似,使用變數的16進位地址給定長指標計算地址,並將要查看的單元數設為1,則全域變數斷點可發揮最付佳功效.如上例要在變數改動時中斷,則輸入: "*(long*)0x5000 ".
六.WINDOWS訊息斷點.
Breakpoint框的Message頁.需要指定一個視窗過程,注意:MFC世界中AfxWndProc是多數視窗的一個視窗過程,所以總會在該斷點處中斷.
較好的方法是在CWnd::WindowProc中設定一個條件位置斷點.首先尋找需要的類的this指標,然後在WINUSER.h中尋找訊息的實際數值,最後就可設條件斷點如下所示:
{,WINCORE.CPP,}.1500 when (this==0x12345678)&&(message==oxF) //將在0x12345678視窗的WM_PAINT訊息時中斷.
this指標可能會因類的分配方式和對代碼的更改而變,通常可用類嚮導添加一個處理器方法,且只需設定一個簡單的位置斷點.
七.技巧.
1.可在源碼行和Disassembly視窗中設定位置斷點,且可在Call Stack宣傳品中設定----右擊要中斷的函數,選Insert/Remove Breakpoint,可以一返回到該函數就中斷.
2.調試結束後不必刪除斷點,而是將它們設定為無效.方法是右擊斷點選擇無效項或在斷點對話方塊關閉選中標記.這樣的話,可在以後需要時隨時斷點的狀態.
3.為方便地設定所有斷點,在設定任何進階斷點之前,單擊Step Into來擷取被調試的程式.僅當被調試的程式是活動的,調試器才能校正所設定的斷點,顯示Resolve Ambiguity對話方塊.調試器可協助更快地設定斷點,並確認它們是正確的.
VC調試器進階應用程式----WATCH視窗篇
一.格式化資料和運算式指派陳述式.
常用變數格式化符(運算式的值後跟逗號,接格式化符,如 "(int)0xFFFF,d "):
d,I:有符號的十進位數.
u :無符號的十進位數.
o :無符號的八
x,X:十六進位數.
l,h:d,i,u,o,x,X的長首碼或短首碼.
f :有符號浮點數.
e :有符號的科學計數法.
g :有符號的浮點或有符號的科學計數法,用其中較短的一個.
c :單字元.
s :字串.
su :雙位元組字元串.
st :雙位元組字元串或ANSI字串,取決於AUTOEXP.DAT中的Unicode String設定.
hr :Windows類標記.
wm :Windows訊息碼.
常用記憶體轉儲對象的格式化符(用法同變數格式化符):
ma :64個ASCII碼字元.
m :以16進位書寫的16位元組,後跟16個ASCII字元.
mb :以16進位書寫的16位元組,後跟16個ASCII字元.
mw :8個字長.
md :4個雙精確度字.
mq :4個四倍字長的字.
mu :2位元組字元(Unicode標準).
# :將指標擴充到指定的數值數目的記憶體儲存單元上.(#代表一個數字)
WATCH視窗允許重新設定資料變數的格式,
如:可用BY,DW運算式來定位指標的位移量;
可用&和*運算子,且兩運算子都可直接操作記憶體位址;
甚至可用上下說明符明確指定變數的上下文.
總之,所有格式化方法和指定方法在WATCH視窗都有效
WATCH視窗是一個完整的運算式求值程式,可以在其中查看任何條件陳述式.
運算式中可用的偽寄存器(可當普通變數進行查看):
@ERR:最後一個錯誤值,GetLastError API返回相同的值.
@TIB:當前線程的線程資訊塊.(調試器不能處理 "FS:0 "格式).
@CLK:時鐘寄存器.
@EAX,@EBX,@ECX,@EDX,@ESI,@EDI,@DIP,@ESP,@EBP,@EFL
:Intel CPU寄存器.
@CS,@DS,@ES,@SS,@FS,@GS
:Intel CPU段寄存器.
@ST0,@ST1,@ST2,@ST3,@ST4,@ST5,@ST6,@ST7
:Intel CPU浮點寄存器.
二.適時編碼
許多時候只想對兩斷點間的執行時間有個大致印象,可用@CLK得出兩斷點間所需執行時間(包括調試器佔用的時間).
需要輸入兩個@CLK觀察符,第一個是@CLK,第二個是@CLK=0.第二個的目的是重新運行時將定時器清0.
時間以微秒為單位,大多數情況下需要格式化為毫秒: "@CLK/1000,d ".
三.在WATCH視窗中調用函數
大多數情況下用於執行專門編寫的校正資料結構,保證資料的相關性的函數.在釋放構件中,從未調用過的函數不會被連結,因此不必擔心這類函數會對影響發布構件.
如函數沒有參數,也要求使用括弧 "() ",調用時像用普通函數一樣傳送參數.WATCH右邊將顯示函數傳回值.
這裡有些限制:
1.只能在一個單線程上下文中執行函數.如是多線程程式,將函數輸入到WATCH視窗中檢查結果後應立即從WATCH視窗清除,否則,如調試函數在第二個線程上下文中執行,會立即終止第二個線程的運行.
2.調試函數必須在20秒內執行.如執行過程中出現異常,程式會在調試器中中止.
3.(常識)只對資料驗證進行記憶體讀取,如有問題,調用OutputDebugString類的函數.如更改記憶體或調用API函數----儘管這是可能的,但無法預知可能會發生什麼.
只要在WATCH視窗中重新計算運算式,已輸入WATCH視窗的調試函數就會執行:
.程式處於運行狀態並觸發某一斷點時.
.單步調試某一程式碼或某一指令時.
.在WATCH視窗左邊編輯完成調試函數的文本並按下斷行符號時.
.在運行程式時出現異常情況,並讓你返回調試器中時.
使用調試函數的建議:輸入調試函數並查看值後,立即從WATCH視窗清除;只為最關鍵的資料結構編寫調試函數;不要更改個別結構的轉儲內像.
四.自動擴充自己的類型
常見的自動擴充是RECT,輸入RECT型的變數後直接顯示其中的某些資料成員的值.
自訂類型擴充時,只需將自己的類型入口加入 <VS Common> /MSDev98/Bin目錄的AUTOEXP.DAT檔案中.
例:
擴充CreateProcess()所用到的PROCESS_INFORMATION結構
1.檢查調試器將該類型識別為什麼.將PROCESS_INFORMATION變數輸入WATCH視窗,右擊變數,選擇Properties,在這裡它被標註為_PROCESS_INFORMATION類型.
2.開啟AUTOEXP.DAT文字檔,加入擴充入口.文法如下:
Type=[text] <member[format]>
本例中要查看hProcess和hThread值,故輸入:
_PROCESS_INFORMATION=hProcess= <hProcess,X> hThread= <hThread,X>
其中X表示以16進位查看.有個特殊的格式化符 <,t> ,用於通知調試器輸入最易衍生類別型的類型名.如B派生至A,只有B有自動擴充規則,則B的自動擴充將會是後面跟隨著類A的自動擴充規則的類型名B.
五.Set Next Statement命令
可以在調試時從菜單運行,但也可在WATCH視窗中直接設定EIP寄存器----小心,可能很容易摧毀程式.在最佳化的釋放構件中,最安全的方法是在Disassembly視窗中使用該命令.如代碼在堆棧上建立了臨時變數,更要多加小心.
最常用的情況是:在出問題的函數前設定一個斷點,檢查進入的參數,單步調試整個函數;如問題不是重複的,使用Set Next Statement設定返回到斷點的執行點,並更改參數.這樣可在一個偵錯工作階段中測試多個假設,節省測試時間,但它不能用於所有場合,因為函數執行會破壞其狀態.
另一個常用地點是測試時填充資料結構,如表和數組,可用它輸入額外的資料並查看代碼如何處理--當某些資料條件難於複製時更為方便.