WinDbg
הוא כלי דיבאגינג בעל יכולות מתקדמות המופץ חינמית על ידי מיקרוסופט.
במקור, הוא יועד לעבודות דיבאג של תוכניות Native, אבל, כשאנחנו מצרפים לו
את חבילת ההרחבה SOS (או: Son of Strike) אנחנו מקבלים תמיכה גם כן בעבודה מול תוכניות Managed, כל שבפועל, בעזרת SOS אנחנו יכולים למעשה גם לדבג את ה-CLR בעצמו.
איך
שאני רואה את זה, השימוש ב-SOS מתחלק ל-2 חלקים: עבודה, ומחקר. הכוונה
היא שבדרך כלל, לא נצטרך להשתמש באותן יכולות (שנדבר עליהן עוד רגע) שהוא
חושף לנו. בדרך כלל המקרים בהם הוא יוכל לעזור לנו, הם בניתוח Crash
Dumps, פתרון Deadlock'ים, ובעיות זכרון. מעבר לנקודות האלה, עבודה עם SOS
יכולה גם לעזור לנו לקבל הבנה טובה ומעמיקה יותר על "איך דברים באמת עובדים"
בכל הקשור ל-CLR. למשל, גם אם קראנו אינספור מאמרים על MethodTables
בדוט-נט, איך הן בנויות, ואיך הן עובדות, עדיין אפשר לקבל איזשהיא תמורה
"מעבר" ברגע שאנחנו באמת רואים איך הכל קורה "תכל'ס" בתוכנית שכתבנו
בעצמנו.
כדי לדבג בעזרת SOS, אנחנו יכולים לבחור להשתמש ב-WinDbg או ב-Visual Studio. בשניהם, עלינו לטעון את רכיב ה-SOS בנפרד, ולאחר מכן, העבודה בשניהם זהה לחלוטין. בפוסט הזה אני אשתמש ב-WinDbg לצורך ההדגמה, אך שוב, העבודה מול SOS היא זהה גם כשמדובר ב-VS.
עבור משתמשי VS, שימו לב שכדי לדבג בעזרת SOS, תצטרכו לאפשר Unmanaged Debugging בפרוייקט שלכם. וכדי לקרוא לפקודות המתאימות, תצטרכו לעבוד דרך חלון ה-Immediate. גם יכולים להיות מספר הבדלים סמנטים לא משמעותים כשטוענים את SOS, אך שוב... לא מדובר בשום דבר משמעותי.
אז כדי להתחיל, תחילה נצטרך להוריד את WinDbg וההרחבה של SOS מכאן.
בהדגמה, ננתח את התוכנית הבאה:
interface IFoo { }
class Foo : IFoo { }
class Program
{
static void Main()
{
Foo a = new Foo();
IFoo b = a;
while (true) { }
}
}
הצעד הראשון לאחר שהפעלנו את WinDbg, הוא לבחור את כתובת
ה-Symbol'ים של Windows שנשתמש בהם. מכיוון שחבילת ה-Symbol'ים לא נמצאת
על המחשב הממוצע, יש לנו 2 אפשרויות לבחור מהן. או שנפנה את WinDbg לכתובת
מיוחדת של מיקרוסופט, שדרכה הוא יוכל להוריד אך ורק את ה-Symbol'ים שהוא
צריך בזמן העבודה, או שנוריד מבעוד מועד את
החבילה המלאה (כמה מאות מגה-בייטים) ונשתמש בה במקום זה. לצורך הדוגמה הזאת, אני אפנה את WinDbg לשרתים של מיקרוסופט. הסינטקס הוא נראה כך:
SRV*_LOCAL_FOLDER_*http://msdl.microsoft.com/download/symbols
אחרי זה, אנחנו כמעט ומוכנים. כעת, נדאג שהתוכנית שלנו רצה ברקע, ונבחר לדבג אותה דרך חלון ה-Attach to Process.
לאחר הצירוף, נקבל את החלון הראשי:
לאחר מכן, עלינו לטעון את חבילת ההרחבה של SOS. בהנחה שאנו עובדים מול גרסה 2 ומעלה של הפריימוורק, נשתמש בפקודה הבאה:
.loadby sos mscorwks
// For VS users, you could use the following command:
.load sos
זהו זה. אנחנו מוכנים (טוב, בערך).
מכאן, אנחנו יכולים להתחיל להשתמש בפקודות השונות ש-SOS יודע לקבל. כדי
לקבל תמונה כללית על האפשרויות העומדות בפנינו, תמיד אפשר לחזור ולהקליד
help! כדי לקבל את רשימת הפקודות הבסיסית.
אז עכשיו נחזור לרגע לתוכנית הבדיקה שלנו. בואו נאמר שאנחנו רוצים לנתח את
מופע האובייקט הבודד שיצרנו (Foo). כדי לעשות זאת, קודם נצטרך לקבל את
כתובת הזכרון שלו, ולשם כך נשתמש בפקודה CLRStack -l! שלמעשה תיתן לנו
dump של כל המשתנים המוקצים של ה-Managed Stack הנוכחי שלנו. אבל, מה
הבעיה? בדרך כלל כשאנחנו מתחילים לדבג עם WinDbg, אנחנו מופנים
ל-Unmanaged Thread שלא קשור לתוכנית שלנו. לכן, קריאה ל-CLRStack! במצב
כזה תכשל מאחר ואין שום Managed Stack שאפשר לנתח בכלל. על כן, עלינו
לעבור ל-Managed Thread אחר. אז מה שנעשה, זה מה שקודם כל נרצה, זה לקבל
את רשימת ה-Thread'ים הקיימים בתוכנית שלנו. לשם כך נשתמש בפקודה
Threads!. הפלט עבור התוכנית שלנו נראה כך:
0:003> !Threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
PreEmptive GC Alloc Lock
ID OSID ThreadOBJ State GC Context Domain Count APT Exception
0 1 33c 003b90b8 200a020 Enabled 01c35618:01c35fe8 003b4da0 0 MTA
2 2 15e8 003c8bb8 b220 Enabled 00000000:00000000 003b4da0 0 MTA (Finalizer)
במקרה הזה, הת'רד שמעניין אותנו הוא הת'רד הראשי, שנמצא תחת מספר 0. שימו
לב שהעמודה שמעניינת אותנו בהקשר של בחירת ת'רדים ב-WinDbg היא השמאלית
ביותר. היא מסמלת עבורנו את ה-ID שדרכו נוכל לזהות ת'רדים כשנבקש להחליף
ביניהם.
אז כדי לעבור לת'רד המתאים נשתמש בפקודה THREAD_ID s~. במקרה שלנו, אותו Id יהיה 0 מאחר והוא מזהה את הת'רד הראשי.
אחרי שעברנו ל-Managed Thread המתאים, נוכל להשתמש ב CLRStack -l! בשביל לקבל את רשימת המשתנים הנמצאת על ה-Stack שלנו.
הפלט הפעם נראה כך:
0:000> !CLRStack -l
OS Thread Id: 0x33c (0)
ESP EIP
002af310 003800d8 Program.Main()
LOCALS:
0x002af318 = 0x01c33758
0x002af314 = 0x01c33758
0x002af31c = 0x00000001
002af544 6d911b4c [GCFrame: 002af544]
והנה, אנחנו רואים שקיימים לנו 3 משתנים על ה-Stack. שני המשתנים הראשונים
הם הרפרנסים של Foo, אפשר לשים לב שהם מצביעים לאותו המקום, אותו מופע של
Foo (שימו לב שמידע כזה, כמו כתובת זכרון של אובייקט הנמצא על ה-GC Heap
לא ניתן
לקבל דרך כלים קונבנציונלים אחרים, גם כשמדובר למשל ב-Unsafe Code
Blocks). המשתנה השלישי הוא לא אחר מאשר המשתנה שיצרנו תחת לולאת ה-while.
כן, מדובר באותו Constant True, שבזמן הריצה זקוק "לאיזשהו" ייצוג בזכרון,
לכן נוצר לנו משתנה חדש שקיבל את הערך 1.
כדי להתחיל לנתח את האובייקט שלנו, נשתמש בפקודה do!, ובתור פרמטר נעביר לה את הכתובת של האובייקט.
0:000> !do 0x01c33758
Name: Foo
MethodTable: 001130a4
EEClass: 00111368
Size: 12(0xc) bytes
(F:\Users\Liran\Documents\Visual Studio
2005\Projects\ConsoleApplication2\ConsoleApplication2\bin\Debug\ConsoleApplication2.exe)
Fields:
None
אפשר לשים לב כאן לעוד פרט מעניין. למרות שהגדרנו את הטיפוס שלנו ללא שום
שדות, אנחנו יכולים לראות שהגודל שהוא תופס ב-Heap הוא 12 בתים. הסיבה לכך
היא שבחישוב גודל האובייקט, נלקחים גם המצביע ל-MethodTable שנמצא בו, וגם
ה-ObjHeader הנמצא לפניו בזכרון (פירוט נוסף בפוסט
הזה).
אז רק כדי לסגור מעגל עם תחילת הפוסט, נאמר שאנחנו רוצים לדלות מידע על
איך ה-MethodTable שלנו בנוייה, ואיך הקומפיילר החליט לסדר את ה-VTable
שנמצאת בתוכה (מדובר בתהליך סבוך שאני מקווה לדבר עליו באחד הפוסטים
הבאים).
0:000> !DumpMT -md 001130a4
EEClass: 00111368
Module: 00112c5c
Name: Foo
mdToken: 02000003 (F:\Users\Liran\Documents\Visual Studio
2005\Projects\ConsoleApplication2\ConsoleApplication2\bin\Debug\ConsoleApplication2.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 1
Slots in VTable: 5
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
6c4d6ab0 6c354944 PreJIT System.Object.ToString()
6c4d6ad0 6c35494c PreJIT System.Object.Equals(System.Object)
6c4d6b40 6c35497c PreJIT System.Object.GetHashCode()
6c547540 6c3549a0 PreJIT System.Object.Finalize()
003800f8 0011309c JIT Foo..ctor()
מכאן, השמיים הם הגבול. אנחנו יכולים להתחיל לנתח כל חלקיק הכי קטן
בזכרון. אם זה ה-MethodTable/EEClass/GCDesc/SyncBlocks ... ועוד ועוד.
הפוסט הזה באמת קטן מלהכיל את כל האפשרויות העומדות לפנינו בשלב הזה. מה
שכן, שעות של הנאה מובטחת לכל המשפחה.