DCSIMG
Advanced Debugging Using SOS - Liran Chen's Blog

Liran Chen's Blog

.Net Internals, Debugging, Multithreading - and More!

Advanced Debugging Using SOS

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 ... ועוד ועוד. הפוסט הזה באמת קטן מלהכיל את כל האפשרויות העומדות לפנינו בשלב הזה. מה שכן, שעות של הנאה מובטחת לכל המשפחה.

תוכן התגובה

Liran Chen כתב/ה:

@יוני

לא כל כך הבנתי את השאלה (השילובים כאן של אנגלית/עברית אף פעם לא יוצאים טוב מדי). אתה מציע להשתמש בכלי אחר? מחלקות מה-BCL?..

# June 29, 2009 10:29 PM

Beyond The Spec כתב/ה:

אחד החסרונות/יתרונות של דוט-נט הוא השימוש במנגנון ה-JIT (הלא הוא ה-Just in Time Compilation). למעשה,

# September 26, 2009 2:11 AM
שלח תגובה

(שדה חובה)  

(שדה חובה)  

(אופציונלי)

(שדה חובה) 

Please add 3 and 7 and type the answer here:


Enter the numbers above: