時間旅行調試(TTD)允許用戶記錄跟蹤,這些跟蹤是對程序執行的記錄。時間線是執行過程中發生的事件的直觀表示,這些事件可以是包括斷點,內存讀/寫,函數調用和返回以及異常。
使用時間線窗口可以快速查看重要事件,了解相對位置并輕松跳轉到它們在TTD跟蹤文件中的位置,使用多個時間線以可視方式探索時間旅行軌跡中的事件并發現事件相關性。
打開TTD跟蹤文件時將顯示時間線窗口,并顯示關鍵事件,而無需手動創建數據模型查詢。同時,所有時間旅行對象均可用,以允許進行更復雜的數據查詢。有關創建和使用時間旅行跟蹤文件的更多信息,請點此查看。
時間線類型
時間線窗口可以顯示以下事件:
1.異常(你可以進一步過濾特定的異常代碼);
2.斷點(添加斷點時也會自動添加斷點的時間線);
3.函數調用(以module!function形式搜索);
4.內存訪問(在兩個內存地址之間進行讀/寫/執行);
將鼠標停在每個事件上,通過工具提示獲取更多信息。點擊事件將運行事件查詢并顯示更多信息,雙擊事件將跳至TTD跟蹤文件中的該位置。
異常
當你加載跟蹤文件并且時間線處于活動狀態時,它將自動顯示記錄中的任何異常。當你將鼠標懸停在斷點上時,將顯示諸如異常類型和異常代碼之類的信息。
你可以使用可選的異常代碼字段進一步過濾特定的異常代碼:
你還可以為特定的異常類型添加新的時間線。
斷點
添加斷點后,會將斷點的時間線自動添加到時間線。例如,可以使用bp Set Breakpoint命令完成此操作。當你將鼠標懸停在斷點上時,將顯示地址和與斷點關聯的指令指針。
清除斷點后,關聯的斷點時間線將自動刪除。
函數調用
你可以在時間線上顯示函數調用的位置。為此,就要以module!function的形式提供搜索,例如TimelineTestCode!multiplyTwo。你還可以指定通配符,例如TimelineTestCode!m*。
將鼠標懸停在函數上時,將顯示函數名稱、輸入參數及其值和返回值。此示例顯示緩沖區和大小,因為這些是DisplayGreeting!GetCppConGreeting的參數。

內存訪問
使用內存訪問時間線顯示何時已讀取或寫入特定范圍的內存,或在何處執行了代碼。起始地址和終止地址用于定義兩個存儲器地址之間的范圍。
將鼠標懸停在內存訪問項上時,將顯示值和指令指針。
使用時間線
當將鼠標懸停在時間線上時,一條垂直的灰色線將跟隨鼠標,藍色豎線表示跟蹤中的當前位置,點擊放大鏡圖標可放大和縮小時間線。
在頂部時間線控制區域中,使用矩形來平移時間線的視圖。拖動矩形的外部定界符以調整當前時間線視圖的大小。
鼠標移動
使用Ctrl +滾輪放大和縮小,使用Shift +滾輪可左右滾動。
時間線調試技術
為了演示調試時間線技術,此處重用了“時間旅行調試演練(Time Travel Debugging Walkthrough)”。本演示假設你已經完成了構建樣例代碼的前兩個步驟,并使用前面描述的前兩個步驟創建了TTD記錄。
第1部分:構建示例代碼;
第二部分:記錄“DisplayGreeting”示例的蹤跡;
在本示例中,第一步是在時間旅行跟蹤中查找異常,這可以通過雙擊時間線上唯一的異常來實現。
在命令窗口中,我們看到點擊異常時發出了以下命令。
(2dcc.6600): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: CC:0
@$curprocess.TTD.Events.Where(t => t.Type == "Exception")[0x0].Position.SeekTo()
選擇查看>>寄存器以在時間線上顯示寄存器,開始我們的調查。
在命令輸出中,請注意堆棧(esp)和基本指針(ebp)指向兩個截然不同的地址。這可能表明堆棧已損壞,可能是返回的函數損壞了堆棧。為了驗證這一點,我們需要返回到CPU狀態被損壞之前的狀態,并查看是否可以確定何時發生堆棧損壞。在此過程中,我們將檢查局部變量和堆棧的值。選擇查看>>當地,以顯示本地值,選擇查看>>堆棧以顯示代碼執行堆棧。
在跟蹤失敗時,通常會在錯誤處理代碼的真正原因之后結束幾個步驟。通過時間旅行,我們可以一次返回一條指令,找出真正的根本原因。
從主頁功能區中,使用“后退一步”命令后退三步指令。執行此操作時,請繼續檢查堆棧,本地變量和注冊窗口。
當你使用“后退一步”命令后,命令窗口將顯示時間行進位置和寄存器。
0:000> t-
Time Travel Position: CB:41
eax=00000000 ebx=00564000 ecx=c0d21d62 edx=7a1e4a6c esi=00061299 edi=00061299
eip=00540020 esp=003cf7d0 ebp=00520055 iopl=0? ? ? ? ?nv up ei pl zr na pe nc
cs=0023? ss=002b? ds=002b? es=002b? fs=0053? gs=002b? ? ? ? ? ? ?efl=00000246
00540020 ??? ? ? ? ? ? ? ???
0:000> t-
Time Travel Position: CB:40
eax=00000000 ebx=00564000 ecx=c0d21d62 edx=7a1e4a6c esi=00061299 edi=00061299
eip=00061767 esp=003cf7cc ebp=00520055 iopl=0? ? ? ? ?nv up ei pl zr na pe nc
cs=0023? ss=002b? ds=002b? es=002b? fs=0053? gs=002b? ? ? ? ? ? ?efl=00000246
DisplayGreeting!main+0x57:
00061767 c3? ? ? ? ? ? ? ret
0:000> t-
Time Travel Position: CB:3A
eax=0000004c ebx=00564000 ecx=c0d21d62 edx=7a1e4a6c esi=00061299 edi=00061299
eip=0006175f esp=003cf718 ebp=003cf7c8 iopl=0? ? ? ? ?nv up ei pl nz na pe nc
cs=0023? ss=002b? ds=002b? es=002b? fs=0053? gs=002b? ? ? ? ? ? ?efl=00000206
DisplayGreeting!main+0x4f:
0006175f 33c0? ? ? ? ? ? xor? ? ?eax,eax
在跟蹤時,我們的堆棧和基本指針具有更有意義的值,因此看來我們越來越接近代碼中發生損壞的位置。
esp=003cf718 ebp=003cf7c8
另一個有趣的地方是,本地窗口包含來自我們的目標應用程序的值,而源代碼窗口則高亮顯示在跟蹤中的源代碼中準備執行的代碼行。
為了進一步研究,我們可以打開一個內存窗口來查看堆棧指針(esp)內存地址附近的內容。在此示例中,其值為003cf7c8。選擇內存>>文本>> ASCII以顯示存儲在該地址的ASCII文本。
內存訪問時間線
確定了感興趣的內存位置后,使用該值添加內存訪問時間線。點擊+添加時間線,然后填寫起始地址。我們將查看4個字節,因此將其添加到003cf7c8的起始地址中,即得到003cf7cb。默認設置是查看所有內存寫入,但是你也可以僅查看該地址處的寫入或代碼執行。
現在,我們可以反向遍歷時間線,以檢查這段時間在此行的跟蹤記錄的哪一點被寫入,以查看可以找到的內容。點擊時間線上的此位置,我們看到本地人為要復制的字符串取不同的值。目標值似乎不完整,好像我們的字符串長度不正確。
斷點時間線
使用斷點是在某些情況下暫停代碼執行的常用方法,TTD允許你設置一個斷點并及時返回,直到在記錄跟蹤之后找到該斷點為止。在問題發生后檢查過程狀態,確定斷點的最佳位置的能力啟用了TTD特有的其他調試工作流。
要探索替代的時間線調試技術,請點擊時間線中的異常,然后使用Home功能區上的“后退一步”命令再次前進三步。
在這個示例中,僅查看代碼將非常容易,但是如果有數百行代碼和數十個子例程,則可以使用此處描述的技術來減少定位問題所需的時間。
如前所述,基本指針(esp)并非指向指令,而是指向我們的消息文本。
使用ba命令在內存訪問上設置斷點,我們將設置一個w - write斷點,以查看何時寫入此內存區域。
0:000> ba w4 003cf7c8
盡管我們將使用簡單的內存訪問斷點,但可以將斷點構造為更復雜的條件語句。有關更多信息,請參見bp,bu,bm(設置斷點)。
從“主頁”菜單中,選擇“返回”以返回到斷點之前的時間。此時,我們可以檢查程序堆棧以查看哪些代碼處于活動狀態。
由于Microsoft提供的wscpy_s()函數不太可能出現這樣的代碼錯誤,因此我們在堆棧中進行了進一步的研究。堆棧顯示Greeting!main調用Greeting!GetCppConGreeting。在這個非常小的代碼示例中,我們可以在此時打開代碼,并且很容易發現錯誤。但是,為了說明可以用于更大、更復雜的程序的技術,為此我們將設置添加一個函數調用時間線。
? ? ? ?函數調用時間線
點擊+添加時間線,然后填寫DisplayGreeting!GetCppConGreeting作為函數搜索字符串。
“開始”和“結束位置”復選框表示跟蹤中函數調用的開始和結束位置。
我們可以使用dx命令顯示函數調用對象,以查看關聯的TimeStart和TimeEnd字段,它們與函數調用的“開始位置”和“結束位置”相對應。
dx @$cursession.TTD.Calls("DisplayGreeting!GetCppConGreeting")[0x0]
? ? EventType? ? ? ? : 0x0
? ? ThreadId? ? ? ? ?: 0x6600
? ? UniqueThreadId? ?: 0x2
? ? TimeStart? ? ? ? : 6D:BD [Time Travel]
? ? SystemTimeStart? : Thursday, October 31, 2019 23:36:05
? ? TimeEnd? ? ? ? ? : 6D:742 [Time Travel]
? ? SystemTimeEnd? ? : Thursday, October 31, 2019 23:36:05
? ? Function? ? ? ? ?: DisplayGreeting!GetCppConGreeting
? ? FunctionAddress? : 0x615a0
? ? ReturnAddress? ? : 0x61746
? ? Parameters
必須選中“開始”或“結束”,或者同時選中“開始”和“結束”位置框。
由于我們的代碼既不是遞歸代碼也不是可重入代碼,因此調用GetCppConGreeting方法時,很容易在時間線上定位。對GetCppConGreeting的調用也與我們的斷點以及我們定義的內存訪問事件同時發生。因此,似乎我們已經縮小了代碼范圍,仔細研究了導致應用程序崩潰的根本原因。
通過查看多個時間線來探索代碼執行
盡管我們的代碼示例很小,但是使用多個時間線的技術可以對時間旅行軌跡進行可視化探索。你可以查看跟蹤文件以詢問問題,例如“何時在命中斷點之前訪問內存區域?”。
本文翻譯自:https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/windbg-timeline-preview