DCSIMG
CLRStack -p" Isn't Always Reliable" - Liran Chen's Blog

Liran Chen's Blog

.Net Internals, Debugging, Multithreading - and More!

CLRStack -p" Isn't Always Reliable"

אחת הפקודות היותר שימושיות שזמינה לנו ב-SOS היא CLRStack!, שביחד עם הדגל p- מנסה להציג לנו את ערכי הפרמטרים שהועברו לכל פונקציה ב-Stack הדוט-נטי שלנו. אני אומר "מנסה" בגלל ש-SOS יציג לנו את הערכים רק אם  הוא חושב שהוא הצליח למצוא את הערכים הנכונים, ואני אומר "חושב" כי במקרים מסויימים הוא פשוט הולך לטעות.

במקרה שבו SOS יגיע למסקנה שהוא לא יודע מה הערך שהועבר בפרמטר, תודפס התשובה <no data>. זה קורה בגלל שלעיתים ל-SOS אין באמת דרך לדעת בוודאות מה היה ערכו של הפרמטר שהועבר לפונקציה רק על ידי הסתכלות על מצב ה-Stack Frame הרלוונטי. במקרה שבו אנחנו משתמשים ב-Calling Convention כגון fast call, שני הפרמטרים הראשונים (מצד שמאל) שמבחינת הגודל מתאימים להכנס לתוך רג'יסטר, יועברו בתוך ECX ו-EDX במקום להיות מועתקים על המחסנית. עבור Member Functions למשל, ערכו של המצביע this נוהג לעבור בתוך ECX. ההתנהגות הזאת יכולה להוביל לכך שאם לקחנו Dump או עצרנו עם הדיבאגר בנקודה מאוחרת מדי בחיי התוכנית, יתכן שערכם של אותם פרמטרים שהועברו כרג'יסטרים כבר נדרסו.
להבדיל ממקרים בהם SOS מגיע למסקנה שהוא לא בטוח מה היה ערכו של הפרמטר, מדי פעם הוא גם הולך פשוט לטעות ולהדפיס את הערכים הלא נכונים. מה שיכול כמובן להוביל ללא מעט כאב ראש אצל מי שמנסה לדבג את התוכנית (הופעה של ערכים "בלתי הגיוניים", קיום של Execution Paths "בלתי אפשריים" וכו'..)
מה שיפה, זה שלא צריך להתאמץ יותר מדי כדי לגרום לעיוות כזה לבוא לידי ביטוי. ניקח את התוכנית הבאה כדוגמה:

    class Program
    {
        static void Main(string[] args)
        {
            Foo a = new Foo();
            a.Work(1, 2, 3, 4, 5);
        }
    }

    class Foo
    {
        [MethodImpl(MethodImplOptions.NoInlining)]
        public void Work(int x, int y, int z, int k, int p)
        {
            // break with debugger here
        }
    }
כעת נריץ את WinDbg, נטען את SOS, ונראה מה יש ל-CLRStack! יש להגיד על הפרמטרים שהועברו ל-Work:
(הפלט עשוי להראות מעט שונה מגרסה לגרסה. בדוגמה הנ"ל נעשה שימוש בגרסה 4.0.30319.1)

0:000> !clrstack -p
OS Thread Id: 0xfbc (0)
Child SP IP       Call Site
0012f3fc 030300b3 ConsoleApplication1.Foo.Work(Int32, Int32, Int32, Int32, Int32) [...\Program.cs @ 24]

    PARAMETERS:
        this (<CLR reg>) = 0x00b3c358
        x (<CLR reg>) = 0x00000001
        y (0x0012f40c) = 0x00000003
        z (0x0012f408) = 0x00000004
        k (0x0012f404) = 0x00000005
        p (0x0012f400) = 0x03030092

0012f414 03030092 ConsoleApplication1.Program.Main(System.String[]) [...\Program.cs @ 16]
    PARAMETERS:
        args = <no data>

0012f648 791421db [GCFrame: 0012f648]

0:000> r ecx   // holds "this" pointer
ecx=00b3c358
0:000> r edx  // holds "x" parameter
edx=00000001


אם כן, לא קשה לראות שמלבד מצביע ה-this ופרמטר x (שהועברו ברג'יסטרים), עבור כל שאר הפרמטרים הודפסו ערכים שגויים. למעשה, אפשר להבחין שלמעשה התרחש כאן "Shift" אחד בערכים של כל הפרמטרים (y קיבל את ערכו של z, בעוד שזה קיבל את ערכו k וכן הלאה..). כדי להבין קצת יותר טוב מה קרה כאן, נדפיס את הזכרון שנמצא בין שני ה-Stack Pointers הרלוונטים (בגרסאות קודמות מיוצג על ידי עמודת ESP במקום Child SP).

0:000> dp /c 1 0012f3fc  0012f414
0012f3fc  0012f414    // EBP
0012f400  03030092 // Return Address
0012f404  00000005  // p
0012f408  00000004  // k
0012f40c  00000003  // z
0012f410  00000002  // y
0012f414  0012f424   // EBP


אם נשווה עכשיו בין כתובות הפרמטרים על המחסנית ש-SOS נתן לנו, לעומת הכתובות האמיתיות שאנחנו רואים בזכרון, נראה שאכן נעשה Shift בודד לכתובות הזכרון של הפרמטרים שיושבים על המחסנית, כך ש-SOS למעשה מציג לנו עבור כל פרמטר את הערך של כתובת הזכרון השכנה לו.
המקרה הזה הוא דוגמא קלאסית לאופי המעט "Buggy" של SOS. זה לא אומר שצריך להפסיק להשתמש ב-CLRStack!, אבל זה כן צריך להזכיר לנו לא לקחת את הפלט שלו כהאמת המוחלטת, ופשוט לדאוג להשאר עירניים כדי לשים לב לכל מיני התנהגויות "מוזרות" כמו זאת.
שלח תגובה

(שדה חובה)  

(שדה חובה)  

(אופציונלי)

(שדה חובה) 

Please add 8 and 7 and type the answer here:


Enter the numbers above: