Time Travel Debugging — להקליט ולהריץ אחורה באגים שלא משתחררים באפליקציות ארוכות-ריצה
· עודכן בתאריך: · Go Komura · WinDbg, Time Travel Debugging, Debugging, חקירת באגים, Windows, פיתוח Windows, .NET, C++, ייעוץ טכני
היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 2 Sep 2026)
- פרסום ראשון
“זה קורס פעם בחודש, רק באמצע הלילה.” “אותה פעולה אף פעם לא משתחררת אצלי.” “קיבלנו dump, אבל הסתכלות על אתר הקריסה לא אומרת למה הערך הפך למה שהוא.” בין חקירות הבאגים של אפליקציות Windows ארוכות-ריצה, אלה המקרים שצורכים הכי הרבה זמן. במאמר קודם, “קריאת crash dumps עם WinDbg + SOS,” הסתכלנו איך לקרוא crash dump, שהוא תצלום בודד. המאמר הזה ממשיך משם ומכסה את הכלי למצבים שבהם תצלום אינו מספיק: Time Travel Debugging (TTD).
TTD היא יכולת של WinDbg שמתעדת את כל הביצוע של process ומאפשרת replay מאוחר יותר, קדימה ואחורה. במקום לנסות שוב ושוב לשחזר באג, אפשר “להריץ אחורה” את session הדיבאגר.1 הקוראים המיועדים הם מפתחים ומתחזקים של אפליקציות Windows, .NET ו-C++ שכבר עשו את חלקם בחקירת dump ולוגים ועדיין יש להם מקרים שאינם מגיעים לשורש. סביבת התנאי המוקדם היא Windows 10/11 או Windows Server 2016 ואילך עם WinDbg הנוכחי, וההקלטה דורשת הרשאות Administrator.12 רמת הקושי היא ביניים.
הנחות המאמר
| פריט | תוכן |
|---|---|
| קוראים מיועדים | מפתחים ומתחזקים של אפליקציות Windows עם באגים ארוכי-ריצה או לסירוגין ש-dumps ולוגים אינם מגיעים לשורש שלהם |
| ידע מוקדם | ניסיון בפתיחת dump ב-WinDbg והרצת !analyze -v או !clrstack. תוכן מאמר אנליזת SOS מונח |
| סביבת תנאי מוקדם | Windows 10/11 או Windows Server 2016/2019/2022/2025, WinDbg (גרסה נוכחית), TTD.exe, הרשאות Administrator2 |
| מחוץ להיקף | שילוב עם Visual Studio Enterprise Snapshot Debugger; kernel mode (TTD הוא user mode בלבד3) |
1. קודם המסקנה
- Dump שומר “מצב”; TTD שומר את “הנתיב.” התיעוד הרשמי מציין ש-dumps נוטים להחמיץ את המצב ואת נתיב הביצוע שהובילו לכשל.1 אם תצלום של רגע הקריסה אינו אומר את הסיבה, מה שצריך אחר כך אינו עוד תצלומים אלא הקלטה.
- ההקלטה כבדה. בזמן הקלטה, תהליך היעד רץ פי 5 עד 20 לאט יותר או גרוע מזה, וקובץ ה-trace גדל ב-5 עד 50 MB לשנייה כשהתהליך פעיל, בלי תקרה.24 זה אינו כלי שמחברים ללא תנאי לאפליקציה ארוכת-ריצה.
- לאפליקציות ארוכות-ריצה, מתכננים “מה להקליט.” פרק 5 מכסה את ארבע נקודות הכניסה ש-TTD.exe מספק:
-ring/-maxFile(לשמור רק את N ה-MB האחרונים),-module(להקליט רק בזמן שמודול שלכם רץ),-recordmode Manual(לתת לאפליקציה לציין את מרווח ההקלטה), ו--monitor(להקליט כל הפעלה).2 - Replay סובב סביב שלושה דברים: positions (
!tt), events (dx @$curprocess.TTD.Events), ושאילתות (TTD.Calls/TTD.Memory). משלביםba(breakpoint מסוג break-on-access) עםg-(ביצוע לאחור), והדיבאגר עונה ישירות “מי כתב אחרון את הערך הזה?”5 - Trace מכיל את תוכן הזיכרון. הוא יכול לכלול מידע אישי וסודי כמו נתיבי קבצים, נתוני Registry ותוכן של זיכרון וקבצים.1 מתכננים שיתוף ואחסון כמו לקובץ סודי.
- יודעים מה TTD אינו יכול לפני שמתחילים. הוא אינו יכול להקליט kernel mode, אינו יכול להזריק ל-protected processes (PPL), אינו יכול לנתק את עצמו אחרי attach, ואי אפשר לשנות זיכרון בזמן replay.32
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 32, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
2. איפה dump אינו מספיק
Crash dump הוא העתק של זיכרון ו-registers ברגע שהתהליך קרס (או נעצר). כפי שמתואר במאמר אנליזת SOS, !clrstack אומר איפה זה קרס ו-!dumpheap -stat אומר מה צורך את ה-heap. מה שהוא אינו יכול להגיד הוא מה קרה בדרך למצב הזה.
flowchart TB
accTitle: מה dump לוכד ומה TTD לוכד
accDescr: Crash dump לוכד רק את המצב ברגע הקריסה, לא את הנתיב שהוביל לשם. TTD שומר את כל ביצוע ההוראות מתחילת ההקלטה עד הסוף, כך שהנתיב נשמר יחד עם המצב
dump["Crash dump: מצב ברגע הקריסה"] --> q1["לא מראה למה הערך הפך למה שהוא"]
ttd["TTD trace: ביצוע הוראות של מרווח ההקלטה"] --> q2["אפשר לחזור ל-position שבו הערך נכתב"]
q1 -.-> gap["הפער הזה הוא מה שגורר חקירות ארוכות-ריצה"]
איור 1: Dump הוא מצב; TTD הוא הנתיב. בבאגים ארוכי-ריצה, מה שרוצים בדרך כלל הוא האחרון.
מקרים טיפוסיים נראים כך.
- שדה אחד של מבנה ב-heap מחזיק ערך בלתי אפשרי. ה-dump מראה את הערך המשובש, אבל לא מי כתב אותו או מתי.
- מיקום ה-exception ידוע, אבל לא למה הארגומנט שהועבר אליו היה לא תקין. כשעולים בקוראים, הפונקציה שייצרה את הערך בדרך כבר אינה ב-stack.
- Handles או זיכרון גדלים לאורך חודש. Dump בודד יכול רק לומר “זה גדל”; נתיב הקריאה שהגדיל אותו אינו שם.
flowchart TB
accTitle: מה dump מחמיץ במקרים טיפוסיים ארוכי-ריצה
accDescr: הערך המשובש נלכד אבל לא מי כתב אותו, מיקום ה-exception נלכד אבל הפונקציה שייצרה את הארגומנט כבר אינה ב-stack, וגידול המשאב נלכד אבל לא הנתיב שהגדיל אותו; שלושת סוגי המקרה חולקים את זה
c1["שדה משובש"] --> m1["אין רישום מי כתב אותו או מתי"]
c2["Exception על ארגומנט לא תקין"] --> m2["הפונקציה שייצרה את הערך כבר אינה ב-stack"]
c3["משאב שגדל לאורך חודש"] --> m3["אין רישום של נתיב הקריאה שהגדיל אותו"]
m1 --> same["נקודה משותפת: יש מצב, אבל אין נתיב"]
m2 --> same
m3 --> same
איור 2: לכל שלושת המקרים יש “מצב אבל אין נתיב.” עוד תצלומים אינם ממלאים את הנתיב.
התיעוד של Microsoft משווה את החוזקות והחולשות של שיטות החקירה כך.1
| שיטה | חוזקות | חולשות |
|---|---|---|
| Live debugging | אינטראקטיבי, מראה את זרימת הביצוע, ומאפשר לשנות מצב | עוצר את עבודת המשתמש. דורש מאמץ לשחזר שוב ושוב. לעתים קרובות לא שמיש בייצור. קשה לחזור מנקודת הכשל לסיבה |
| Dumps | אין צורך בשינויי קוד מראש. חודרנות נמוכה, אפשר לאסוף ב-trigger. overhead כמעט אפס כשלא בשימוש | גם עם snapshots רצופים, התצוגה של “זמן שחלף” גסה |
| Telemetry ולוגים | קל. קשור לתרחישים עסקיים | אין לוגים בנתיבי קוד בלתי צפויים. עומק נתונים לא מספיק, ומוטמע סטטית בקוד |
| TTD | חזק בבאגים מורכבים. אין צורך בשינויי קוד מראש. אפשר replay offline כמה פעמים ומתעד הכול | overhead גדול בזמן הקלטה. עשוי לאסוף יותר נתונים מהנחוץ. קבצים גדלים |
יש עוד מאפיין חשוב שה-walkthrough הרשמי של TTD מצביע עליו. כשהדיבאגר נעצר בנקודת הכשל, הנקודה הזו לעתים קרובות נמצאת בתוך קוד טיפול בשגיאות כמה צעדים אחרי הסיבה האמיתית.5 Dump תמיד נלקח ב-position “כמה צעדים אחר כך” הזה. עם TTD אפשר ללכת אחורה משם הוראה אחת בכל פעם.
flowchart TB
accTitle: הפער בין נקודת הכשל לסיבה האמיתית
accDescr: נקודת הכשל שבה נלקח dump לעתים קרובות נמצאת בתוך טיפול בשגיאות כמה צעדים אחרי הסיבה האמיתית, ועם TTD אפשר להריץ אחורה מהנקודה הזו הוראה-הוראה בחזרה לסיבה
cause["סיבה אמיתית (ההוראה ששיבשה את הערך)"] --> steps["כמה צעדים קדימה"]
steps --> fail["נקודת כשל (exception, טיפול בשגיאה)"]
fail -->|"Dump"| photo["קפוא כאן"]
fail -->|"TTD"| back["חוזרים עם p- / t- / g-"]
back --> cause
איור 3: Dump קפוא בנקודת הכשל. TTD יכול ללכת אחורה מנקודת הכשל לסיבה.
3. איך TTD עובד ומה הוא עולה
3.1 מה מוקלט
TTD מזריק מנוע הקלטה לתהליך היעד ומתעד את ההוראות שבוצעו, הוראה-הוראה. במילות התיעוד הרשמי, הוא “מקודד trace ברמת הוראה מלא בערך פחות מבית אחד להוראה”; בפועל זה נוחת איפשהו בין bit אחד לבית אחד להוראה. תוכניות שמבצעות מעט סוגי פונקציות ומטפלות במעט נתונים מייצרות traces קטנים יותר; ההפך מייצר גדולים יותר.24
הקלטה מייצרת שני קבצים.1
| קובץ | תפקיד | גודל משוער |
|---|---|---|
.run |
ה-trace עצמו. שומר את ביצוע ההוראות בזמן ההקלטה | גדל ב-5 עד 50 MB לשנייה כשפעיל. אינו גדל כש-idle4 |
.idx |
ה-index. נתוני עזר שמאפשרים ל-WinDbg replay ושאילתות זיכרון ביעילות. נוצר כשההקלטה נעצרת, וגם נוצר אוטומטית כש-WinDbg פותח את קובץ .run |
פי 1 עד 2 מגודל ה-trace4 |
flowchart TB
accTitle: הזרימה מהקלטת TTD ל-replay
accDescr: מנוע הקלטה מוזרק לתהליך היעד וביצוע הוראות נרשם לקובץ .run; כש-WinDbg פותח את קובץ .run הוא יוצר index מסוג .idx ומריץ קדימה ואחורה באמצעות positions, events ושאילתות
proc["תהליך יעד"] --> inj["מנוע הקלטה מוזרק (TTDRecordCPU)"]
inj --> run[".run (רישום ביצוע הוראות)"]
run --> open["פתיחה ב-WinDbg"]
open --> idx["יצירת .idx (index)"]
idx --> play["Replay קדימה ואחורה עם positions, events ושאילתות"]
איור 4: ההקלטה נעשית על ידי TTD.exe או WinDbg; הקריאה נעשית על ידי WinDbg. שיתוף רק של קובץ .run מספיק.
הזמן בתוך קובץ .run מבוטא כ-“position.” הוא לובש צורה של שני מספרים הקסדצימליים מופרדים בנקודתיים, כמו 12:0 או 1A0:12F; החצי הראשון הוא מספר הרצף (שמתאים לאירוע רצף) והחצי השני הוא מספר ההוראות המשוער מאז אותו אירוע.6 FFFFFFFFFFFFFFFE:0 פירושו סוף ה-trace.7 Positions תופסים את הבמה בפרק 6.
flowchart TB
accTitle: איך positions ב-trace מבוטאים
accDescr: Position הוא מספר רצף הקסדצימלי וספירת צעדים מופרדים בנקודתיים; ההתחלה קרובה ל-0, הסוף מבוטא כ-FFFFFFFFFFFFFFFE:0, ואפשר גם לעבור ל-position משוער לפי אחוז
pos["Position xx:yy (הקסדצימלי)"] --> seq["xx: מספר רצף"]
pos --> step["yy: הוראות מאז אותו אירוע"]
seq --> tail["הסוף הוא FFFFFFFFFFFFFFFE:0"]
step --> pct["אחוזים כמו !tt 50 גם עובדים"]
איור 5: Position הוא “מספר אירוע:ספירת הוראות.” זה אינו זמן שעון קיר, אבל אפשר להמיר לזמן שעון קיר כפי שמוצג בפרק 6.
3.2 העלות
התיעוד הרשמי עצמו מתאר TTD כ-“invasive technology.”2 הנה העלות במספרים.
| פריט | תוכן |
|---|---|
| מהירות | תהליך היעד רץ פי 5 עד 20 לאט יותר או גרוע מזה בזמן הקלטה (תלוי באפליקציה ובאפשרויות ההקלטה). זה עשוי לא להיות מורגש ב-UI, אבל זה מורגש בפעולות כבדות כמו דיאלוג Open File23 |
| גידול קובץ | 5 עד 50 MB לשנייה כשפעיל. כמה דקות הקלטה יכולות להגיע לכמה GB. אין תקרה4 |
| דיסק נגמר | אם הדיסק מתמלא בזמן הקלטה, TTD כותב את העמוד האחרון ואז למעשה ממתין עד שיוכל לכתוב שוב. WinDbg ממשיך להציג את דיאלוג ההקלטה ואינו פולט לא שגיאה ולא אזהרה. התוצאה היא trace לא שלם4 |
| זיכרון | ההקלטה מוסיפה את ה-overhead של virtual CPUs לזיכרון תהליך היעד (ברירת מחדל 55 ב-x64/ARM64, 32 ב-x86). מקטינים עם -numVCpu רק כשחסר זיכרון2 |
| לא ניתן לנתק | אחרי attach, TTD אינו יכול לנתק את עצמו. כשמסיימים הקלטה, סוגרים את האפליקציה או מסיימים את התהליך. אם התהליך חיוני למערכת, צריך reboot ל-OS2 |
flowchart TB
accTitle: עלות ההקלטה והשפעתה על אפליקציות ארוכות-ריצה
accDescr: ההאטה בזמן הקלטה, גידול הקובץ, ההמתנה השקטה כשהדיסק נגמר, ואי-היכולת לנתק אחרי attach הם ארבע העלויות שהופכות לבלתי אפשרי לחבר TTD ללא תנאי לאפליקציה ארוכת-ריצה
cost["הקלטת TTD"] --> slow["פי 5 עד 20 לאט יותר"]
cost --> grow["גדל 5 עד 50 MB לשנייה, אין תקרה"]
cost --> disk["ממתין בשקט כשהדיסק נגמר"]
cost --> stuck["לא ניתן לנתק אחרי attach"]
slow --> no["הקלטה רציפה ללא תנאי אינה מעשית"]
grow --> no
disk --> no
stuck --> no
איור 6: כל אחת מארבע העלויות שוללת הקלטה רציפה. לכן נחוץ “תכנון ההקלטה” של פרק 5.
3.3 מה הוא אינו יכול
- User mode בלבד. אפשר להקליט רק את הביצוע ב-user mode של process; קוד שרץ ב-kernel mode, כמו drivers, אינו ניתן לדיבוג.3
- Protected processes. TTD אינו יכול להזריק את עצמו ל-Windows protected processes כמו Protected Process Light (PPL).3
- Replay הוא לקריאה בלבד. אפשר לחזור בזמן, אבל אי אפשר לשנות היסטוריה. פקודות שקוראות זיכרון עובדות; פקודות שמשנות אותו לא.3
- אי-תאימות עם אנטי-וירוס ותוכנות ניטור זיכרון. כי TTD עושה hook לתהליך, הוא מתנגש עם תוכנה שעוקבת או מצלה קריאות זיכרון מערכת. אם ההקלטה מייצרת שגיאה שנראית כמו הרשאות לא מספיקות, מכבים זמנית תוכנה כזו כדי לבודד. גם Electron framework הוא התנגשות ידועה; גם אם הקלטה מצליחה, תהליך היעד עלול deadlock או לקרוס.3
- אפליקציות UWP אינן ניתנות להפעלה ולהקלטה (attach לאפליקציית UWP שכבר רצה אפשרי). גם “תהליכים חריגים” שרצים ב-session אחר או בהקשר אבטחה שונה אינם נתמכים כרגע.8
flowchart TB
accTitle: מה TTD אינו יכול להקליט או לעשות
accDescr: קוד kernel-mode, protected processes, הקלטת הפעלה של אפליקציות UWP, ותהליכים ב-session או הקשר אבטחה אחר אינם ניתנים להקלטה; אי אפשר לשנות זיכרון בזמן replay; ואנטי-וירוס ו-Electron יכולים להתנגש
no["מגבלות TTD"] --> g1["לא ניתן להקליט"]
no --> g2["מגבלות והתנגשויות"]
g1 --> k["קוד kernel-mode (drivers וכו')"]
k --> ppl["Protected processes (PPL)"]
ppl --> uwp["הקלטת הפעלה של UWP (attach אפשרי)"]
uwp --> sess["Sessions והקשרי אבטחה אחרים"]
g2 --> ro["Replay לקריאה בלבד"]
ro --> av["עשוי להתנגש עם אנטי-וירוס ו-Electron"]
איור 7: המגבלות באות בשני סוגים: “לא ניתן להקליט” ו-“התנגשויות.” האחרון צריך בידוד לפי סביבה.
הפריט האחרון מכשיל כל מי שרוצה להקליט service. התיעוד של TTD.exe מתאר -attach כמיועד ל-“חקירת services ואפליקציות ארוכות-ריצה” ו--monitor כמקליט “בכל פעם שתוכנית או service עולה”,2 מה שאינו מסתדר פשוט עם האמירה בדף ה-troubleshooting. בפועל, הגישה הבטוחה היא לאמת לפי סביבה בסדר הזה: מאשרים שאפשר להקליט ping.exe או cmd.exe באותה תצורה כמו הייצור, ואז מנסים את תהליך היעד.8
4. הקלטה — ה-UI של WinDbg ו-TTD.exe
יש שתי נקודות כניסה להקלטה: ה-UI של WinDbg, או TTD.exe בשורת הפקודה.
4.1 הקלטה מ-UI של WinDbg
מריצים WinDbg כמנהל (elevation חובה ל-TTD1), בוחרים File > Start debugging > Launch executable (advanced), מציינים את קובץ ההרצה, ומסמנים Record with Time Travel Debugging. בחירת Configure and Record מאפשרת להגדיר את מיקום קובץ ה-trace ו-Record subset of execution (להגביל את המודולים המוקלטים ברשימה מופרדת בפסיקים כמו notepad.exe,kernelbase.dll). לתהליך שכבר רץ, בוחרים File > Start debugging > Attach to process ומסמנים באותו אופן Record Process with Time Travel Debugging.9
בזמן הקלטה מופיע דיאלוג קטן עם כפתורי “Stop and Debug” ו-“Cancel”. כשהאפליקציה יוצאת (או קורסת), ה-trace נסגר ו-WinDbg פותח אותו אוטומטית ובונה את ה-index.5
flowchart TB
accTitle: זרימת ההקלטה מ-UI של WinDbg
accDescr: ב-WinDbg שהופעל כמנהל, בוחרים Launch executable (advanced) או Attach to process, מסמנים Record with Time Travel Debugging, מגדירים מיקום שמירה וסינון מודול ב-Configure and Record, עוברים בדיאלוג ההקלטה, וכשהאפליקציה יוצאת ה-trace נסגר ומאונדקס אוטומטית
adm["מפעילים WinDbg כמנהל"] --> pick["Launch executable (advanced) / Attach to process"]
pick --> chk["מסמנים Record with Time Travel Debugging"]
chk --> cfg["Configure and Record: מיקום שמירה, סינון מודול"]
cfg --> rec["דיאלוג הקלטה (Stop and Debug)"]
rec --> fin["האפליקציה יוצאת, ה-trace נסגר ומאונדקס אוטומטית"]
איור 8: הקלטה מה-UI לוקחת חמישה שלבים. מדלגים על “הפעלה כמנהל” ונעצרים בדיאלוג הראשון.
4.2 הקלטה עם TTD.exe
כשצריך להקליט במחשב שבו אי אפשר להתקין WinDbg, או לאוטומציה של הקלטה, משתמשים ב-TTD.exe לבדו. הוא מותקן דרך App Installer מ-https://aka.ms/ttd/download, ואחרי ההתקנה אפשר לאמת עם ttd.exe -help. לסביבות offline, Microsoft גם מספקת נוהל רשמי (עם סקריפט PowerShell) לפירוק חבילת MSIX ידנית וחילוץ רק הבינאריים.2 ההקלטה דורשת הרשאות Administrator ובדרך כלל רצה מ-command prompt של מנהל.2
יש שלושה מצבי הקלטה.2
flowchart TB
accTitle: שלושת מצבי ההקלטה של TTD.exe
accDescr: launch מפעיל תהליך חדש עם ארגומנטים ומקליט אותו, אבל הוא רץ עם הרשאות מוגבהות. attach מתחבר לתהליך רץ לפי PID. monitor מקליט בכל פעם שהתוכנית שצוינה עולה, וההפעלה קורה עם הרשאות רגילות
m["מצבי הקלטה של TTD.exe"] --> l["-launch: הפעלה והקלטה"]
m --> a["-attach: attach ל-PID רץ"]
m --> mo["-monitor: הקלטה בכל הפעלה"]
l -.-> lp["מופעל עם הרשאות Administrator"]
a -.-> ap["שומר הרשאות רגילות"]
mo -.-> mp["נתיב הפעלה רגיל, מתאים לאוטומציה"]
איור 9: רק -launch יכול להעביר ארגומנטים, אבל הוא רץ עם הרשאות מוגבהות. כדי להקליט התנהגות קרובה לייצור, משתמשים ב--attach או -monitor.
:: מפעילים ומקליטים (מצב ברירת מחדל; אפשר להשמיט -launch)
TTD.exe -out C:\traces MyApp.exe --config prod.json
:: מצמידים לתהליך שרץ (יוצרים קודם את תיקיית הפלט)
TTD.exe -attach 21440 -out C:\traces\MyApp.run
:: מקליטים בכל הפעלה (Ctrl+C מסיים את הניטור; -out דורש נתיב מלא)
TTD.exe -out C:\traces\ -monitor MyApp.exe
-launchהוא המצב היחיד שיכול להעביר ארגומנטים, אבל התוכנית מופעלת עם אותן הרשאות (Administrator) כמו TTD.exe. לאפליקציות שההתנהגות שלהן משתנה עם הרשאות, מקליטים עם-attachאו-monitorכדי לשמור הרשאות רגילות.2-attachדורש שתיקיית הפלט כבר קיימת. אם מציינים שם קובץ, לא יכול להיות קובץ בשם הזה.2-monitorמתקין driver לניטור הפעלת תהליכים ומקליט בכל פעם שהתוכנית שצוינה (אפשר יותר מאחת) עולה. הוא נשאר בתוקף עד reboot ונעצר עם Ctrl+C. היתרונות הם שאין צורך להרכיב את ההפעלה בעצמכם, היעד רץ עם הרשאות רגילות, והוא מתאים לאוטומציה בסקריפט. הוספת-cmdLineFilter "string"מקליטה רק הפעלות ששורת הפקודה שלהן מכילה את המחרוזת הזו.2-childrenמקליט גם תהליכי בן, אבל כל תהליך מקבל קובץ.runמשלו, ו-WinDbg יכול לפתוח רק אחד בכל פעם.2
בזמן הקלטה מופיע UI קטן עם שני כפתורים: “Tracing Off” (לעצור הקלטה ולתת לאפליקציה להמשיך) ו-“Exit App” (לסגור את האפליקציה ולסיים הקלטה). לאוטומציה, מסתירים אותו עם -noUI ומקבלים את ה-EULA עם -accepteula.2 לוג ההקלטה נשמר בקובץ .out באותו מיקום כמו קובץ .run, שבו אפשר לקרוא את זמני שעון הקיר של התחלה וסיום ההקלטה, אורך session ההקלטה (זמן סימולציה), האם זה היה launch או attach, וגרסת ה-OS. כשהקלטה נכשלת, חלק מהודעות השגיאה מופיעות רק בקובץ .out.2
5. תכנון הקלטה לאפליקציות ארוכות-ריצה
זה לב המאמר. בהתחשב בעלויות של פרק 3, לתפוס באג שמתרחש בזמן לא ידוע בתהליך שרץ ימים דורש תכנון הקלטה. TTD.exe מספק ארבעה אמצעים, ובוחרים לפי אופי התסמין.
flowchart TB
accTitle: בחירת היקף ההקלטה לפי תסמין של אפליקציה ארוכת-ריצה
accDescr: אם לא יודעים מתי זה יקרה, שומרים רק את החלק האחרון עם ring buffer; אם יודעים איזה מודול חשוד, מקליטים רק את המודול הזה; אם אפשר לשנות את האפליקציה, מציינים את המרווח עם API ההקלטה הידנית; אם זה מופיע רק באתחול או בהפעלות מסוימות, מקליטים כל הפעלה עם monitor mode
q{"אופי התסמין?"} -->|"לא ידוע מתי זה קורה"| ring["-ring / -maxFile: שומרים רק את הסוף"]
q -->|"רק באתחול או בהפעלות מסוימות"| mon["-monitor: מקליטים כל הפעלה"]
ring -->|"מודול חשוד ברור"| mod["מוסיפים -module"]
ring -->|"אפשר לשנות את האפליקציה"| man["מציינים את המרווח עם -recordmode Manual"]
איור 10: הארבעה אינם בלעדיים זה לזה. שילוב -ring עם -module הוא השילוב המעשי ביותר לאפליקציות ארוכות-ריצה.
5.1 Ring Buffer — שומרים רק את N ה-MB האחרונים
עם -ring, ה-trace נכתב ל-ring buffer בגודל שניתן ב--maxFile, והקובץ לעולם אינו גדל מעבר למגבלה הזו. מה שנשאר הוא רק החלק האחרון של ההקלטה שמתאים לגודל הזה.2 היחידה של -maxFile היא MB; במצב ring-buffer ברירת המחדל היא 2,048 MB, המינימום 1 MB, והמקסימום 32,768 MB (ברירת המחדל ל-ring בזיכרון של תהליך 32-bit היא 256 MB).2
:: מצמידים לאפליקציית ניטור שרצה ומשאירים רק את 4 GB האחרונים של הביצוע
TTD.exe -accepteula -noUI -attach 21440 -ring -maxFile 4096 -out C:\traces\MyApp.run
:: כשמזהים את התסמין, עוצרים את ההקלטה (האפליקציה ממשיכה לרוץ)
TTD.exe -stop 21440
-stop מקבל שם תהליך, PID, או all, ועוצר את ההקלטה הזו. -wait <seconds> ממתין עד שכל session הקלטה במערכת הסתיים (-1 ללא הגבלה), ומשמש בסקריפטי אוטומציה לאכיפת הסדר “עוצרים, ואז אוספים את הקובץ.”2
flowchart TB
accTitle: ציר זמן של הקלטת ring-buffer
accDescr: ההקלטה מתחילה ב-attach, חלקים ישנים נדחפים החוצה מה-ring buffer, וכשהתסמין מופיע ומוציאים stop, נשאר רק ה-maxFile האחרון כ-trace
s["מתחילים הקלטה עם -attach -ring"] --> old["מרווחים ישנים נדחפים החוצה"]
old --> sym["התסמין מופיע"]
sym --> stop["עוצרים הקלטה עם -stop"]
stop --> keep["נשאר רק ה-maxFile האחרון"]
keep -.-> note["מגדירים גודל כך שהרגעים לפני התסמין ייכנסו ל-buffer"]
איור 11: Ring buffer הוא אמצעי לשמור “את הרגעים לפני התסמין.” עובדים את -maxFile אחורה מ-“זמן מהבחנת התסמין עד עצירה, כפול גידול לשנייה.”
יש שתי נקודות תכנון.
- עושים את ה-buffer גדול מספיק כדי לספוג את הזמן מהבחנה עד עצירה. בתהליך פעיל ה-trace גדל ב-5 עד 50 MB לשנייה,4 כך ש-ring של 4 GB מתאים לדקה עד שתיים תחת עומס כבד ולקצת יותר מעשר דקות תחת עומס קל. צריך מנגנון ששומר את הפיגור בין זיהוי התסמין (שורה ספציפית בלוג, סף מונה, התראה מניטור) לבין
-stopבתוך החלון הזה. - עצירה אינה ניתוק.
-stopעוצר את ההקלטה, אבל TTD אינו מנתק את עצמו מתהליך היעד.2 סיום ההקלטה לגמרי דורש סיום התהליך, לכן מכניסים “restart בחלון התחזוקה הבא אחרי איסוף ה-trace” לנוהל התפעול.
sequenceDiagram
accTitle: עצירת הקלטת ring-buffer בתיאום עם ניטור
accDescr: כשצד הניטור מזהה את התסמין דרך לוגים או מונים, הוא קורא ל-TTD.exe stop, אוסף את קובץ .run הסופי, ומפעיל מחדש את התהליך בחלון התחזוקה הבא כדי להסיר TTD
participant W as ניטור (לוגים, מונים)
participant T as TTD.exe
participant P as תהליך היעד
W->>W: מזהים את התסמין
W->>T: -stop PID
T->>P: עוצרים הקלטה (התהליך ממשיך)
T-->>W: .run מוכן
W->>W: אוספים .run, מצפינים, ושומרים
W->>P: מפעילים מחדש בחלון התחזוקה הבא
איור 12: רק כשהפיגור בין זיהוי ל--stop נכנס ל-buffer נשארים הרגעים לפני התסמין ב-trace. ה-restart הוא חלק מהתפעול.
5.2 סינון מודול — מקליטים רק בזמן שקוד שלכם רץ
-module <module name> מקליט רק את המודול שצוין (קובץ ההרצה עצמו או DLL טעון; אפשר יותר מאחד) ואת הקוד שהמודול הזה קורא. תהליך היעד רץ במהירות מלאה עד שקוד במודול שצוין מתבצע; ההקלטה מתחילה כשהביצוע נכנס למודול, נעצרת כשהוא יוצא, והתהליך חוזר למהירות מלאה. כי הדלקה וכיבוי של הקלטה יקרים, ההקלטה נשארת דולקת כל עוד המודול שצוין קורא למודולים אחרים בתהליך.2
:: מקליטים רק בזמן ש-DLL של לוגיקת המדידה שלנו רץ (בשילוב עם ring)
TTD.exe -accepteula -noUI -attach 21440 -module MeasureCore.dll -ring -maxFile 2048 -out C:\traces\
Trace שנעשה כך פשוט מדלג על המרווחים שבהם ההקלטה הייתה כבויה, ומתייחס ל-“ההוראה הבאה” כהוראה הראשונה אחרי שההקלטה חודשה, ומדבגים אותו לא אחרת מ-trace של כל התהליך.2 באפליקציות ארוכות-ריצה, לולאת idle של ה-UI והעיבוד הפנימי של ה-framework נוטים להסביר את רוב ההוראות שבוצעו, כך שהגבלת ההקלטה למודול שלכם לבדו מפחיתה overhead וגודל קובץ משמעותית.
flowchart TB
accTitle: איך הקלטה מסוננת לפי מודול מתנהגת
accDescr: תהליך היעד רץ במהירות מלאה מחוץ למודול שצוין; ההקלטה מתחילה כשנכנסים לקוד במודול שצוין, ממשיכה בזמן שהמודול הזה קורא למודולים אחרים, ונעצרת כשהביצוע יוצא מהמודול
out1["מחוץ למודול שצוין: מהירות מלאה"] --> in1["נכנסים למודול שצוין: ההקלטה מתחילה"]
in1 --> callee["מודולים אחרים שהוא קורא: ההקלטה ממשיכה"]
callee --> out2["יוצאים מהמודול שצוין: ההקלטה נעצרת"]
out2 --> out1
איור 13: כי גם Win32 APIs וה-runtime שה-DLL שלכם קורא מוקלטים, זה מספיק כדי לעקוב אחרי “מה הקוד שלנו מסר ל-OS.”
5.3 הקלטה ידנית — נותנים לאפליקציה לציין את המרווח
עם -recordmode Manual, התהליך ממשיך לרוץ במהירות מלאה גם אחרי ש-TTD מוזרק, וההקלטה קורה רק כשהתוכנית קוראת ל-API ההקלטה הפנימי של TTD (ברירת המחדל, Automatic, מקליטה מרגע ההזרקה).2 תיעוד ה-API והדוגמאות נמצאים במאגר WinDbg-Samples ב-GitHub.10
אם אפשר לשנות את האפליקציה, זו הגישה הכי פחות בזבזנית. אפשר לבנות לוגיקה שמתחילה הקלטה כשהאפליקציה עצמה יודעת שחריגה קרובה ועוצרת כשהדברים חוזרים לנורמה: “שלושה ניסיונות תקשורת רצופים נכשלו,” “הצטברות התור עברה את הסף,” וכן הלאה. בתצורת תהליך מפקח כמו זו שמכוסה במאמר Job Object, המפקח יכול להחליט על המרווח במקום.
5.4 Monitor Mode — מקליטים כל הפעלה
לבאגים שמופיעים רק מיד אחרי האתחול או בהפעלות מסוימות, -monitor מתאים. גם טבלת ההשוואה הרשמית מתארת monitor mode כמיועד ל-“תפיסת בעיות לסירוגין ובעיות אתחול.”2 כשמקליטים את אותה תוכנית פעמים רבות, שמות הקבצים הרציפים ברירת המחדל (MyApp01.run, MyApp02.run וכן הלאה) נהיים לא יעילים כי צריך לסרוק קבצים קיימים, לכן משתמשים ב--timestampFilename לשמות עם חותמת זמן. אפשר להגביל את מספר ההקלטות הסימולטניות עם -maxConcurrentRecordings.2
flowchart TB
accTitle: איך monitor mode מתנהג
accDescr: אפשרות monitor מתקינה driver לניטור הפעלת תהליכים, מצמצמת את היעד עם סינון שורת הפקודה בכל פעם שהתוכנית שצוינה עולה, מקליטה אותה, יוצרת קובץ trace נפרד לכל הפעלה, וממשיכה עד Ctrl+C או reboot
drv["מתקינים את driver ניטור ההפעלה"] --> launch["מזהים הפעלה של התוכנית שצוינה"]
launch --> filt{"מתאים ל-cmdLineFilter?"}
filt -->|"כן"| rec["מקליטים את ההפעלה הזו (קובץ נפרד לכל הפעלה)"]
filt -->|"לא"| skip["לא מקליטים"]
rec --> next["מחכים להפעלה הבאה (עד Ctrl+C או reboot)"]
skip --> next
איור 14: Monitor mode “אורב להפעלה.” הוא מתאים לבאגים שמופיעים בכל הפעלה ולבאגים שאפשר לצמצם לפי תנאי הפעלה.
5.5 איפה לשים את הדיסק
להקלטות ארוכות-ריצה, שמים את ה-traces ב-volume ייעודי ומכניסים את גידול קובץ .run לניטור. כפי שצוין בסעיף 3.2, כשהדיסק מתמלא, ההקלטה פשוט ממתינה בשקט בלי שגיאה. ה-workaround הרשמי פרימיטיבי באותה מידה: “בודקים מקום פנוי ב-Explorer” ו-“בודקים שקובץ .run גדל באופן סדיר.”4 בלי ניטור מקום פנוי, נשארים עם trace לא שלם שבו הרגע שרציתם ביותר מעולם לא נכתב.
flowchart TB
accTitle: איך דיסק שנגמר מייצר trace לא שלם
accDescr: כשהדיסק נגמר בזמן הקלטה, TTD כותב את העמוד האחרון וממתין בשקט בלי שגיאה ובלי אזהרה, כך שהתסמין שקורה אחר כך אינו נרשם, ומשאיר trace לא שלם שנפתח אבל חסר בו החלק הקריטי. מונעים עם volume ייעודי וניטור גידול .run
full["הדיסק נגמר"] --> wait["כותב את העמוד האחרון וממתין בשקט"]
wait --> none["אין שגיאה, אין אזהרה"]
none --> sym["התסמין קורה אחר כך"]
sym --> inc["Trace לא שלם בלי התסמין"]
guard["Volume ייעודי + ניטור גידול .run"] -.->|"מונע"| full
איור 15: Trace ש”נפתח אבל חסר בו החלק הקריטי” נולד מפער בניטור דיסק.
6. Replay — Positions, Events והרצה אחורה
6.1 פתיחה
כשפותחים קובץ .run ב-WinDbg, אם אין index, !index רץ אוטומטית ובונה את קובץ .idx תוך ספירת keyframes (positions ב-trace שנוצרים אוטומטית לאינדוקס; traces גדולים יותר יש להם יותר). ככל שה-trace גדול יותר, זה לוקח יותר זמן.5 את מצב ה-index אפשר לבדוק עם !index -status; אם הוא מדווח משהו שאינו “Index file loaded,” בונים מחדש עם !index -force. אם זה עדיין נכשל, סוגרים את הדיבאגר, מוחקים את קובץ .idx, ופותחים מחדש את קובץ .run. בניית index מחדש אינה משנה את קובץ .run, כך שנתונים אינם אובדים.8
סייג אחד: כשאינדוקס של traces גדולים שופר ב-TTD 1.11.611, פורמט ה-index השתנה, וtraces קיימים צריכים אינדוקס מחדש.11 אם סוחבים קבצי .idx ישנים, כאן נכשלים.
flowchart TB
accTitle: בדיקה ובנייה מחדש של ה-index
accDescr: אחרי פתיחת ה-trace, בודקים את המצב עם !index -status; אם זה משהו שאינו Index file loaded, בונים מחדש עם !index -force, ואם זה עדיין נכשל, סוגרים את הדיבאגר, מוחקים את קובץ .idx, ופותחים מחדש את קובץ .run. בנייה מחדש אינה משנה את קובץ .run
open["פותחים את ה-trace"] --> st["!index -status"]
st -->|"Index file loaded"| ok["ממשיכים לאנליזה"]
st -->|"כל דבר אחר"| force["בונים מחדש עם !index -force"]
force -->|"נכשל"| del["סוגרים, מוחקים .idx, פותחים מחדש .run"]
del --> ok
force -->|"מצליח"| ok
איור 16: בניית index מחדש אינה נוגעת בקובץ .run. כשיש ספק, מוחקים ופותחים מחדש.
6.2 תנועה לפי Position
העברת position ל-!tt עוברת לנקודת הזמן הזו.6
!tt 0 ; תחילת ה-trace
!tt 50 ; בערך מיקום 50%
!tt 100 ; סוף ה-trace
!tt 1A0:12F ; למיקום 1A0:12F
Position הוא זוג מספרים הקסדצימליים, sequencing number:step count.6 לאובייקט Position יש את המאפיינים Percent (החלק מה-trace), Sequence, ו-Steps, פלוס SeekTo(), שעובר ל-position הזה, ו-ToSystemTime(), שמחזיר את זמן שעון הקיר המשוער (UTC).7 בחקירות ארוכות-ריצה, ToSystemTime() הוא מה שעושה את ההבדל, כי הוא מאפשר להתאים חותמות זמן בלוג האפליקציה מול positions ב-trace. גם לתוצאות TTD.Calls יש SystemTimeStart / SystemTimeEnd.12
flowchart TB
accTitle: התאמת positions מול חותמות זמן בלוג
accDescr: החל מחותמת זמן בלוג האפליקציה, עוקבים אחרי זמן שעון הקיר המשוער של אובייקט Position כדי לזהות את ה-position ב-trace, עוברים לשם עם SeekTo, וקוראים את הביצוע סביבו
log["לוג האפליקציה: זמן החריגה"] --> match["מוצאים position ש-ToSystemTime שלו קרוב"]
match --> seek["עוברים לשם עם SeekTo"]
seek --> read["קוראים את הקריאות והערכים סביב"]
איור 17: “מה הוא עשה רגע לפני שורת הלוג הזו?” אפשר לחפש דרך ההתאמה בין positions לזמן שעון קיר.
Positions שימושיים גם לשיתוף. כשמוסרים trace לעמית, מצרפים את ה-position של !tt x:y והם יכולים להתחיל להסתכל מאותה נקודת זמן. כתיבת טווחי position בדוחות באגים היא גם פרקטיקה מומלצת רשמית.2
6.3 הרצה אחורה
הוספת - לפקודות הצעידה הרגילות זזה אחורה בזמן.13
| פקודה | משמעות | כפתור Ribbon |
|---|---|---|
p- |
חוזרים הוראה אחת (או שורת מקור אחת). קריאת פונקציה נספרת כצעד אחד | Step Over Back |
t- |
חוזרים הוראה אחת (או שורת מקור אחת). נכנסים לקריאות פונקציה | Step Into Back |
g- |
מבצעים לאחור. נעצרים כשפוגעים ב-breakpoint, באירוע, או בתחילת ה-trace | Go Back |
האירועים שעוצרים g- הם אותם אלה שעוצרים g קדימה.13 כלומר, מגדירים ba (breakpoint מסוג break-on-access) או bp ומריצים g-, והולכים ישר אחורה ל-“ה-position האחרון שבו התנאי הזה החזיק.” זה הצעד הבסיסי של פרק 7.
flowchart TB
accTitle: בחירה בין פקודות לאחור
accDescr: p- חוזר צעד אחד מעבר לקריאות פונקציה, t- חוזר הוראה אחת לתוך פונקציות, ו-g- הולך ישר אחורה ל-breakpoint, לאירוע, או לתחילת ה-trace. כל מה שעוצר g קדימה עוצר גם g-
cur["Position נוכחי"] -->|"p-"| over["חוזרים צעד אחד מעבר לקריאות"]
cur -->|"t-"| into["חוזרים הוראה אחת לתוך פונקציות"]
cur -->|"g-"| run["הולכים ישר אחורה לתנאי העצירה הבא"]
run -.-> stop["ba / bp / אירוע / תחילת ה-trace"]
איור 18: כדי להסתכל בזהירות בקרבת מקום, משתמשים ב-t-; כדי לקפוץ לסיבה רחוקה, משתמשים ב-ba + g-.
6.4 כניסה דרך Events
אם לא בטוחים מאיפה להתחיל לקרוא, מתחילים מרשימת האירועים. @$curprocess.TTD.Events מפרט יצירה וסיום של threads, טעינה ופריקה של מודולים, ו-exceptions כאירועים.14
dx -g @$curprocess.TTD.Events
dx @$curprocess.TTD.Events.Where(t => t.Type == "Exception").Select(e => e.Exception)
אירוע exception מכיל את ה-position, הסוג (Software / Hardware), קוד ה-exception, ואת program counter באותו זמן, ולחיצה על קישור [Time Travel] בפלט עוברת ל-position הזה.15 ה-walkthrough הרשמי עוקב אחרי הזרימה הזו: הוא קופץ ל-position של access violation (0xc0000005), חושד בשיבוש stack כי מצביע ה-stack ומצביע הבסיס אינם מסכימים, וצועד אחורה שלוש הוראות עם t- כדי לבדוק את הערכים.5 חלון Timelines של WinDbg ממחיז exceptions, breakpoints, גישות זיכרון וקריאות פונקציה כציר זמן, ולחיצה כפולה על exception מנפיקה את אותו SeekTo().16
6.5 Threads ו-Positions
!positions מציג כל thread פעיל ב-position הנוכחי יחד עם ה-position של כל thread ב-trace.17 יש כאן מלכוד אחד. החלפת threads עם ~<number>s אינה מזיזה את ה-position ב-trace. ה-position שהדיבאגר משתמש בו לקריאת זיכרון אינו משתנה, לכן כדי להסתכל על הזיכרון של thread אחר “באותה נקודת זמן,” עוברים לשם עם קישור ה-position בפלט !positions או עם !tt x:y.13
flowchart TB
accTitle: הנתיב הבסיסי דרך replay
accDescr: פותחים את ה-trace ובונים את ה-index, עוברים מרשימת האירועים ל-position של ה-exception, צועדים אחורה לסיבה, ואם צריך עוברים ל-position של thread אחר עם positions
open["פותחים את ה-trace ומאנדקסים"] --> ev["מוצאים את ה-exception ב-TTD.Events"]
ev --> seek["עוברים ל-position עם [Time Travel]"]
seek --> back["חוזרים עם t- / p- / g-"]
back --> th["בודקים positions של threads אחרים עם !positions"]
th -.-> caution["~s אינו מזיז את position ה-trace"]
איור 19: “נכנסים דרך אירוע, הולכים אחורה” הוא הנתיב הבסיסי דרך replay. החלפת threads אינה הזזת positions.
7. מציאת “מתי” עם שאילתות — TTD.Calls ו-TTD.Memory
החוזק האמיתי של replay הוא שאפשר לשאול את כל ה-trace. אובייקטי TTD חשופים דרך debugger data model (פקודת dx), ואפשר לסנן, למיין ולאגד אותם בסגנון LINQ.18
7.1 TTD.Calls — חיפוש קריאות פונקציה
@$cursession.TTD.Calls("module!symbol") אוסף את הקריאות לפונקציה שצוינה מכל ה-trace. Wildcards מותרים, וכל קריאה נושאת את positions ההתחלה והסיום שלה (TimeStart / TimeEnd), מזהה ה-thread (עם UniqueThreadId שלעולם אינו בשימוש חוזר), הארגומנטים (Parameters[]), ערך ההחזרה (ReturnValue), וכתובת החזרה (ReturnAddress).12
הדוגמה בתיעוד הרשמי היא GetLastError. מאגדים את הקריאות שערך ההחזרה שלהן אינו אפס לפי קוד שגיאה, ומקבלים רשימה של אילו שגיאות קרו כמה פעמים במהלך ה-trace.18
dx -g @$cursession.TTD.Calls("kernelbase!GetLastError").Where(x => x.ReturnValue != 0).GroupBy(x => x.ReturnValue).Select(x => new { ErrorNumber = x.First().ReturnValue, ErrorCount = x.Count() }).OrderByDescending(p => p.ErrorCount),d
ל-“מאיפה נקרא MessageBox האחרון?”, לוקחים את הקריאה האחרונה עם OrderBy(c => c.TimeStart).Last() ועוברים לשם דרך קישור [Time Travel] של ה-TimeStart שלה.18
flowchart TB
accTitle: בניית שאילתת TTD.Calls
accDescr: אוספים קריאות לפי שם פונקציה, מסננים לפי ערך החזרה או ארגומנטים, מאגדים לפי קוד שגיאה וכדומה, ממיינים לפי זמן, ועוברים ל-position של הקריאה שרוצים דרך קישור Time Travel
calls["TTD.Calls (שם פונקציה, wildcards)"] --> where["Where: סינון לפי ערך החזרה או ארגומנטים"]
where --> group["GroupBy: איגוד לפי קוד שגיאה וכו'"]
group --> order["OrderBy: מיון לפי זמן"]
order --> jump["עוברים דרך [Time Travel] ב-TimeStart"]
איור 20: הדרך להשתמש בשאילתות היא להתחיל לא מ-“איפה” אלא מ-“מתי, כמה פעמים, ועם אילו ארגומנטים.”
מילה על סימולים. TTD קובע את מספר וסוגי הארגומנטים של פונקציה, את סוג ההחזרה שלה, ואת מוסכמת הקריאה ממידע סימול PDB. עם private symbols מקבלים את שם הפונקציה ואת הארגומנטים הנכונים. עם public symbols בלבד, מקבלים את שם הפונקציה וארגומנטים ברירת מחדל (ארבעה integers של 64-bit). מודול בלי סימולים בכלל מקבל את שם הפונקציה UnknownOrMissingSymbols.1218 לסוגי PDB ואיך לאחסן אותם, ראו “מה זה PDB?”.
flowchart TB
accTitle: זמינות סימולים ותוצאות TTD.Calls
accDescr: עם private symbols מקבלים את שם הפונקציה ואת הארגומנטים הנכונים; עם public symbols בלבד מקבלים את שם הפונקציה וארבעת ארגומנטי integer של 64-bit ברירת מחדל; בלי סימולים שם הפונקציה הופך ל-UnknownOrMissingSymbols
sym{"סימולים למודול?"} -->|"private symbols"| full["שם פונקציה + ארגומנטים וערך החזרה נכונים"]
sym -->|"public symbols"| pub["שם פונקציה + ארגומנטים ברירת מחדל (4 x 64-bit integers)"]
sym -->|"אין"| unk["UnknownOrMissingSymbols"]
איור 21: האם שמרתם את ה-PDBs של המודולים שלכם קובע ישירות כמה השאילתות שימושיות.
Calls כולל חישוב, כך שככל שה-trace גדול יותר, זה לוקח יותר זמן ושימוש CPU גבוה יותר. תוצאות נשמרות ב-cache בזיכרון, כך שהשאילתות השנייה ואילך לאותה פונקציה מהירות יותר.12 כששאילתה מחזירה כלום, יש ארבע סיבות: איך הקריאה כתובה (בודקים את שם המודול עם פקודת x; אם זה חוזר באותיות גדולות, משתמשים בזה), ה-DLL היעד עדיין לא טעון ב-position הזה (עוברים ל-position אחרי הטעינה ומריצים שוב), הפונקציה היא inlined (לא ניתן לעקוב), או שה-wildcard רחב מדי (מצמצמים).18
7.2 TTD.Memory — חיפוש גישות זיכרון
@$cursession.TTD.Memory(start address, end address, "access type") אוסף גישות לטווח הזיכרון שצוין מכל ה-trace. הסוגים הם r (קריאה), w (כתיבה), rw, e (הרצה), rwe, ו-ec (הרצה/שינוי).19 “מי כתב אחרון למשתנה הזה?” נענה באיסוף עם "w", לקיחת .Last(), ומעבר ל-position הזה.5
dx -g @$cursession.TTD.Memory(0x00a4fca0, 0x00a4fca4, "w")
dx @$cursession.TTD.Memory(0x00a4fca0, 0x00a4fca4, "w").Last().TimeStart.SeekTo()
אם צריך לחפש רק קדימה או אחורה מה-position הנוכחי, @$curprocess.TTD.PrevMemoryAccess("w", address, size) / NextMemoryAccess קלים יותר ומקבלים כמה טווחים בבת אחת. את ה-position שבו register השתנה אפשר למצוא עם @$curthread.TTD.PrevRegisterWrite("rcx").6
7.3 ba + g- — לגרום לדיבאגר לענות “מי שיבש?”
דחיסת הנוהל שמוצג ב-walkthrough הרשמי לצורה שמישה לחקירות ארוכות-ריצה נותנת את הבא.5
- עוברים ל-position של ה-exception עם
TTD.Events - צועדים אחורה עם
t-ומניחים איזה משתנה מחזיק את הערך המשובש - מקבלים את כתובת המשתנה הזה עם
dx &variable - מגדירים breakpoint כתיבה עם
ba w4 <address> - מריצים
g-כדי לחזור ישר ל-position שבו המשתנה הזה נכתב אחרון - בודקים אם הנקודה הזו (או כמה הוראות קודם) היא הסיבה. אם הערך שנכתב הגיע ממשתנה אחר, מגדירים
baעל המשתנה הזה ומריציםg-שוב - חוזרים עד שמגיעים להוראה ששיבשה
flowchart TB
accTitle: מעקב אחרי מקור ערך עם ba ו-g-
accDescr: מגדירים breakpoint כתיבה על כתובת הערך המשובש ומבצעים לאחור, נעצרים בהוראה שכתבה אחרונה, ואם הערך הגיע ממשתנה אחר חוזרים על אותם שלבים עד שמגיעים להוראה ששיבשה
bad["מזהים את כתובת הערך המשובש"] --> ba["מגדירים breakpoint כתיבה עם ba w"]
ba --> gb["מבצעים לאחור עם g-"]
gb --> writer["נעצרים בהוראה שכתבה אחרונה"]
writer --> q{"הערך הגיע ממשתנה אחר?"}
q -->|"כן"| bad
q -->|"לא"| found["ההוראה המשבשת = הסיבה"]
איור 22: חקירה שמסתיימת ב-“זה משובש” עם dump מתקדמת מכנית ל-“מי שיבש” עם TTD.
TTD 1.11.553 ואילך גם מוסיפים @$curframe.TTD.VariableHistory(), שמחזיר את ההיסטוריה של ערכי משתנים מקומיים של frame. הוא מציג טבלה של שמות המשתנים ואילו ערכים כל משתנה החזיק באילו טווחי position.11 במצבים כמו שיבוש stack שבהם רוצים לדעת “מאז מתי הערך שגוי,” זה שימושי לצמצום איפה להגדיר ba קודם.
8. יישום לבאגים ארוכי-ריצה
עכשיו מיישמים את הכלים האלה על שלושת סוגי המקרה שרשומים בהתחלה.
flowchart TB
accTitle: סוגי באגים ארוכי-ריצה ונקודת הכניסה של TTD לכל אחד
accDescr: ל-exceptions לסירוגין ולשיבוש נתונים, חוזרים מהאירוע עם ba ו-g-; לגידול משאבים, מתאימים קריאות רכישה ושחרור עם TTD.Calls; ל-not responding ולשרשרות המתנה, עוקבים אחרי positions וזמני שעון הקיר של קריאות wait API
t1["Exceptions לסירוגין, שיבוש נתונים"] --> a1["TTD.Events, ואז ba + g-"]
t2["גידול handle וזיכרון"] --> a2["מתאימים רכישה ושחרור עם TTD.Calls"]
t3["Not responding, שרשרות המתנה"] --> a3["!positions וזמני שעון קיר של wait APIs"]
a2 -.-> pre["מקליטים עם -module מוגבל ל-DLL שלכם"]
איור 23: נקודת הכניסה נבדלת לפי סוג. מה שהם חולקים הוא צמצום היקף ההקלטה לפני שמתחילים לקרוא.
סוג 1: Exceptions לסירוגין ושיבוש נתונים. שומרים את הרגעים לפני התסמין עם ring buffer של סעיף 5.1, קופצים ל-position של ה-exception מרשימת האירועים של סעיף 6.4, ועוקבים אחרי מקור הערך עם ba + g- של סעיף 7.3. ההבדל מחקירת dump הוא שהחקירה אינה מסתיימת כשמוצאים “את המשתנה המשובש”; משם ממשיכים מכנית אחורה.
סוג 2: גידול handle וזיכרון. זה סוג המקרה שמפורק ב-“Handle leak באפליקציית מצלמה תעשייתית שקורסת אחרי ריצה ארוכה.” אי אפשר להקליט חודש שלם, לכן מגבילים את ההקלטה ל-DLL שלכם עם -module של סעיף 5.2, משלבים עם -ring, ושומרים “את הדקות הבודדות בזמן שזה גדל.” ב-trace, אוספים TTD.Calls("kernelbase!CreateFileW") ו-TTD.Calls("kernelbase!CloseHandle") ומתאימים את ערך ההחזרה של CreateFileW (ערך ה-handle) מול הארגומנט הראשון של CloseHandle (Parameters[0]). ה-ReturnValue של CloseHandle הוא דגל הצלחה בוליאני, כך שהוא חסר תועלת להתאמה. אם ערך handle נשאר לא סגור, ה-ReturnAddress (הקורא) של קריאת CreateFileW הזו נותן לכם את הקורא שדולף.12 עם זאת, תצפית אם דליפה קיימת וכמה גדולה היא זולה יותר עם Application Verifier או ספירת handles; חלוקת העבודה הנכונה היא להביא TTD בשלב של קיבוע איזה נתיב דולף. שימו לב שהקלטה עם Application Verifier מופעל פוגעת בבירור בביצועי replay בגלל איך הזיכרון בשימוש, לכן מכבים אותו בזמן הקלטה.8
סוג 3: “Not responding” ושרשרות המתנה. כפי שכתוב ב-“מה Not Responding באמת אומר,” hang הוא שאלה של מי ממתין למי. Trace אינו גדל בזמן idle,4 כך שזמן ההמתנה של thread שממתין ב-kernel אינו מופיע כהוראות. מה שמופיע הוא הקריאה רגע לפני הכניסה להמתנה וההוראות אחרי שהיא חזרה. מסתכלים על ה-position של כל thread עם !positions,17 ומיישרים “איזה thread התחיל להמתין למה, ומתי” מ-SystemTimeStart / SystemTimeEnd של TTD.Calls("kernelbase!WaitForSingleObject") וכדומה; בהתאמה מול חותמות זמן בלוג, זה מאפשר לשחזר את שרשרת ההמתנות.12 “באגים תלויי סדר” כמו DllMain ו-loader lock או spurious wakeups של condition variables הם אזור שבו TTD, שמתעד את הסדר עצמו, מתאים.
9. TTD עם אפליקציות .NET
התיעוד הרשמי של TTD מציין שקוד מנוהל ניתן לדיבוג עם TTD ב-WinDbg באמצעות הרחבת SOS (sos.dll) שרצה במצב 64-bit.1 הטעינה זהה לפרק 3 של מאמר אנליזת SOS (.loadby sos coreclr או טעינה אוטומטית), ו-!clrstack ו-!pe עובדים בכל position ב-trace. הנתיב הבסיסי הוא לעבור ל-position של ה-exception עם TTD.Events ואז לקרוא את ה-stack המנוהל עם !clrstack.
flowchart TB
accTitle: הכנה לקריאת TTD trace של אפליקציית .NET
accDescr: פותחים את TTD trace ב-WinDbg, טוענים את הרחבת SOS של 64-bit, עוברים ל-position של אירוע ה-exception, וקוראים את המצב המנוהל עם !clrstack ו-!pe. משתמשים ב-TTD.Calls לקריאות מעבר לגבולות native
run["TTD trace (.run)"] --> wd["WinDbg"]
wd --> sos["הרחבת SOS (64-bit)"]
sos --> ev["עוברים ל-position של ה-exception עם TTD.Events"]
ev --> clr["קוראים עם !clrstack / !pe"]
wd -.-> calls["TTD.Calls לקריאות מעבר לגבולות native"]
איור 24: מצב מנוהל דרך SOS, חיפושי קריאה בגבולות native. מפרידים את התפקידים ולא הולכים לאיבוד.
שני סייגים. ראשית, מה ש-Microsoft מבטיחה הוא “SOS במצב 64-bit.” שום דבר אינו נאמר על אפליקציות .NET שנבנו ל-x86, לכן מריצים את יעד החקירה כ-x64 אם אפשר. שנית, TTD.Calls מסתמך על מידע סימול PDB.12 לא סומכים על חיפוש מתודות מנוהלות שעברו JIT לפי שם; השימוש האמין הוא לעקוב אחרי קריאות מעבר לגבולות native כמו יעדי P/Invoke ב-DLLs native, COM ו-Win32 APIs. בעיות heap מנוהל כמו אלה ב-“להבדיל ב-.NET בין המתנה ל-GC לבין memory leak” נרדפות קודם עם dotnet-counters / dotnet-gcdump / !gcroot על dump, ו-TTD יוצא בשלב שבו מעורב גבול native.
10. בחירה בין dumps, לוגים, ETW ו-TTD
TTD אינו תרופת פלא, והוא אינו מחליף את הכלים הקיימים. כאן הוא מיושר מול הכלים שמכוסים במאמרים שלנו.
| מה רוצים לדעת | כלי להשתמש בו קודם | מתי TTD נכנס |
|---|---|---|
| המצב ברגע הקריסה | Crash dump (WER LocalDumps / ProcDump) | כשה-dump מראה “זה משובש” אבל לא מי שיבש |
| איזה קובץ או מפתח Registry נכשל | Process Monitor | כשצריך גם איך נבנו הארגומנטים שהועברו ל-API שנכשל |
| ביצועים של כל ה-PC לאורך זמן | WPR/WPA, PerfView (ETW) | כשזו בעיית נכונות ולא ביצועים וצריך סדר ברמת הוראה |
| רצף האירועים ברמה העסקית | הלוגים של האפליקציה | כשזה קרה בנתיב קוד בלי לוגים (TTD מתעד הכול בלי שינויי קוד מראש1) |
| מי כתב את הערך הזה, סדר הקריאות | TTD | — |
flowchart TB
accTitle: בחירת שיטת חקירה
accDescr: קודם תופסים את המצב עם dumps ולוגים קלים, חוקרים APIs שנכשלים עם ProcMon וביצועים עם ETW, ועוברים ל-TTD רק כשעדיין צריך מי העביר מה ומתי
start["תסמין"] --> light["תופסים את המצב עם dumps ולוגים (קל)"]
light --> api["APIs שנכשלים: ProcMon"]
light --> perf["ביצועים: WPR/WPA, PerfView"]
light --> need{"צריך מי, מתי, ומה הועבר?"}
need -->|"כן"| ttd["מתכננים את היקף ההקלטה ומקליטים עם TTD"]
need -->|"לא"| done["מיושב עם כלים קלים"]
איור 25: תופסים את “התוצאה” עם כלים קלים קודם, ומוציאים TTD רק למקרים שבהם מתברר שה”נתיב” נחוץ.
הסדר הוא “הכי קל קודם.” Dumps ולוגים עולים כמעט כלום לאיסוף, לכן משאירים אותם במקום באופן קבוע; מוציאים TTD, אחרי התכנון של פרק 5, למקרים שבהם קריאת ה-dump הראתה שהנתיב נחוץ. בהתחשב ב-overhead של TTD ובכך ש-traces מכילים מידע סודי, אין סיבה להפוך את הסדר הזה.
11. הערות תפעול
- מתייחסים ל-traces כקבצים סודיים. הקלטה מכילה תוכן זיכרון ויכולה לכלול מידע אישי וקשור לאבטחה כמו נתיבי קבצים, נתוני Registry ותוכן של זיכרון וקבצים.12 כשמקליטים בסביבת לקוח, מבינים מראש מה עשוי להיכלל (connection strings, tokens, נתוני לקוחות) ומחליטים על ערוץ העברה מוצפן, מיקום אחסון ותקופת שמירה.
- משתפים רק את קובץ
.run. קובץ.idxגדול בערך כמו קובץ.runונוצר אוטומטית כש-WinDbg פותח אותו. קבצי.runנדחסים היטב. כשמדווחים באג ב-TTD עצמו, מצרפים גם את קובץ.out.2 - מיישרים גרסאות. TTD ממשיך להתעדכן יחד עם WinDbg; 1.11.611 כולל תיקון לקריסות הקלטה בתוכניות שמשתמשות ב-AVX/AVX512 ושינוי בפורמט ה-index.11 ערבוב גרסה ישנה בצד ההקלטה או בצד ה-replay פירושו אינדוקס מחדש או הקלטה מחדש.
- בודקים
-replayCpuSupportכשמריצים replay על CPU שונה. ברירת המחדל מעדיפה ניידות, ו-MostConservativeמסופק למקרים שבהם CPU ההקלטה ו-CPU ה-replay שונים (כמו replay של trace Intel על arm64). ולהפך, אם יודעים ש-CPU ה-replay שווה או טוב יותר, אפשר לבחור הקלטה קטנה ומהירה יותר.2 - אפשר להקליט גם Windows Server. TTD.exe תומך ב-Windows Server 2016/2019/2022/2025.2
- בידוד כשל להקליט. קודם מנסים אם אפשר להקליט
ping.exeאוcmd.exe; אם לא, חושדים בהתנגשות עם תוכנה חודרנית כמו אנטי-וירוס או וירטואליזציית אפליקציות.8
flowchart TB
accTitle: נוהל טיפול ב-traces
accDescr: כי trace מוקלט מכיל תוכן זיכרון, מבינים איזה מידע הוא עשוי להכיל, דוחסים רק את קובץ .run ומוסרים דרך ערוץ מוצפן, מחליטים על מיקום האחסון ותקופת השמירה, ובצד האנליזה פותחים עם אותה גרסת WinDbg ובונים את ה-index
rec["הקלטה הושלמה (.run / .idx / .out)"] --> know["מבינים איזה מידע הוא עשוי להכיל"]
know --> share["דוחסים רק .run, מצפינים ומוסרים"]
share --> keep["מחליטים על מיקום האחסון ותקופת השמירה"]
keep --> open["פותחים עם אותה גרסת WinDbg ויוצרים .idx"]
איור 26: מסירת trace היא מסירת קובץ סודי. מחליטים על הנוהל לפני שמקליטים.
12. סיכום
- Dump הוא “מצב”; TTD הוא “הנתיב.” במקרים שצריכים את מקור ערך משובש או את סדר הקריאות, TTD הופך את החקירה לעבודה מכנית
- ההקלטה איטית פי 5 עד 20, גדלה 5 עד 50 MB לשנייה, ואינה יכולה לנתק. לאפליקציות ארוכות-ריצה, מתכננים קודם את היקף ההקלטה עם
-ring/-maxFile,-module,-recordmode Manual, ו--monitor - Replay הוא “נכנסים דרך אירוע, הולכים אחורה”:
TTD.Events, ואז[Time Travel], ואזt-/g-, ו-ba+g-כדי לגרום לדיבאגר לענות “מי שיבש” TTD.Callsו-TTD.Memoryהן שאילתות על כל ה-trace. מתאימים מול חותמות זמן בלוג עםToSystemTime()ו-SystemTimeStart- ל-.NET, קוראים את המצב עם SOS של 64-bit ומשתמשים ב-
TTD.Callsבגבולות native - Trace הוא קובץ סודי. משתפים רק את קובץ
.run, ומחליטים על הערוץ והאחסון קודם
מקרים שבהם dump נלכד אבל הסיבה מחוץ להישג, או שבהם העבודה נעצרה כי הבאג אינו משתחרר, מיושבים לא פעם עם TTD ברגע שאפשר לתכנן את היקף ההקלטה. אפשר גם לקחת על עצמנו הכול מתכנון ההקלטה עד אנליזת קובץ .run, אז אפשר לפנות עם ה-dumps והלוגים.
מאמרים קשורים
- קריאת crash dumps עם WinDbg + SOS — מדריך מעשי לאנליזה אחרי איסוף
- איך אוספים crash dump באפליקציית Windows: WER, ProcDump, WinDbg
- מה זה PDB (Program Database)? — להבין מידע דיבוג, סימולים ו-Source Link
- Handle leak באפליקציית מצלמה תעשייתית שקורסת אחרי ריצה ארוכה
- מה Not Responding באמת אומר — איך Windows מחליט שיש hang, ואיך לתכנן אפליקציות שלא נתקעות
- מדריך מעשי ל-Process Monitor (ProcMon)
- WPR/WPA בפועל — מבוא ל-performance analysis מערכתי כשכל ה-PC איטי
- לאתר “איטי” עם PerfView ו-dotnet-trace
- מה נשאר אחרי שההורה מת — להחזיק תהליכי בן ברצועה עם Job Objects
תחומי ייעוץ קשורים
KomuraSoft LLC מטפלת בחקירת שורש של באגי אפליקציות Windows שמופיעים רק אחרי ריצות ארוכות או לסירוגין, בשילוב crash dumps, לוגים ו-TTD traces; בבניית מערך חקירה כולל תכנון היקף ההקלטה; ובבידוד כשלים שמערבים גבולות native (COM, P/Invoke, SDKs של התקנים). פונים כבר בשלב של “יש dump אבל לא מגיעים לסיבה.”
קישורים
-
Microsoft Learn, Time Travel Debugging - Overview. על כך ש-TTD מקליט את ביצוע התהליך ומריץ אותו קדימה ואחורה, ש-dumps נוטים להחמיץ את המצב ואת נתיב הביצוע שהובילו לכשל, שהקלטה דורשת הרשאות Administrator, שהקלטות עשויות להכיל מידע אישי וקשור לאבטחה, טבלת השוואת שיטות חקירה, התפקידים של
.run/.idx, ודיבוג קוד מנוהל עם הרחבת SOS במצב 64-bit. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 -
Microsoft Learn, Time Travel Debugging - TTD.exe command line utility. על ההאטה פי 5 עד 20 או יותר, אי-יכולת לנתק את עצמו אחרי attach, תמיכה ב-Windows Server 2016 עד 2025, התקנה ופריסה offline, שלושת המצבים
-launch/-attach/-monitor, האפשרויות-out/-noUI/-accepteula/-stop/-wait/-tracingOff/-children/-cmdLineFilter/-timestampFilename/-ring/-maxFile/-maxConcurrentRecordings/-numVCpu/-replayCpuSupport/-module/-recordmode, קריאת קובץ.out, ועצות לשיתוף traces. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 ↩18 ↩19 ↩20 ↩21 ↩22 ↩23 ↩24 ↩25 ↩26 ↩27 ↩28 ↩29 ↩30 ↩31 ↩32 ↩33 ↩34 -
Microsoft Learn, Time Travel Debugging - Overview - Things to look out for. על אי-תאימות עם אנטי-וירוס ותוכנות ניטור זיכרון ועם Electron, user mode בלבד, replay לקריאה בלבד, אי-יכולת להזריק ל-protected processes (PPL), והשפעת ביצועים של בערך פי 10 עד 20 בזמן הקלטה. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Time Travel Debugging - Working with Trace Files. על גורמי גודל ה-trace (bit אחד עד בית אחד להוראה), גידול של 5 עד 50 MB לשנייה כשפעיל ואפס כש-idle, היעדר תקרת גודל מקסימלית, ה-index שהוא פי 1 עד 2 מה-trace, והתנהגות ההקלטה והאינדוקס כשהדיסק נגמר יחד עם ה-workaround. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, Time Travel Debugging - Sample App Walkthrough. על הנוהל הכללי של מעבר ל-position של אירוע exception וחזרה עם
baו-g-ל-position שבו נכתב ערך לא תקין אחרון, נקודת הכשל שלעתים קרובות נמצאת בתוך טיפול בשגיאות כמה צעדים אחרי הסיבה האמיתית, ה-trace שנסגר בקריסה ו-WinDbg שמאנדקס אותו אוטומטית, והשימוש ב-TTD.Memoryוב-.Last(). ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Microsoft Learn, !tt (time travel). על ציון positions ל-
!tt(אחוז אוxx:yy), משמעות שני רכיבי position (מספר רצף וספירת צעדים), ו-TTD.PrevRegisterWrite/PrevMemoryAccess/NextMemoryAccess. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, TTD Position Objects. על
Percent/Sequence/Stepsשל אובייקט Position,SeekTo(),ToSystemTime()שמחזיר את זמן שעון הקיר המשוער (UTC), ו-FFFFFFFFFFFFFFFE:0שמציין את סוף ה-trace. ↩ ↩2 -
Microsoft Learn, Time Travel Debugging - Troubleshooting. על כך שנדרש elevation, שהקלטת הפעלה של אפליקציות UWP אינה נתמכת, ש”תהליכים חריגים” ב-session או הקשר אבטחה אחר מחוץ להיקף, בידוד עם
ping.exe/cmd.exe, האטת replay כשמשתמשים ב-Application Verifier באותו זמן, ובניית index מחדש עם!index -status/!index -force. ↩ ↩2 ↩3 ↩4 ↩5 -
Microsoft Learn, Time Travel Debugging - Record a trace. על הקלטה מ-Launch executable (advanced) / Attach to process ב-UI של WinDbg, תיבת הסימון Record with Time Travel Debugging, הגדרת מיקום השמירה עם Configure and Record, והגבלת מודולים עם Record subset of execution. ↩
-
Microsoft, WinDbg-Samples - TTD in-process recording API (GitHub). על תיעוד API ההקלטה הפנימי ש, בשילוב עם
-recordmode Manual, מאפשר לתוכנית לשלוט בהתחלה ובעצירה של ההקלטה. ↩ -
Microsoft Learn, Time travel debugging release notes. על תיקון ההקלטה לתוכניות שמשתמשות ב-AVX/AVX512 ושינוי פורמט ה-index (נדרש אינדוקס מחדש) ב-1.11.611, ו-
@$curframe.TTD.VariableHistory()שנוסף ב-1.11.553. ↩ ↩2 ↩3 -
Microsoft Learn, TTD Calls Objects. על הארגומנטים של
TTD.Calls, המאפייניםThreadId/UniqueThreadId/Function/ReturnValue/ReturnAddress/Parameters[]/TimeStart/TimeEnd/SystemTimeStart/SystemTimeEnd, ברירות המחדל כשחסר מידע סימול PDB (ארבעה ארגומנטי integer של 64-bit,UnknownOrMissingSymbols), והחישוב שלוקח זמן עם תוצאות שנשמרות ב-cache. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
Microsoft Learn, Time Travel Debugging - Replay a trace. על ביצוע לאחור עם
p-/t-/g-,g-שנעצר באותם אירועים כמו ביצוע קדימה,!positions, ו-~sשאינו משנה את ה-position ב-trace. ↩ ↩2 ↩3 -
Microsoft Learn, TTD Event Objects. על סוגי האירועים (ThreadCreated/ThreadTerminated/ModuleLoaded/ModuleUnloaded/Exception) ואובייקטי הבן Position, Module, Thread ו-Exception. ↩
-
Microsoft Learn, TTD Exception Objects. על
Type(Software/Hardware),ProgramCounter,Code,Flagsו-Positionשל אובייקט exception. ↩ -
Microsoft Learn, WinDbg: Timelines. על חלון Timelines שממחיז exceptions, breakpoints, גישות זיכרון וקריאות פונקציה, ולחיצה כפולה על exception שמנפיקה
Position.SeekTo(). ↩ -
Microsoft Learn, !positions. על הצגת כל thread פעיל ואת ה-position של כל thread ב-trace. ↩ ↩2
-
Microsoft Learn, Introduction to Time Travel Debugging objects. על האובייקטים
@$curprocess.TTD/@$cursession.TTD, שאילתות עםOrderBy/Where/Select/GroupBy, דוגמאות איגוד שגיאותGetLastErrorומציאת הקריאה האחרונה ל-MessageBoxW, משמעותUnknownOrMissingSymbols, וארבע הסיבות ש-Callsמחזיר כלום. ↩ ↩2 ↩3 ↩4 ↩5 -
Microsoft Learn, TTD Memory Objects. על סוגי הגישה של
TTD.Memory(r/w/rw/e/rwe/ec) ומעבר ל-position דרך קישור[Time Travel]בתוצאות. ↩
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
למה arguments נשברים — כללי command-line arguments ב-Windows
Windows מעביר ל-CreateProcess מחרוזת אחת שהמקבל מפצל. מכסה את כללי CommandLineToArgvW, CRT ו-.NET, ArgumentList, ובניה ב-C++.
סיום התמיכה ב-printer drivers של Windows — איך אפליקציות עסקיות מתכוננות להדפסת דוחות ותוויות
Microsoft מפסיקה בהדרגה printer drivers מסוג v3/v4. מה Windows protected print mode מסיר, ואיך עושים inventory ומכינים הדפסת דוחות ותוויו...
מה נשאר אחרי שה-parent מת — מחזיקים child processes ב-Job Object
למה SDK helpers שורדים UI שנהרג ומחזיקים את המצלמה או את ה-COM port? מתכננים משך חיים של child process עם Job Objects, KillOnJobClose ו-c...
Win32 Thread Pool API — מקביליות בלי CreateThread, דרך CreateThreadpoolWork
מפזרים קריאות CreateThread בכל הקוד ה-native? המאמר מסביר את ה-Win32 Thread Pool API שעוצב מחדש ב-Vista: ארבעת האובייקטים work, timer, wa...
Named Pipes בפועל — ה-IPC הסטנדרטי של Windows, מתכנון עד אבטחה
מדריך מעשי ל-Named Pipes, ה-IPC הסטנדרטי ב-Windows. המאמר מסדר לפי מקורות ראשוניים את הבחירה בין byte mode ל-message mode, תכנון server ל...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
חקירת תקלות ובעיות בריצות ארוכות
תקלות לסירוגין, אבחון תקשורת, תקיעות בריצות ארוכות ובדיקת נתיבי כשל.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
יישומים עסקיים, חיבור התקנים וכלי תקשורת, מהגדרת הדרישות ועד הפיתוח.
חקירת תקלות ואיתור גורמים
אנו חוקרים תקלות שקשה לשחזר, בעיות אחרי ריצות ארוכות, דליפות וניתוקי תקשורת.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה ההבדל בין Time Travel Debugging (TTD) לבין crash dump?
- Crash dump הוא תצלום של הזיכרון ברגע הקריסה: הוא מראה את המצב באותו רגע, אבל לא את הנתיב שהוביל לשם. TTD trace הוא הקלטה מלאה של ביצוע ההוראות של ה-process שאפשר אחר כך להריץ קדימה ואחורה, כך שאפשר לחזור אחורה ולבדוק ישירות מי כתב אחרון למשתנה או מה נקרא רגע לפני exception. גם התיעוד הרשמי של Microsoft מציין ש-dumps נוטים להחמיץ את המצב ואת נתיב הביצוע שהובילו לכשל. המחיר הוא שהתהליך רץ פי 5 עד 20 לאט יותר בזמן ההקלטה, וקובץ ה-trace גדל בערך ב-5 עד 50 MB לשנייה כשהתהליך פעיל.
- אפשר להשאיר TTD מחובר לאפליקציית ייצור שרצה ימים?
- לא כפי שהוא. TTD מאט את התהליך משמעותית בזמן הקלטה, ה-trace גדל ב-5 עד 50 MB לשנייה בתהליך פעיל, ואין תקרה לגודל הקובץ. שימוש באפליקציה ארוכת-ריצה מניח תכנון הקלטה: -ring (ring buffer) של TTD.exe עם -maxFile כדי לשמור רק את N ה-MB האחרונים, -module כדי להקליט רק בזמן שמודול שלכם רץ, או -recordmode Manual כדי שהאפליקציה תבחר את מרווח ההקלטה. וגם, אחרי attach, TTD אינו יכול לנתק את עצמו, לכן צריך להחליט מראש איך מסיימים את ההקלטה, כלומר לסיים (restart) את התהליך.
- אפשר להקליט services ותהליכים ב-sessions אחרים?
- התיעוד של TTD.exe מתאר -attach כמיועד לחקירת services ואפליקציות ארוכות-ריצה, ו-monitor כמקליט בכל פעם שתוכנית או service עולה. מצד שני, דף ה-troubleshooting מציין שתהליכים חריגים שרצים ב-session אחר או בהקשר אבטחה שונה אינם נתמכים כרגע להקלטה. האם זה באמת עובד תלוי בסביבה, לכן קודם מקליטים תהליך פשוט כמו ping.exe או cmd.exe באותה תצורה כמו הייצור, אחר כך מנסים את תהליך היעד, ורק אז בונים את זה לתפעול.
- אפשר להשתמש ב-TTD על traces של אפליקציות .NET?
- כן. התיעוד הרשמי מציין שהרחבת SOS (sos.dll) שרצה במצב 64-bit יכולה לשמש על TTD trace ב-WinDbg לדיבוג קוד מנוהל. התבנית הבסיסית היא להריץ פקודות SOS כמו !clrstack ו-!pe בכל position ב-trace, לעבור ל-position של אירוע exception ואז לקרוא את ה-stack המנוהל. שאילתת TTD.Calls, שמחפשת קריאות לפי שם סימול, מסתמכת על מידע PDB, לכן באפליקציות .NET השימוש האמין הוא לעקוב אחרי קריאות מעבר לגבולות native כמו P/Invoke, COM ו-Win32 APIs.
- בטוח לשלוח קובץ trace (.run) לחברה אחרת או מחוץ לארגון?
- לא כפי שהוא. הקלטת TTD מכילה את תוכן הזיכרון של התהליך, והתיעוד הרשמי מציין במפורש שהיא יכולה לכלול מידע אישי או סודי כמו נתיבי קבצים, נתוני Registry ותוכן של זיכרון וקבצים. אם שולחים, קודם מבינים מה הוקלט (connection strings, tokens, נתוני לקוחות וכן הלאה), ואז מחליטים על ערוץ העברה מוצפן ועל מיקום אחסון. שיתוף רק של קובץ .run מספיק; קובץ ה-index (.idx) נוצר אוטומטית כש-WinDbg פותח אותו.
- TTD.Calls מחזיר כלום כשאני מחפש פונקציה. למה?
- יש ארבע סיבות עיקריות. ראשית, סימולים: פונקציות במודול בלי PDB נקראות UnknownOrMissingSymbols, ושם המודול עשוי להיות באותיות גדולות, לכן בודקים את שם הסימול בפועל עם פקודת x. שנית, ה-DLL היעד עשוי עדיין לא להיות טעון ב-position הזה; עוברים ל-position אחרי שה-DLL נטען ושואלים שוב. שלישית, אם הפונקציה היא inlined, מנוע השאילתה אינו יכול לעקוב אחריה. רביעית, ה-wildcard עשוי להיות רחב מדי ולהתאים ליותר מדי פונקציות; מצמצמים את התבנית.