תשתית בדיקות למקרי קצה ב-Windows עם Application Verifier

· עודכן בתאריך: · · פיתוח Windows, חקירת תקלות, מצלמה תעשייתית, Application Verifier, בדיקות מקרי קצה, דליפת handle

Application Verifier הוא כלי חזק כשרוצים לחשוף מראש חריגות שמתרחשות בקוד native של Windows או בגבול ה-Win32. בפרט, כשרוצים לבדוק חריגות handle, השחתת heap, ו-failure path בזמן מחסור במשאבים, אפשר לחשוף בעזרתו מהר מאוד בעיות שלא נראות בבדיקות של המסלול הרגיל בלבד.

בפרק הראשון, חקירת קריסה בהפעלה ממושכת של מצלמה תעשייתית — פרק דליפת ה-handle, סידרנו מקרה שבו חקרנו אפליקציית בקרה שקורסת אחרי הפעלה ממושכת, וגילינו שהגורם היה דליפת handle. עם זאת, חיזוק הלוגים לבדו הוא רק חצי מהעבודה. מה שבאמת רצינו הוא לבדוק מראש האם, גם אם בעתיד תתרחש טעות תכנות בלתי צפויה שתגרום לדליפת זיכרון, דליפת handle, כשל באמצע או שכחת שחרור, המצב יישאר כזה ש”אפשר לדעת מה קרה”.

לשם כך השתמשנו ב-Application Verifier. זה כלי שמאפשר להכניס בדיקות ו-fault injection בזמן ריצה, עבור עיבוד שרץ בקוד native של Windows או בגבול ה-Win32. מה שנוח במיוחד בעבודה בפועל הוא היכולת לגרום מראש לשברים שדומים למחסור בזיכרון או במשאבים, בלי באמת לצרוך את כל זיכרון המכונה.

בפרק השני נסדר מהו Application Verifier, מה אפשר לעשות איתו, ואיך משלבים אותו בתשתית בדיקות למקרי קצה, בהקשר של אפליקציית בקרת מצלמה תעשייתית.

תוכן עניינים

  1. קודם כול, המסקנה (במשפט אחד)
  2. מהו Application Verifier
    • 2.1. בקיצור, מה זה
    • 2.2. באילו מצבים זה עוזר
    • 2.3. מה היתרון בזה
    • 2.4. מהשגה ועד הפעלה בפועל
  3. מה אפשר לעשות עם Application Verifier
    • 3.1. Basics:‏ Handles /‏ Heaps /‏ Locks /‏ Memory /‏ TLS וכדומה
    • 3.2. Low Resource Simulation: הקדמת מחסור בזיכרון ובמשאבים
    • 3.3. Page Heap וה-debugger
    • 3.4. !avrf /‏ !htrace / הלוגים
  4. למה זה הופעל הפעם
    • 4.1. המטרה היא לא רק “למצוא באג”
    • 4.2. לגרום לתופעה שדומה למחסור בזיכרון
    • 4.3. לבדוק אם אפשר לעקוב כשמתרחשת חריגת handle
  5. איך גורמים לתופעה שדומה למחסור בזיכרון או במשאבים
    • 5.1. הרעיון מאחורי Low Resource Simulation
    • 5.2. מה אפשר לגרום לו להיכשל
    • 5.3. איך מיישמים את זה בעבודה בפועל
  6. איך מסתכלים על חריגת handle
    • 6.1. בדיקת Handles
    • 6.2. הסתכלות על ה-stack של open /‏ close עם !htrace
    • 6.3. איך משלבים את זה עם הלוג העצמי
  7. איך בונים תשתית בדיקות למקרי קצה
    • 7.1. להטות את יחידת ההרצה לכיוון harness
    • 7.2. לחלק את תפריט הבדיקות
    • 7.3. מה אוספים
    • 7.4. תנאי ההצלחה
    • 7.5. נקודות לתשומת לב
  8. חלוקה גסה בין המצבים
  9. סיכום
  10. מקורות

מפת הידע של המאמר

‏Application Verifier הוא כלי אימות בזמן ריצה שמנטר עבור אפליקציות user-mode ב-Windows את השימוש ב-API של מערכת ההפעלה וניהול משאבים, ומזהה שימוש ב-handle לא תקין והרס ערמה כ-verifier stop. עם fault injection שנקרא Low Resource Simulation אפשר לגרום לכשל בהסתברות לקריאות API שונות, וכך לדרוך מראש על ה-failure path של מצב משאבים נמוך בלי לגרום למחסור זיכרון אמיתי. כשמפעילים בדיקת Handles, מעקב handle (‏handle tracing) מופעל אוטומטית, ואפשר לבדוק את היסטוריית ה-handle עם ‏!htrace ואת מצב ההגדרות וה-stop עם ‏!avrf ב-WinDbg. ה-EXE היעד דורש הגדרה לפני ההפעלה ואי אפשר להוסיף בדיעבד, ומכיוון שלא מציאותי להסתמך רק על Application Verifier לחקירת דליפת handle באפליקציה שפועלת זמן ארוך, השילוב של harness EXE ייעודי עם לוגים עצמיים הוא התפעול המעשי.

מפת הידע של בדיקות למקרי קצה עם Application Verifierתרשים שמראה איך Application Verifier מזהה שימוש ב-handle לא תקין והרס ערמה כ-verifier stop, באמצעות fault injection של Low Resource Simulation ומעקב handle; איך מאתרים את הסיבה עם ‏!avrf ו-‏!htrace ב-WinDbg; ואת התפעול של הפעלה עבור harness EXE ייעודי, לצד הצורך בשילוב עם לוגים עצמיים בחקירת דליפת handle בהפעלה ממושכת.משתמש במשתמש במשתמש במשתמש במשתמש בנבדק באמצעותנבדק באמצעותנבדק באמצעותנבדק באמצעותנבדק באמצעותעלול לגרום לעלול לגרום למענה מומלץ לשימוש לא מומלץ למחייבמוגדר באמצעותמענה מומלץ לApplication Verifierבדיקות מסלולי כשלהזרקת תקלות (fault injection)Low Resource Simulationמעקב handle‏ (handle tracing)WinDbgשימוש ב-handle לא חוקי (invalid handle)השחתת ערימה (heap corruption)‏page heapverifier stop‏!avrf (פקודת הרחבה של WinDbg)דפוס ה-harness EXEדליפת handle

בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 17, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle

1. קודם כול, המסקנה (במשפט אחד)

  • Application Verifier הוא כלי שמקל על מציאת שימוש שגוי בזמן ריצה בגבול הבלתי-מנוהל / native של Windows
  • היתרון שלו הוא לא רק “למצוא באג”, אלא לגרום מראש למקרי קצה שבדרך כלל קשה שיצוצו
  • ב-Handles אפשר לזהות invalid handle, ב-Heaps לחשוף השחתת heap, וב-Low Resource Simulation להזריק תקלות שדומות למחסור בזיכרון או במשאבים
  • להטיל את כל חקירת הדליפה של EXE שרץ הפעלה ממושכת רק על Application Verifier היא גישה גרועה; עדיף לשלב עם Handle Count ולוג עצמי של מחזור חיי המשאב
  • בתשתית בדיקות למקרי קצה, נוח יותר להריץ בנפרד הרצת verifier של המסלול הרגיל והרצה עם fault injection
  • גם כשרוצים לבדוק DLL, היעד שמפעילים עליו את Application Verifier הוא ה-EXE לבדיקה שמריץ בפועל את אותו DLL

בקיצור, Application Verifier הוא כלי שמוציא החוצה “באגים מציקים” שמסתובבים סביב native /‏ Win32 של Windows. בפרט בעולם שבו SDK ילידי, P/Invoke ו-Win32 API מתערבבים באופן שגרתי — כמו באפליקציית בקרת התקנים — זה מתאים מאוד.

שני התפקידים של Application Verifierתרשים המראה ש-Application Verifier מגלה שימוש שגוי בגבול הבלתי-מנוהל ומקדים מקרי קצה שבדרך כלל קשה שיצוצו, ומאפשר לזהות invalid handle והשחתת heap וגם להזריק מצבים שדומים למחסור בזיכרון.Application Verifierגילוי שימוש שגוי בגבול הבלתי-מנוהלהקדמת מקרי קצה נדיריםinvalid handle או השחתת heapהזרקת מצב שדומה למחסור בזיכרון

איור 1: הפעולה של Application Verifier נשענת על שני עמודים — “גילוי שימוש שגוי” ו”הקדמת מקרי קצה”.

2. מהו Application Verifier

2.1. בקיצור, מה זה

Application Verifier הוא כלי אימות בזמן ריצה עבור אפליקציות user-mode ב-Windows. הוא מנטר את השימוש ב-API של מערכת ההפעלה ואת אופן הטיפול במשאבים באפליקציה בזמן ריצה, ומאפשר לזהות שימוש חשוד וגם להזריק כשל בכוונה.

בשונה מ”ניתוח סטטי” או “בדיקת יחידה”, זה כלי שמראה איך זה נשבר בפועל כשעוברים במסלול הקוד הזה. לכן הוא מתאים לחשיפת failure path שלא נראה בבדיקות פונקציונליות רגילות.

מבנה הבדיקה עם harness ו-Application Verifierתרשים המראה שה-harness מפעיל את אפליקציית הבקרה או ה-wrapper של ה-SDK, ש-Application Verifier מנטר, וש-Application Verifier מייצר verifier stop, פלט debugger ולוגי AppVerifier, בעוד האפליקציה עצמה מייצרת גם לוג מובנה עצמאי.harness לבדיקהאפליקציית הבקרה / wrapper של ה-SDKApplication VerifierWin32 API / DLL native / משאבי OSverifier stopפלט ה-debuggerלוגי AppVerifierלוג מובנה עצמאי

איור 2: ה-harness מריץ את אפליקציית הבקרה, Application Verifier מנטר אותה, והתוצאות נשארות כ-verifier stop, פלט debugger ולוגים.

2.2. באילו מצבים זה עוזר

זה עוזר במיוחד במצבים כאלה:

  • קוראים ל-DLL native או ל-SDK של מצלמה
  • חוצים גבול של P/Invoke או COM
  • משתמשים הרבה, במישרין או בעקיפין, ב-handle, heap, lock וזיכרון וירטואלי
  • במסלול הרגיל כמעט אף פעם לא קורס, אבל נראה שניהול מחזור החיים מתקלקל דווקא במסלול החריג
  • “לפעמים מחזיר כשל מוזר” מופיע לפני “קורס”

מצד שני, זה לא כלי למעקב אחרי object graph בעולם managed טהור בלבד. לכן, גם באפליקציית ‏C#, אם הגבול מול SDK native או Win32 עבה, זה יעיל מאוד, אבל זה לא כלי שרואה לבד את כל דליפת ה-heap ה-managed הטהור.

ההבחנה מתי Application Verifier עוזרתרשים המראה שכאשר הבעיה נמצאת בשכבת SDK native או בגבול Win32, Application Verifier עוזר, אך כשמדובר ב-object graph של עולם managed טהור, זה מחוץ לתחום שלו ונדרש כלי אחר.SDK native או גבול Win32object graph של managed טהורבאיזו שכבה הבעיהApplication Verifier עוזרמחוץ לתחום, כלי אחר

איור 3: קו הגבול הוא עובי גבול ה-native /‏ Win32, וזה לא כלי שרואה לבד את עולם ה-managed הטהור.

2.3. מה היתרון בזה

בעבודה בפועל, שלושת היתרונות המרכזיים הם בערך אלה:

  1. ניתן לעצור מוקדם שימוש שגוי בגבול ה-native
    • invalid handle
    • heap corruption
    • lock misuse
    • שימוש שגוי ב-API של זיכרון וירטואלי, וכדומה
  2. ניתן להקדים שברים שמופיעים רק במחסור במשאבים
    • המקבילה ל-malloc נכשלת מדי פעם
    • CreateEvent או CreateFile נכשלים מדי פעם
    • VirtualAlloc נכשל
  3. קל לעקוב בשילוב עם debugger
    • !avrf
    • !htrace
    • !heap -p -a
    • לוג של verifier stop

מה שמקשה באפליקציית בקרת התקנים הוא “לא לדעת מה קרה במסלול החריג”. Application Verifier מאוד יעיל בצמצום אותה “אי-ידיעה”.

שלוש הנקודות המועילות בעבודה בפועלתרשים המראה שהיתרונות בעבודה בפועל הם עצירה מוקדמת של שימוש שגוי, הקדמת שברים שמופיעים רק במחסור במשאבים, וקלות מעקב בשילוב עם debugger.Application Verifierעצירה מוקדמת של שימוש שגויהקדמת שברים נדיריםקלות מעקב עם debuggerהרחבות כמו avrf ו-htrace

איור 4: היתרונות בעבודה בפועל מצטמצמים לשלושה: גילוי מוקדם, הקדמת מקרי קצה, ושילוב עם debugger.

2.4. מהשגה ועד הפעלה בפועל

קודם נסדר את השגת הכלי. בלי זה, כל מה שיבוא אחר כך יישאר תיאורטי בלבד.

Application Verifier מגיע כחלק מה-Windows SDK. הוא לא מגיע עם Windows בפני עצמו, ולכן צריך להריץ את מתקין ה-SDK ולסמן את התיבה “Application Verifier” במסך בחירת התכונות. שם קובץ ההרצה הוא appverif.exe.

יש שלוש דרישות קדם לשימוש:

  • המשתמש שמריץ צריך להיות חבר בקבוצת ה-Administrators של אותה מכונה
  • ‏ARM64EC לא נתמך
  • היעד לאימות הוא קוד בלתי-מנוהל (native)

הבנת היחס בין ה-GUI לשורת הפקודה מונעת בלבול:

  מה זה עושה
GUI (‏appverif.exe) כותב לרישום (registry) את שם ה-EXE היעד ואת שילוב הבדיקות שמפעילים
שורת פקודה (‏appverif -enable ...) כותב בפקודה בדיוק את אותה הגדרת רישום
בזמן ריצה כשה-EXE היעד עולה, ה-DLL של ה-verifier נטען לפי אותה הגדרה, ונכנס hook ל-Win32 API

כלומר, בשתי הדרכים עושים בדיוק אותו דבר. השימוש המקובל הוא GUI בפעם הראשונה שעושים זאת ידנית, ושורת פקודה ב-CI או בסקריפט.

היחס בין ה-GUI לשורת הפקודהתרשים המראה שגם ה-GUI וגם שורת הפקודה רק כותבים אותה הגדרת רישום, ושכשה-EXE היעד עולה, ה-DLL של ה-verifier נטען לפי אותה הגדרה ונכנס hook.GUI (appverif.exe)כתיבת ההגדרה לרישוםשורת פקודההפניה בעלייה של ה-EXE היעדטעינת ה-DLL של ה-verifier וכניסת hook

איור 5: גם ה-GUI וגם שורת הפקודה רק כותבים את אותה הגדרת רישום, וה-hook נכנס רק בעליית ה-EXE היעד.

פעולת ה-GUI: לוחצים ימני על אזור Applications בצד שמאל, בוחרים “Add Application” ומוסיפים את ה-EXE היעד, מסמנים למשל את Basics באזור Tests בצד ימין, ולוחצים “Save”. לביטול, לוחצים ימני על אותו אזור Applications, בוחרים “Delete Application”, ולוחצים “Save”.

מכאן נובעות שתי מגבלות חשובות:

  • אי אפשר להפעיל בדיעבד על תהליך שכבר רץ. ה-hook נכנס בזמן טעינת ה-DLL, ולכן הסדר הוא להגדיר לפני ההפעלה
  • ההגדרה נשארת עד שמוחקים אותה במפורש. אם משאירים אותה במחשבה “ניסיתי רק פעם אחת”, אותו EXE ימשיך לרוץ תמיד תחת ה-verifier באותה מכונה

בנוסף, לוג הזיהוי נשמר כברירת מחדל בפורמט בינארי בתיקייה %USERPROFILE%\AppVerifierLogs, ואפשר להמיר אותו ל-XML ב-GUI או בשורת הפקודה כדי לרכז אותו.

סדר ההפעלה ואופן שמירת ההגדרהתרשים המראה שה-hook נכנס בזמן טעינת ה-DLL ולכן אי אפשר להוסיף אותו לתהליך שכבר רץ, ושהסדר הוא להגדיר ואז להפעיל, וההגדרה נשארת עד שמוחקים אותה במפורש.כתיבת ההגדרההפעלת ה-EXE היעדפועל תחת ה-verifierההגדרה נשארת עד שמוחקיםאם משאירים, זה תמיד רץ תחת ה-verifierתהליך שכבר רץאי אפשר להפעיל בדיעבד

איור 6: לא ניתן לשנות את הסדר “להגדיר ואז להפעיל”, וההגדרה נשארת על המכונה עד שמוחקים אותה במפורש.

3. מה אפשר לעשות עם Application Verifier

3.1. Basics:‏ Handles /‏ Heaps /‏ Locks /‏ Memory /‏ TLS וכדומה

הסט הבסיסי של Application Verifier הוא Basics. כאן מרוכזות הבדיקות הנפוצות בעבודה בפועל.

שכבה מה נבדק שימוש בהקשר הזה
Handles שימוש ב-invalid handle האם נתקלים ב-handle שנסגר / פגום
Heaps השחתת heap חשיפת השחתת buffer או use-after-free בגבול ה-SDK native
Leak משאבים שלא שוחררו עד רגע ה-unload של ה-DLL בדיקת harness קצר-חיים או מקרים שכוללים unload
Locks /‏ SRWLock שימוש שגוי ב-lock בדיקת תחרות (race) בין reconnect ל-shutdown
Memory שימוש שגוי ב-VirtualAlloc /‏ MapViewOfFile וכדומה בדיקת חריגות סביב buffer גדול או זיכרון משותף
TLS שימוש שגוי ב-API של Thread Local Storage ביטוח לקוד native עם גבולות ת’רד מורכבים
Threadpool עקביות ה-API של ה-threadpool ומצב ה-worker סיוע כשיש הרבה callback או עיבוד אסינכרוני

הנקודה היא לא “קוראים אחרי הקריסה ומבינים”, אלא “עוצרים שימוש חשוד במקום, מיד”. בתקלות מסוג הפעלה ממושכת, ההקדמה הזו יעילה מאוד.

רעיון הגילוי המוקדם של Basicsתרשים המראה שבמקום לקרוא לוגים אחרי קריסה ולנחש, Basics עוצר שימוש חשוד במקום, וכך תקלת הפעלה ממושכת נחשפת מוקדם.שימוש חשוד ב-APIקבוצת הבדיקות של Basicsעצירה במקוםחשיפת הבעיה מוקדםבעבר רק קוראים אחרי הקריסה

איור 7: הערך של Basics הוא הפיכת “קריאה אחרי הקריסה” ל”עצירה במקום”.

3.2. Low Resource Simulation: הקדמת מחסור בזיכרון ובמשאבים

זה החלק שבאמת נוח בעבודה בפועל. מכיוון שאפשר לגרום לתופעה שדומה למחסור בזיכרון או במשאבים, בלי באמת לצרוך את כל ה-RAM.

הרעיון פשוט:

  • קריאה ל-API מסוימת
  • בהסתברות קבועה
  • נכשלת בכוונה

כך אפשר לעבור ב-error path שבדרך כלל כמעט אף פעם לא עוברים בו.

באופן קונקרטי, קל לגרום בכוונה לתופעות כאלה:

  • HeapAlloc או VirtualAlloc נכשלים
  • CreateFile נכשל
  • CreateEvent נכשל
  • MapViewOfFile נכשל
  • הקצאת OLE/COM כמו SysAllocString נכשלת

זה נוח בהרבה מלנסות לגרום למחסור אמיתי בזיכרון ולסבל של המכונה כולה. יתרה מכך, אפשר גם להזריק כשל ממוקד רק ל-DLL מסוים. במבנה שבו wrapper עצמאי מתערבב עם SDK של ספק, כמו באפליקציית בקרת התקנים, זה מתאים מאוד לעבודה בפועל.

המנגנון של Low Resource Simulationתרשים המראה שקריאה מסוג מסוים ל-API נכשלת בכוונה בהסתברות קבועה, וכך עוברים ב-error path שבדרך כלל לא עוברים בו, ואפשר גם לצמצם את היעד ל-DLL מסוים.כןלאקריאה ל-APIפגעו בהסתברות?מחזירים כשל בכוונהעיבוד רגילמעבר ל-error path נדיראפשר לצמצם ל-DLL מסוים

איור 8: הזהות האמיתית של Low Resource Simulation היא fault injection שגורם לקריאת API להיכשל בהסתברות קבועה.

3.3. Page Heap וה-debugger

כדי לראות השחתת heap, השילוב של Heaps עם page heap חזק. ה-full page heap במיוחד משתמש ב-guard page, כך שהיתרון שלו הוא קלות עצירה ברגע ההשחתה עצמו.

עם זאת, זה כבד למדי. במקום ריצה כוללת וממושכת, נוח יותר לצמצם לתרחיש שקרוב לשחזור ולהריץ תחת debugger.

לכן, בתפעול, החלוקה הבאה ריאלית:

  • קודם, לפגוע רחב עם Basics
  • כשה-heap נראה חשוד, להשתמש ב-full page heap
  • כשזה כבד מדי, לרדת ל-light page heap
  • בדיקה ארוכת טווח ברמת ייצור, להסתכל בעיקר על הלוג העצמי

בסופו של דבר, AppVerifier הוא לא שרביט קסמים אלא כלי שמחליף להב לפי המצב.

זרימת בחירת page heapתרשים המראה שקודם פוגעים רחב עם Basics, ואם ה-heap חשוד עוברים ל-full page heap שעוצר ברגע ההשחתה, אם זה כבד מדי יורדים ל-light page heap, ובדיקה ארוכת טווח ברמת ייצור מסתכלת בעיקר על הלוג העצמי.כןלאכןלאפגיעה רחבה עם Basicsה-heap חשוד?עצירה עם full page heapבדיקה ארוכת טווח בלוג עצמיזה כבד מדי?ירידה ל-light page heapשחזור מקומי תחת debugger

איור 9: page heap הוא לא כלי לשימוש קבוע; פוגעים רחב עם Basics, ורק אז מחליפים להב לפי המצב.

3.4. !avrf /‏ !htrace / הלוגים

קודם מונח אחד. ה-verifier stop שהופיע כבר כמה פעמים עד עכשיו הוא אירוע גילוי שיוצא כש-Application Verifier קובע ש”השימוש הזה מוזר”. זה לא רק שורת לוג — התכונה שלו היא שאם רצים תחת debugger, הוא עוצר (break) מיד במקום. ל-stop יש מספר, והוא מוצג בצורה כמו VERIFIER STOP 00000300. יש stop שאפשר להמשיך אחריו, ויש stop שאי אפשר להמשיך אחריו (חייבים לסיים את התהליך).

Application Verifier לא מסתיים בהוצאת ה-stop. יש הרחבות debugger ולוגים, ולכן קל יותר לעקוב אחרי מה שקרה.

  • !avrf
    • הצגת הגדרות ה-verifier הנוכחיות וה-stop שקורה עכשיו
  • !htrace
    • הצגת ה-stack של open /‏ close / הפניה לא חוקית (invalid reference) של handle
  • !heap -p -a
    • שילוב עם page heap כדי לעקוב אחרי בלוק heap פגום
  • לוג ה-AppVerifier
    • שמירת הלוג שנוצר בזמן ה-stop

כשמפעילים בפרט את Handles, נוח מאוד ש-handle tracing מופעל אוטומטית. כך קל יותר לעקוב אחר כך אחרי “איפה נפתח ה-handle הזה ואיפה נסגר”.

הזרימה מ-verifier stop ועד חקירהתרשים המראה שגילוי שימוש חשוד מוציא verifier stop עם מספר, שתחת debugger עוצר במקום, ושאפשר לבדוק עם avrf את ההגדרות וה-stop ועם htrace את היסטוריית ה-handle.גילוי שימוש חשודverifier stop (עם מספר)עצירה במקום תחת debuggeravrf לבדיקת הגדרות ו-stophtrace לבדיקת היסטוריית handleיש stop שממשיכים ממנו ויש שלא

איור 10: verifier stop הוא לא רק שורת לוג; תחת debugger הוא עוצר במקום והופך לנקודת פתיחה לחקירה.

4. למה זה הופעל הפעם

4.1. המטרה היא לא רק “למצוא באג”

המטרה הפעם לא הייתה סתם “למצוא באג אחד עם AppVerifier”. בניסוח מעשי יותר, מה שרצינו לבדוק היה:

  • אם בעתיד תתרחש דליפת משאב ב-failure path אחר
  • האם ההקשר נשאר בלוג כמו שצריך
  • האם אפשר לעקוב עד הסוף בשילוב עם מידע ה-debugger
  • שהמצב לא יהפוך ל”לא ברור מה קרה”

כלומר, השתמשנו בזה לא רק כגלאי, אלא כבדיקה של תשתית התצפית.

4.2. לגרום לתופעה שדומה למחסור בזיכרון

לגרום למחסור אמיתי בזיכרון על מחשב פיתוח רגיל זה די מסובך. יתרה מכך, אם המכונה כולה נהיית לא יציבה, הבדיקה עצמה מתמלאת ברעש.

לכן, השתמשנו ב-Low Resource Simulation כדי לדרוך בכוונה על ה-failure path שסביר שיתרחש במחסור בזיכרון או במשאבים.

כך קל יותר לענות על שאלות כאלה:

  • אם CreateEvent נכשל, האם cameraId ו-phase נשארים בלוג
  • האם ה-clean up רץ כמו שצריך אחרי אתחול חלקי
  • אם VirtualAlloc נכשל, האם ה-retry לא נשבר
  • האם ה-handle חוזר כשכשל CreateFile בנתיב השמירה

מה שחשוב להדגיש הוא שהמטרה אינה לגרום לחריגה כשלעצמה, אלא שאפשר לקרוא איך זה נשבר בזמן החריגה.

תשתית התצפית שבודקים באמצעות fault injectionתרשים המראה שדריכה בכוונה על כשלים באמצעות Low Resource Simulation נועדה לבדוק אם ההקשר נשאר בלוג, אם ה-clean up רץ, ואם ה-retry לא נשבר, כאשר המטרה היא לא לגרום לחריגה אלא לוודא שאפשר לקרוא איך זה נשבר.דריכה בכוונה על כשלהאם ההקשר נשאר בלוגהאם ה-clean up רץהאם ה-retry לא נשברמצב שאפשר לקרוא איך זה נשבר

איור 11: המטרה של fault injection היא לא לגרום לחריגה, אלא לבדוק אם אפשר לקרוא איך זה נשבר בזמן החריגה.

4.3. לבדוק אם אפשר לעקוב כשמתרחשת חריגת handle

כמו דליפת ה-handle שהופיעה בפרק הראשון, גם בעולם ה-handle המקום שבו קורסים לבסוף נוטה להיות שונה מהגורם האמיתי.

לכן, מה שרצינו לוודא היה:

  • כש-invalid handle stop יוצא, האם אפשר לעקוב עם !htrace אחרי open /‏ close
  • האם זה מתקשר ל-resourceId /‏ sessionId /‏ phase של הלוג העצמי
  • האם handle count חוזר אחרי כשל
  • כש-harness הוא תהליך קצר-חיים, האם קל לראות את ההפרש בדליפה

כשמגיעים עד לכאן, אפשר לעבור מסתם “יצא באג” עד “באיזו אחריות ניהול מחזור החיים התקלקל”.

בדיקת יכולת המעקב בזמן חריגת handleתרשים המראה שכש-invalid handle stop יוצא, בודקים אם htrace עוקב אחרי open ו-close, אם זה מתקשר להקשר הלוג העצמי, ואם handle count חוזר, ומגיעים לזיהוי האחריות שבה ניהול מחזור החיים התקלקל.invalid handle stopמעקב open ו-close עם htraceקישור להקשר הלוג העצמיבדיקת החזרת handle countזיהוי האחריות שבה ניהול מחזור החיים התקלקל

איור 12: בחריגת handle לא עוצרים ב”יצא באג”, אלא בודקים אם אפשר להגיע עד לאחריות שבה ניהול מחזור החיים התקלקל.

5. איך גורמים לתופעה שדומה למחסור בזיכרון או במשאבים

5.1. הרעיון מאחורי Low Resource Simulation

Low Resource Simulation הוא בעצם fault injection. זה לא ממש שחזור מדויק של סביבת משאבים דלים, אלא הכנסה מלאכותית של כשלי API אופייניים שקורים במחסור במשאבים.

לכן, מקומות השימוש די ברורים:

  • אימות סיום נכון (clean up) ב-failure path
  • בדיקת עמידות ה-retry / reconnect
  • בדיקת אתחול שמשלב הצלחה חלקית וכשל חלקי
  • לוודא שגם “כשל שבדרך כלל לא קורה” נשאר בלוג

הטריק כאן הוא לא לגרום להכול להיכשל מההתחלה. אם פותחים הכול בבת אחת, הלוג מתפוצץ ולא ברור “על מה בכלל מסתכלים”.

איך מצמצמים את fault injectionתרשים המראה שאם גורמים להכול להיכשל מההתחלה הלוג מתפוצץ ולא ברור על מה מסתכלים, ולכן פותחים בצמצום רק את הכשלים שקרובים ל-failure path שרוצים לראות.גרימת כשל לכול מההתחלההלוג מתפוצץ ובלתי קריאפתיחת הכשלים הרצויים בלבדברור מה מסתכלים עליו

איור 13: הטריק ב-fault injection הוא לא לפתוח הכול; מצמצמים לכשלים שקרובים ל-failure path הרצוי.

5.2. מה אפשר לגרום לו להיכשל

ב-Low Resource Simulation אפשר לגרום באופן טיפוסי לסוגי ה-API הבאים להיכשל בהסתברות:

סוג דוגמה דוגמה באפליקציית בקרת התקנים
Heap_Alloc הקצאת heap buffer זמני, metadata של תמונה, הקצאה פנימית ב-wrapper של ה-SDK
Virtual_Alloc הקצאת זיכרון וירטואלי buffer פריימים גדול, ring buffer
File CreateFile וכדומה פתיחת נתיב שמירה או קובץ לוג
Event CreateEvent וכדומה הודעת frame ready, סנכרון stop/reconnect
MapView CreateMapView וכדומה זיכרון משותף או memory mapped file
Ole_Alloc SysAllocString וכדומה גבול COM /‏ OLE
Wait סדרת WaitForXXX סביב כשל בהמתנה סינכרונית
Registry גישה לרישום קריאה וכתיבה של הגדרות או הגדרות דרייבר

בעבודה בפועל, במקום לפתוח הכול בבת אחת, החשוב הוא לצמצם ולפתוח קודם את מה שקרוב ל-failure path שרוצים לראות הפעם.

5.3. איך מיישמים את זה בעבודה בפועל

בתור דוגמה לשורת פקודה, זה נראה בערך כך:

appverif /verify CameraHarness.exe
appverif /verify CameraHarness.exe /faults
appverif -enable lowres -for CameraHarness.exe -with heap_alloc=20000 virtual_alloc=20000 file=20000 event=20000
appverif -query lowres -for CameraHarness.exe

אין טעם להעתיק-הדביק בלי להבין את הכוונה, אז נכתוב שורה-שורה מה כל אחת עושה:

פקודה מה היא עושה
appverif /verify CameraHarness.exe מפעילה את קבוצת הבדיקות של Basics עבור CameraHarness.exe
appverif /verify CameraHarness.exe /faults בנוסף לזה, מפעילה fault injection. אבל היעד מוגבל ל-OLE_ALLOC ו-HEAP_ALLOC בלבד
appverif -enable lowres -for CameraHarness.exe -with heap_alloc=20000 ... מפעילה את lowres (‏Low Resource Simulation) ומציינת בנפרד את סוג ה-API שנכשל ואת ההסתברות
appverif -query lowres -for CameraHarness.exe מציגה מה מוגדר כרגע ובאיזו הסתברות
appverif /n CameraHarness.exe מוחקת את ההגדרה של אותו EXE (אותה מטרה כמו -disable * -for או -delete settings -for)

נבין גם איך קוראים את הארגומנטים.

  • ההסתברות ניתנת ביחידות של אחד ממיליון. אפשר לציין מספר שלם בין 0 ל-1,000,000, ו-20000 הוא 20000 / 1,000,000, כלומר 2%. זה לא “פעם ב-20 אלף”. גם התיעוד של Microsoft מביא כדוגמה -with registry=20000 file=20000 שגורם ל-API של הרישום והקובץ להיכשל ב-2%.
  • אחרי /faults אפשר לרשום הסתברות, זמן חסד ושמות DLL. התחביר הוא /faults [הסתברות [זמן חסד במילישניות [DLL ...]]]. אם משמיטים את ההסתברות, ברירת המחדל היא 5%, ואם משמיטים את זמן החסד, ברירת המחדל היא 500 מילישניות. זמן החסד פירושו “לא להזריק כשל בפרק הזמן הזה מהעלייה של התהליך”, כדי למנוע מצב שבו עצם תהליך ההפעלה נכשל ואי אפשר לבדוק כלום.
  • /n הוא ביטול. אפשר לחשוב על n בערך כ”no verifier”. זו הפקודה המקבילה שמונעת השארת ההפעלה פועלת לצמיתות.

פלט -query lowres חוזר בערך בצורה הזו. כאן אפשר לבדוק אם ההסתברות שהוגדרה נכנסה בפועל, ושלא נפתחו בטעות סוגים שלא כיוונו אליהם.

Settings for CameraHarness.exe:
Test [lowres] enabled.

Include = *
Exclude =
TimeOut = 2000 (0x7D0)
WAIT = 0 (0x0)
HEAP_ALLOC = 20000 (0x4E20)
VIRTUAL_ALLOC = 0 (0x0)
REGISTRY = 0 (0x0)
FILE = 20000 (0x4E20)
EVENT = 20000 (0x4E20)
MAP_VIEW = 0 (0x0)
OLE_ALLOC = 0 (0x0)
STACKS = false

Include ו-Exclude הם צמצום המודולים היעד, ו-TimeOut הוא הזמן שבו לא מזריקים כשל מיד אחרי ההפעלה. ברירת המחדל משתנה לפי אופן ההגדרה, אז עדיף לבדוק בפלט הזה ולא להניח.

בתור דרך חשיבה, זה נראה כך:

  1. קודם מריצים את המסלול הרגיל רק עם Basics
  2. אחר כך מוסיפים את Low Resource Simulation ומריצים עם fault injection
  3. במידת הצורך, נותנים הסתברות רק לכשלים שרוצים לראות, כמו file או event
  4. אם רוצים לכוון ל-DLL מסוים, מצמצמים אליו בלבד

הקיצור /faults נוח, אבל בו לבד מתמקדים בעיקר ב-OLE_ALLOC ו-HEAP_ALLOC. אם רוצים לראות את ה-failure path של CreateFile או CreateEvent, בטוח יותר לכתוב עד -enable lowres -with file=... event=....

באפליקציית בקרת התקנים, לרוב קל יותר לקרוא אם מצמצמים ל-DLL של ה-wrapper של המצלמה או נתיב השמירה, במקום לפזר כשלים על כל האפליקציה.

סדר הפעלת fault injectionתרשים המראה שקודם מריצים את המסלול הרגיל רק עם Basics, ואז מוסיפים Low Resource Simulation ומריצים עם fault injection, נותנים הסתברות רק לכשלים הרצויים, ובמידת הצורך מצמצמים ל-DLL מסוים.מסלול רגיל עם Basics בלבדהוספת Low Resource והרצההסתברות רק לכשלים הרצוייםצמצום ל-DLL מסוים במידת הצורך

איור 14: לא לתקוף ישר למטרה; מתקדמים בהדרגה מהמסלול הרגיל של Basics לצמצום ה-fault injection.

נשאיר גם את הכתיבה הקונקרטית ל”צמצום ל-DLL”. הארגומנט השלישי ואילך של /faults הוא ציון המודול היעד.

appverif /verify CameraHarness.exe /faults 50000 1000 CameraSdkWrapper.dll

כך, כשמפעילים את CameraHarness.exe, אחרי שעברו 1000 מילישניות מההפעלה, רק פעולות שמתחילות מ-CameraSdkWrapper.dll נכשלות בהסתברות של 5% (‏50000 / 1,000,000). כותבים את שם המודול כולל הסיומת, בלי נתיב. אפשר לציין לא רק .dll, אלא גם מודולים נטענים אחרים כמו .ocx.

אפשר לבדוק אם הצמצום פועל בשורות Include ו-Exclude של appverif -query lowres -for CameraHarness.exe. אם זה עדיין *, זה אומר שהתהליך כולו עדיין היעד.

פעולת fault injection שמצומצם ל-DLLתרשים המראה שכשמציינים הסתברות, זמן חסד ומודול יעד בארגומנט של faults, אחרי שעובר זמן החסד מההפעלה, רק פעולות שמתחילות מה-DLL שצוין נכשלות בהסתברות שצוינה, ואפשר לוודא את הצמצום בשורות Include ו-Exclude של query.ציון הסתברות, זמן חסד ושם DLLללא הזרקה בזמן החסד מההפעלהכשל רק בפעולות שמתחילות מה-DLL שצויןאימות ב-Include וב-Exclude של query

איור 15: fault injection שמצומצם ל-DLL גורם, רק אחרי זמן החסד, לכשל בפעולות שמתחילות מהמודול שצוין.

אם מריצים תחת debugger, אפשר גם לשנות את הטווח באמצע.

!avrf -trg dll CameraSdkWrapper.dll
!avrf -skp dll VendorSdk.dll

-trg הוא “לכוון לכאן”, ו--skp הוא “לדלג על זה”. אפשר גם לבדוק את הגדרות ה-fault injection הנוכחיות עם !avrf -flt, או לראות את ה-stack של הכשלים שהוזרקו לאחרונה עם !avrf -flt stacks 10.

לדוגמה, אפשר להכין תרחישים כאלה:

  • כשל CreateEvent מיד אחרי תחילת reconnect
  • כשל CreateFile בתחילת השמירה
  • כשל בהקצאת buffer זמני
  • כשל SysAllocString בהמרה של COM
  • בדיקת מסלול הכשל ב-API של המתנה

כמעט אי אפשר להגיע לתרחישים כאלה בבדיקות המסלול הרגיל השגרתיות. דווקא בגלל זה יש ערך לדרוך עליהם בכוונה.

6. איך מסתכלים על חריגת handle

6.1. בדיקת Handles

בעולם ה-handle, קודם משתמשים ב-Handles. זה מקל על זיהוי שימוש ב-invalid handle.

התקלות האופייניות שזה עוזר עבורן הן:

  • שימוש חוזר ב-handle שכבר נסגר
  • העברת ערך handle פגום
  • שימוש ב-handle שלא אותחל עקב כשל באמצע
  • מחזור החיים התקלקל ות’רד אחר נוגע ב-handle

בהפעלה ממושכת, מה שנראה בדרך כלל רק כ”לפעמים יוצאת שגיאה מוזרה” עשוי, תחת ה-verifier, לעצור מיד במקום. ההקדמה הזו מאוד עוזרת.

תקלות שבדיקת Handles מזההתרשים המראה שתחת ה-verifier אפשר לעצור מיד תקלות כמו שימוש חוזר ב-handle סגור, ערך handle פגום, handle לא מאותחל עקב כשל באמצע, ושימוש שגוי מת'רד אחר עקב מחזור חיים שהתקלקל.שימוש חוזר ב-handle סגורverifier stop במקוםערך handle פגוםhandle שלא אותחלשימוש שגוי בגלל מחזור חיים שהתקלקל

איור 16: בהפעלה ממושכת, תקלה שהייתה נראית רק כ”שגיאה מוזרה מדי פעם” — בדיקת Handles עוצרת מיד במקום.

6.2. הסתכלות על ה-stack של open /‏ close עם !htrace

מה שנוח ב-Handles הוא שהוא משתלב היטב עם handle tracing.

מכאן והלאה משתמשים ב-debugger, אז קודם מתקינים את WinDbg. הוא מופץ כחלק מ-Debugging Tools for Windows, ואפשר להתקין אותו מאותו מתקין של Windows SDK כמו Application Verifier.

windbg -xd av -xd ch -xd sov CameraHarness.exe
!avrf
!htrace 0x00000ABC

האפשרויות בשורה הראשונה הן לא סתם לחש קסמים. יש שלושה סוגי חריגות ש-Application Verifier זורק בזמן הגילוי:

אפשרות חריגה מתי היא יוצאת
av הפרת גישה (‏0xC0000005) כשמזוהה חריגה מגבולות buffer ב-heap
ch handle לא חוקי (‏0xC0000008) כשמזוהה שימוש ב-invalid handle
sov גלישת stack (‏0xC00000FD) כשנקבע שה-stack ההתחלתי לא מספיק

ו--xd הוא ציון שגורם לתפיסת אותה חריגה ב-second chance. הסיבה היא ש-Application Verifier עצמו מטפל ב-first chance ובונה את מידע ה-stop, ולכן לא נוח שה-debugger יתערב לפני כן. אם ה-debugger כבר פועל, ההגדרה שקולה לכתיבת sxd av,‏ sxd ch,‏ sxd sov.

מה שרוצים לראות עם !htrace הוא בערך:

  • איפה נפתח ה-handle
  • איפה הוא נסגר
  • האם נעשתה בו הפניה כ-invalid handle
  • האם הצטברו יותר פעולות open ממה שציפו

נשאיר גם איך זה נראה בפועל. הפלט הבא הוא דוגמה מהתיעוד הרשמי, לא מהסביבה של החברה שלנו, אבל הצורה נשארת זהה.

כשנתקלים ב-invalid handle, קודם יוצא כך:

Invalid handle - code c0000008 (first chance)
===================================================
VERIFIER STOP 00000300: pid 0x558: invalid handle exception for current stack trace
        C0000008 : Exception code.
        0012FBF8 : Exception record. Use .exr to display it.
        0012FC0C : Context record. Use .cxr to display it.
        00000000 :
===================================================
This verifier stop is continuable.
After debugging it use 'go' to continue.
===================================================

אם אחר כך מריצים !avrf, מוצג מה מופעל כרגע ואיזה stop קורה. השורה האחרונה היא הסיכום.

0:000> !avrf
Global flags: 00000100
Application verifier global flag is set.
Application verifier settings (00000004):
   - no heap checking enabled!
   - handle checks
Page heap is not active for this process.
Current stop 00000300 : c0000008 0012fbf8 0012fc0c 00000000 .
    Using an invalid handle (either closed or simply bad).

אם מסתכלים על היסטוריית ה-handle הזה עם !htrace, מוצגים OPEN /‏ CLOSE /‏ BAD REFERENCE כל אחד עם ה-stack שלו.

0:000> !htrace 7DC

--------------------------------------
Handle 0x7DC - BAD REFERENCE:
0x801902BE: ntoskrnl!NtSetEvent+0x6C
0x010012C1: badhandle!mainCRTStartup+0xE3
--------------------------------------
Handle 0x7DC - CLOSE:
0x801E1EDD: ntoskrnl!NtClose+0x19
0x010012C1: badhandle!mainCRTStartup+0xE3
--------------------------------------
Handle 0x7DC - OPEN:
0x77DE265C: KERNEL32!CreateEventA+0x66
0x010011A0: badhandle!main+0x20
--------------------------------------

אופן הקריאה פשוט: אם אחרי CLOSE מגיע BAD REFERENCE, זה אומר שנעשה שימוש חוזר ב-handle שכבר נסגר. הסתכלות ב-stack של OPEN גם מראה איפה נוצר אותו handle.

מה שמטריד בדליפת handle או שימוש שגוי ב-handle הוא שה-API שנכשל בסוף אינו הגורם האמיתי. עם !htrace, אפשר לעקוב בפירוט רב אחרי היסטוריית ה-handle הזה.

קריאת היסטוריית handle עם htraceתרשים המראה ש-htrace מציג OPEN, CLOSE ו-BAD REFERENCE כל אחד עם stack, ושאם אחרי CLOSE מגיע BAD REFERENCE זה שימוש חוזר ב-handle סגור, ושה-stack של OPEN מראה איפה הוא נוצר.OPEN (איפה נוצר)CLOSE (איפה נסגר)BAD REFERENCEמתברר שזה שימוש חוזר ב-handle סגורלכל רשומה מצורף stack

איור 17: אופן הקריאה של htrace פשוט: אם אחרי CLOSE מופיע BAD REFERENCE, זה שימוש חוזר ב-handle סגור.

6.3. איך משלבים את זה עם הלוג העצמי

עם זאת, Application Verifier לבדו לא מספיק. בפרט, לבצע את כל חקירת הדליפה של EXE שרץ הפעלה ממושכת רק איתו זה די מתיש.

לכן, בעבודה בפועל משלבים את הבאים:

  • Handle Count תקופתי
  • sessionId
  • resourceId
  • phase
  • לוג מחזור חיים של create/open מול close/dispose
  • dump ופלט debugger בזמן verifier stop

כך אפשר לעקוב, לדוגמה, כך:

  1. עם ה-heartbeat, מזהים שהשיפוע של Handle Count חשוד
  2. עם ה-lifecycle log, מצמצמים משאבים שיש להם Create בלי Close
  3. עם הרצת verifier, מוציאים מראש invalid handle או שימוש שגוי
  4. עם !htrace, בודקים את ה-stack של open /‏ close

השילוב הזה מקל מאוד על המעקב.

סדר השילוב בין הלוג העצמי ל-verifierתרשים המראה שה-heartbeat מגלה שיפוע חשוד ב-Handle Count, ה-lifecycle log מצמצם משאבים ללא Close, הרצת verifier מקדימה זיהוי שימוש שגוי, ו-htrace מציג את ה-stack של open ו-close, בסדר הזה.גילוי שיפוע חשוד ב-Handle Countצמצום משאבים עם lifecycle logהקדמת שימוש שגוי עם הרצת verifierבדיקת stack עם htrace

איור 18: חלוקת עבודה שבה הלוג העצמי מזהה שיפוע וה-verifier מזהה שימוש שגוי, מחוברת בסדר הזה.

7. איך בונים תשתית בדיקות למקרי קצה

7.1. להטות את יחידת ההרצה לכיוון harness

אי אפשר להפעיל את Application Verifier בדיעבד על תהליך שכבר רץ. מגדירים ואז מפעילים.

יתרה מכך, ההגדרה נשארת עד שמוחקים אותה במפורש. לכן, בעבודה בפועל, נוח יותר להטות זאת ל-harness EXE ייעודי לבדיקות, ולא לאפליקציית הייצור עצמה.

לדוגמה, מבנה כזה:

מבנה ה-harness שמריץ תרחיש אחד בכל תהליךתרשים המראה ש-Scenario Runner מפעיל את CameraHarness.exe, שקורא ל-CameraSdkWrapper.dll שמתקשר ל-Vendor SDK, ומייצר גם Structured Log וגם Dump או פלט Debugger.Scenario RunnerCameraHarness.exeCameraSdkWrapper.dllVendor SDKStructured LogDump / Debugger

איור 19: מבנה harness שמריץ תרחיש אחד בכל תהליך. היעד של ה-verifier הוא לא ה-DLL, אלא ה-harness EXE שמריץ אותו.

כך יש יתרונות כאלה:

  • אפשר להריץ תרחיש אחד בכל תהליך
  • קל לראות את ההפרש בדליפה
  • קל להחליף בין הפעלה וכיבוי של הגדרת AppVerifier
  • גם בדיקת DLL אפשר לטפל בה מצד ה-EXE

כך נראית הפקודה:

appverif /verify CameraHarness.exe
appverif /n CameraHarness.exe

/verify הוא הפעלת Basics, ו-/n הוא מחיקת ההגדרה (ראו גם את הטבלה בסעיף 5.3). ההפעלה לפני העלייה, והביטול במפורש. כשמריצים את זה בהנחת יסוד של harness, גם קל יותר להפחית תקלות הגדרה.

7.2. לחלק את תפריט הבדיקות

בתשתית בדיקות למקרי קצה, עדיף לא לעשות הכול בבת אחת. בערך חלוקה לשלושה מסלולים כאלה קלה לקריאה:

  1. מסלול רגיל + Basics
    • לא מזריקים שום כשל
    • מוודאים שלא יוצא verifier stop
  2. מסלול fault injection
    • Low Resource Simulation
    • גרימת כשל בכוונה ל-event /‏ file /‏ heap_alloc /‏ virtual_alloc וכדומה
  3. מסלול חפירה ב-heap
    • Heaps
    • full page heap
    • שחזור מקומי תחת debugger

כשמפרידים את זה, קל שלא להתבלבל בין “האם זה נשבר בשימוש הרגיל” ל“האם זה נשבר רק במחסור במשאבים”.

בפרט, נוכחות או היעדר fault injection משנה משמעותית את ה-code path שעוברים בו. לכן, כדאי להריץ גם run בלי fault וגם run עם fault.

תפריט הבדיקות המחולק לשלושה מסלוליםתרשים המראה שמחלקים את הבדיקות למסלול רגיל פלוס Basics שבו מוודאים שלא יוצא stop, מסלול fault injection שמשתמש ב-Low Resource Simulation להזרקת כשל בכוונה, ומסלול חפירה ב-heap שמריץ full page heap תחת debugger לשחזור מקומי.איך מריצים בדיקות מקרי קצהמסלול רגיל ו-Basicsמסלול fault injectionמסלול חפירה ב-heapוידוא שלא יוצא stopהזרקת הכשל שרוציםשחזור מקומי תחת debugger

איור 20: במקום הכול בבת אחת, חלוקה לשלושה מסלולים מונעת בלבול לגבי היכן זה נשבר.

7.3. מה אוספים

כמינימום, כדאי לתעד את הבאים:

סוג מה רוצים
לוג האפליקציה cameraId,‏ sessionId,‏ phase,‏ handleCount,‏ error code
מצב התהליך Handle Count,‏ Private Bytes,‏ Thread Count
מידע debugger !avrf,‏ !htrace, ובמידת הצורך !heap -p -a
dump בזמן verifier stop, או בסיום חריג
לוג AppVerifier תיעוד ה-stop, ובמידת הצורך המרה ל-XML לצורך ריכוז

במידת הצורך, אפשר להמיר גם את הלוג של AppVerifier ל-XML ולרכז אותו. אבל לרוב הסתכלות רק עליו לא סוגרת את הגורם, ולכן עדיף להתייחס אליו לצד קריאה יחד עם הלוג העצמי.

כמות גדולה של לוג לבדה אינה מעלה ערך. החשוב הוא שאפשר לחבר את הסיבתיות בהמשך.

7.4. תנאי ההצלחה

גם תנאי ההצלחה חלש אם הוא רק “לא קרס”. בהקשר הזה, נדרשו לפחות הדברים הבאים:

  • לא יוצא verifier stop במסלול הרגיל + Basics
  • גם עם fault injection, הכשל שציפינו לו נשאר בלוג
  • משאבים שאותחלו רק חלקית מסתדרים כמו שצריך
  • אחרי reconnect /‏ retry,‏ Handle Count חוזר קרוב ל-baseline
  • כש-verifier stop יוצא, אפשר לעקוב לפי sessionId /‏ phase וה-stack
  • הכשל לא הופך ל”לא ברור מה קרה”

החשוב כאן הוא להעריך בנפרד את “לא נשבר” ואת “אפשר לעקוב כשזה נשבר”.

שני הצירים של תנאי ההצלחהתרשים המראה שתנאי ההצלחה מחולק לציר לא נשבר, שבו לא יוצא stop ומשאבים מסתדרים, ולציר אפשר לעקוב כשזה נשבר, שבו הכשל נשאר בלוג ואפשר לעקוב עם ה-stack.תנאי ההצלחהלא נשבראפשר לעקוב כשזה נשברלא יוצא stopהמשאבים מסתדריםהכשל נשאר בלוגאפשר לעקוב עם ה-stack

איור 21: “לא קרס” לבדו חלש. מעריכים בנפרד את “לא נשבר” ואת “אפשר לעקוב כשזה נשבר”.

7.5. נקודות לתשומת לב

Application Verifier נוח מאוד, אבל הוא לא קסם.

  • code path שלא עברו עליו בפועל לא נבדק
  • full page heap כבד
  • לפעמים stop יוצא גם מצד SDK של צד שלישי
  • ה-code path שעוברים בו שונה מאוד בין הימצאות והיעדר fault injection
  • זה לא כלי שעושה לבד את כל חקירת הדליפה ב-heap ה-managed הטהור

לכן, החלוקה היא כזו:

  • שיפוע ארוך טווח — לוג עצמי ו-counters
  • שימוש שגוי בגבול ה-native — Application Verifier
  • שחזור הסיבתיות בזמן חריגה — structured log + dump + debugger

חלוקת העבודה הזו הכי מתאימה לעבודה בפועל.

התמונה הכוללת של חלוקת החקירהתרשים המראה ששיפוע ארוך טווח נבדק בלוג עצמי ו-counters, שימוש שגוי בגבול native נבדק ב-Application Verifier, ושחזור הסיבתיות בזמן חריגה נבדק בלוג מובנה, dump ו-debugger.שיפוע ארוך טווחלוג עצמי ו-countersשימוש שגוי בגבול nativeApplication Verifierשחזור הסיבתיות בזמן חריגהלוג, dump ו-debugger

איור 22: Application Verifier הוא לא שרביט קסמים; מחלקים את הכלים לפי מה שרוצים לראות.

8. חלוקה גסה בין המצבים

  • חושדים ב-invalid handle או double close
    • Handles + !htrace
  • חושדים ב-heap corruption /‏ use-after-free
    • Heaps + full page heap +‏ !heap -p -a
  • רוצים לגרום לתופעה שדומה למחסור בזיכרון או במשאבים
    • Low Resource Simulation
  • נשבר בהדרגה בהפעלה ממושכת
    • קודם כול, Handle Count /‏ Private Bytes / lifecycle log עצמיים
  • רוצים לבדוק DLL
    • להפעיל את Application Verifier על ה-harness EXE שקורא לאותו DLL

אם פותחים הכול בבת אחת מההתחלה, בדרך כלל נוצרת “ערפל לוגים”. הרבה יותר ברור אם פוגעים עם הלהב שקרוב ל-failure path שרוצים לראות.

9. סיכום

המקום של Application Verifier הוא runtime verifier לגבול ה-native /‏ Win32 של Windows. באמצעות Handles,‏ Heaps,‏ Locks,‏ Memory,‏ TLS,‏ Low Resource Simulation וכדומה, אפשר לדרוך מראש על failure path שבדרך כלל קשה שיצוץ.

מה שעזר בהקשר הזה היה שקל לעקוב עם !htrace כשמתרחשת חריגת handle, שאפשר לגרום לתופעה שדומה למחסור בזיכרון או במשאבים בלי לשבור את המכונה כולה, ושאפשר היה לבדוק אם הלוג העצמי באמת מועיל באותו רגע.

בעבודה בפועל, מפרידים בין המסלול הרגיל + Basics לבין מסלול fault injection, מכינים harness EXE ומריצים תרחישים בתהליכים קצרי-חיים. מעל זה, משלבים לוג עצמי, dump ומידע debugger, ואת השיפוע עצמו של דליפה ארוכת טווח בודקים ב-counters עצמיים — זו חלוקת העבודה.

Application Verifier הוא כלי ש במקום “לחכות במקרה” לחריגה “שכמעט אף פעם לא יוצאת”, יוצא לקראתה בעצמו.

באפליקציית בקרת התקנים, חשוב שלא ייקרע, אבל חשוב באותה מידה היכולת להסביר מה קרה כשזה כן נשבר. במובן הזה, זה כלי שמתאים מאוד לעבודה בפועל.

פרק ראשון: חקירת קריסה בהפעלה ממושכת של מצלמה תעשייתית — פרק דליפת ה-handle

10. מקורות

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

חקרי המקרה האלה מציגים גישה דומה לניתוח, לתעדוף או לעיצוב מחדש.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

מהו Application Verifier?
כלי אימות בזמן ריצה (runtime) עבור אפליקציות user-mode ב-Windows. הוא מנטר את השימוש ב-API‏ של מערכת ההפעלה ואת אופן הטיפול במשאבים באפליקציה בזמן ריצה, ומאפשר לזהות שימוש חשוד כמו invalid handle או heap corruption, וגם להזריק כשל בכוונה. בשונה מניתוח סטטי או בדיקת יחידה, זה כלי שמראה איך נשבר בפועל כשעוברים במסלול הקוד עצמו, ולכן הוא מתאים לחשיפת failure path שלא נראה בבדיקות פונקציונליות רגילות.
אפשר לשחזר מחסור בזיכרון עם Application Verifier?
בעזרת Low Resource Simulation אפשר לגרום מראש לתופעה שדומה למחסור בזיכרון או במשאבים, בלי באמת לצרוך את כל ה-RAM של המכונה. המנגנון הוא fault injection: קריאות API כמו HeapAlloc,‏ VirtualAlloc,‏ CreateFile ו-CreateEvent נכשלות בכוונה בהסתברות קבועה. אפשר גם להזריק כשל רק ל-DLL מסוים, ולכן זה נוח גם במבנה שבו wrapper עצמאי מתערבב עם SDK של ספק. עם זאת, אם פותחים הכול מההתחלה, הלוג הופך לבלתי קריא, ולכן הטריק הוא לצמצם ולפתוח רק את מה שקרוב ל-failure path שרוצים לראות.
אפשר להשתמש ב-Application Verifier לחקירת דליפת handle?
כשמפעילים את בדיקת Handles, אפשר לזהות שימוש ב-invalid handle, כמו שימוש חוזר ב-handle שכבר נסגר, וגם handle tracing מופעל אוטומטית, כך שאפשר לעקוב עם !htrace אחרי ה-stack של ה-open וה-close של אותו handle. עם זאת, לא ריאלי להטיל את כל חקירת הדליפה של EXE שרץ הפעלה ממושכת רק על Application Verifier. שילוב עם תיעוד תקופתי של Handle Count ולוג עצמי של מחזור חיי המשאב, כאשר גילוי השיפוע נעשה בלוג העצמי וגילוי השימוש השגוי נעשה ב-verifier, הוא חלוקת עבודה שמתאימה לעבודה בפועל.
איך משתמשים ב-Application Verifier לבדיקת DLL?
היעד שמפעילים עליו את Application Verifier הוא ה-EXE לבדיקה שמריץ בפועל את אותו DLL. אי אפשר להפעיל אותו בדיעבד על תהליך שכבר רץ — צריך להגדיר ולהפעיל ורק אז להריץ. גם ההגדרה נשארת עד שמוחקים אותה במפורש, ולכן נוח יותר להטות את זה ל-harness EXE ייעודי לבדיקות, ולא לאפליקציית הייצור עצמה. הרצה של תרחיש אחד בכל תהליך מקלה גם על ראיית ההפרש בדליפות וגם על החלפת ההגדרה בין מופעל לכבוי.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג