תשתית בדיקות למקרי קצה ב-Windows עם Application Verifier
· עודכן בתאריך: · Go Komura · פיתוח 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, מה אפשר לעשות איתו, ואיך משלבים אותו בתשתית בדיקות למקרי קצה, בהקשר של אפליקציית בקרת מצלמה תעשייתית.
תוכן עניינים
- קודם כול, המסקנה (במשפט אחד)
- מהו Application Verifier
- 2.1. בקיצור, מה זה
- 2.2. באילו מצבים זה עוזר
- 2.3. מה היתרון בזה
- 2.4. מהשגה ועד הפעלה בפועל
- מה אפשר לעשות עם 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.1. המטרה היא לא רק “למצוא באג”
- 4.2. לגרום לתופעה שדומה למחסור בזיכרון
- 4.3. לבדוק אם אפשר לעקוב כשמתרחשת חריגת handle
- איך גורמים לתופעה שדומה למחסור בזיכרון או במשאבים
- 5.1. הרעיון מאחורי Low Resource Simulation
- 5.2. מה אפשר לגרום לו להיכשל
- 5.3. איך מיישמים את זה בעבודה בפועל
- איך מסתכלים על חריגת handle
- 6.1. בדיקת
Handles - 6.2. הסתכלות על ה-stack של open / close עם
!htrace - 6.3. איך משלבים את זה עם הלוג העצמי
- 6.1. בדיקת
- איך בונים תשתית בדיקות למקרי קצה
- 7.1. להטות את יחידת ההרצה לכיוון harness
- 7.2. לחלק את תפריט הבדיקות
- 7.3. מה אוספים
- 7.4. תנאי ההצלחה
- 7.5. נקודות לתשומת לב
- חלוקה גסה בין המצבים
- סיכום
- מקורות
מפת הידע של המאמר
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 ייעודי עם לוגים עצמיים הוא התפעול המעשי.
flowchart LR
accTitle: מפת הידע של בדיקות למקרי קצה עם Application Verifier
accDescr: תרשים שמראה איך Application Verifier מזהה שימוש ב-handle לא תקין והרס ערמה כ-verifier stop, באמצעות fault injection של Low Resource Simulation ומעקב handle; איך מאתרים את הסיבה עם !avrf ו-!htrace ב-WinDbg; ואת התפעול של הפעלה עבור harness EXE ייעודי, לצד הצורך בשילוב עם לוגים עצמיים בחקירת דליפת handle בהפעלה ממושכת.
application_verifier["Application Verifier"]
abnormal_path_testing["בדיקות מסלולי כשל"]
fault_injection["הזרקת תקלות (fault injection)"]
low_resource_simulation["Low Resource Simulation"]
handle_tracing["מעקב handle (handle tracing)"]
windbg["WinDbg"]
invalid_handle["שימוש ב-handle לא חוקי (invalid handle)"]
heap_corruption["השחתת ערימה (heap corruption)"]
page_heap["page heap"]
verifier_stop["verifier stop"]
avrf_command["!avrf (פקודת הרחבה של WinDbg)"]
harness_exe_pattern["דפוס ה-harness EXE"]
handle_leak["דליפת handle"]
abnormal_path_testing -.->|"משתמש ב"| application_verifier
application_verifier -->|"משתמש ב"| fault_injection
low_resource_simulation -->|"משתמש ב"| fault_injection
application_verifier -->|"משתמש ב"| low_resource_simulation
application_verifier -.->|"משתמש ב"| handle_tracing
handle_tracing -->|"נבדק באמצעות"| windbg
invalid_handle -->|"נבדק באמצעות"| application_verifier
heap_corruption -->|"נבדק באמצעות"| application_verifier
heap_corruption -->|"נבדק באמצעות"| page_heap
verifier_stop -->|"נבדק באמצעות"| avrf_command
invalid_handle -->|"עלול לגרום ל"| verifier_stop
heap_corruption -->|"עלול לגרום ל"| verifier_stop
harness_exe_pattern -->|"מענה מומלץ ל"| application_verifier
application_verifier -->|"שימוש לא מומלץ ל"| handle_leak
avrf_command -.->|"מחייב"| windbg
low_resource_simulation -->|"מוגדר באמצעות"| application_verifier
harness_exe_pattern -->|"מענה מומלץ ל"| abnormal_path_testing
בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 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 מתערבבים באופן שגרתי — כמו באפליקציית בקרת התקנים — זה מתאים מאוד.
flowchart TB
accTitle: שני התפקידים של Application Verifier
accDescr: תרשים המראה ש-Application Verifier מגלה שימוש שגוי בגבול הבלתי-מנוהל ומקדים מקרי קצה שבדרך כלל קשה שיצוצו, ומאפשר לזהות invalid handle והשחתת heap וגם להזריק מצבים שדומים למחסור בזיכרון.
av["Application Verifier"] --> detect["גילוי שימוש שגוי בגבול הבלתי-מנוהל"]
av --> inject["הקדמת מקרי קצה נדירים"]
detect --> d1["invalid handle או השחתת heap"]
inject --> d2["הזרקת מצב שדומה למחסור בזיכרון"]
איור 1: הפעולה של Application Verifier נשענת על שני עמודים — “גילוי שימוש שגוי” ו”הקדמת מקרי קצה”.
2. מהו Application Verifier
2.1. בקיצור, מה זה
Application Verifier הוא כלי אימות בזמן ריצה עבור אפליקציות user-mode ב-Windows. הוא מנטר את השימוש ב-API של מערכת ההפעלה ואת אופן הטיפול במשאבים באפליקציה בזמן ריצה, ומאפשר לזהות שימוש חשוד וגם להזריק כשל בכוונה.
בשונה מ”ניתוח סטטי” או “בדיקת יחידה”, זה כלי שמראה איך זה נשבר בפועל כשעוברים במסלול הקוד הזה. לכן הוא מתאים לחשיפת failure path שלא נראה בבדיקות פונקציונליות רגילות.
flowchart LR
accTitle: מבנה הבדיקה עם harness ו-Application Verifier
accDescr: תרשים המראה שה-harness מפעיל את אפליקציית הבקרה או ה-wrapper של ה-SDK, ש-Application Verifier מנטר, וש-Application Verifier מייצר verifier stop, פלט debugger ולוגי AppVerifier, בעוד האפליקציה עצמה מייצרת גם לוג מובנה עצמאי.
A["harness לבדיקה"] --> B["אפליקציית הבקרה / wrapper של ה-SDK"]
B --> C["Application Verifier"]
C --> D["Win32 API / DLL native / משאבי OS"]
C --> E["verifier stop"]
C --> F["פלט ה-debugger"]
C --> G["לוגי AppVerifier"]
B --> H["לוג מובנה עצמאי"]
איור 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 הטהור.
flowchart TB
accTitle: ההבחנה מתי Application Verifier עוזר
accDescr: תרשים המראה שכאשר הבעיה נמצאת בשכבת SDK native או בגבול Win32, Application Verifier עוזר, אך כשמדובר ב-object graph של עולם managed טהור, זה מחוץ לתחום שלו ונדרש כלי אחר.
q{"באיזו שכבה הבעיה"}
q -->|"SDK native או גבול Win32"| yes["Application Verifier עוזר"]
q -->|"object graph של managed טהור"| no["מחוץ לתחום, כלי אחר"]
איור 3: קו הגבול הוא עובי גבול ה-native / Win32, וזה לא כלי שרואה לבד את עולם ה-managed הטהור.
2.3. מה היתרון בזה
בעבודה בפועל, שלושת היתרונות המרכזיים הם בערך אלה:
- ניתן לעצור מוקדם שימוש שגוי בגבול ה-native
- invalid handle
- heap corruption
- lock misuse
- שימוש שגוי ב-API של זיכרון וירטואלי, וכדומה
- ניתן להקדים שברים שמופיעים רק במחסור במשאבים
- המקבילה ל-
mallocנכשלת מדי פעם CreateEventאוCreateFileנכשלים מדי פעםVirtualAllocנכשל
- המקבילה ל-
- קל לעקוב בשילוב עם debugger
!avrf!htrace!heap -p -a- לוג של verifier stop
מה שמקשה באפליקציית בקרת התקנים הוא “לא לדעת מה קרה במסלול החריג”. Application Verifier מאוד יעיל בצמצום אותה “אי-ידיעה”.
flowchart TB
accTitle: שלוש הנקודות המועילות בעבודה בפועל
accDescr: תרשים המראה שהיתרונות בעבודה בפועל הם עצירה מוקדמת של שימוש שגוי, הקדמת שברים שמופיעים רק במחסור במשאבים, וקלות מעקב בשילוב עם debugger.
av["Application Verifier"] --> b1["עצירה מוקדמת של שימוש שגוי"]
av --> b2["הקדמת שברים נדירים"]
av --> b3["קלות מעקב עם debugger"]
b3 -.-> t["הרחבות כמו 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 או בסקריפט.
flowchart TB
accTitle: היחס בין ה-GUI לשורת הפקודה
accDescr: תרשים המראה שגם ה-GUI וגם שורת הפקודה רק כותבים אותה הגדרת רישום, ושכשה-EXE היעד עולה, ה-DLL של ה-verifier נטען לפי אותה הגדרה ונכנס hook.
gui["GUI (appverif.exe)"] --> reg["כתיבת ההגדרה לרישום"]
cli["שורת פקודה"] --> reg
reg --> boot["הפניה בעלייה של ה-EXE היעד"]
boot --> hook["טעינת ה-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 או בשורת הפקודה כדי לרכז אותו.
flowchart TB
accTitle: סדר ההפעלה ואופן שמירת ההגדרה
accDescr: תרשים המראה שה-hook נכנס בזמן טעינת ה-DLL ולכן אי אפשר להוסיף אותו לתהליך שכבר רץ, ושהסדר הוא להגדיר ואז להפעיל, וההגדרה נשארת עד שמוחקים אותה במפורש.
set["כתיבת ההגדרה"] --> launch["הפעלת ה-EXE היעד"]
launch --> on["פועל תחת ה-verifier"]
on --> keep["ההגדרה נשארת עד שמוחקים"]
keep -.-> warn["אם משאירים, זה תמיד רץ תחת ה-verifier"]
running["תהליך שכבר רץ"] -.-> ng["אי אפשר להפעיל בדיעבד"]
איור 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 או עיבוד אסינכרוני |
הנקודה היא לא “קוראים אחרי הקריסה ומבינים”, אלא “עוצרים שימוש חשוד במקום, מיד”. בתקלות מסוג הפעלה ממושכת, ההקדמה הזו יעילה מאוד.
flowchart TB
accTitle: רעיון הגילוי המוקדם של Basics
accDescr: תרשים המראה שבמקום לקרוא לוגים אחרי קריסה ולנחש, Basics עוצר שימוש חשוד במקום, וכך תקלת הפעלה ממושכת נחשפת מוקדם.
use["שימוש חשוד ב-API"] --> basics["קבוצת הבדיקות של Basics"]
basics --> stop["עצירה במקום"]
stop --> early["חשיפת הבעיה מוקדם"]
use -.-> later["בעבר רק קוראים אחרי הקריסה"]
איור 7: הערך של Basics הוא הפיכת “קריאה אחרי הקריסה” ל”עצירה במקום”.
3.2. Low Resource Simulation: הקדמת מחסור בזיכרון ובמשאבים
זה החלק שבאמת נוח בעבודה בפועל. מכיוון שאפשר לגרום לתופעה שדומה למחסור בזיכרון או במשאבים, בלי באמת לצרוך את כל ה-RAM.
הרעיון פשוט:
- קריאה ל-API מסוימת
- בהסתברות קבועה
- נכשלת בכוונה
כך אפשר לעבור ב-error path שבדרך כלל כמעט אף פעם לא עוברים בו.
באופן קונקרטי, קל לגרום בכוונה לתופעות כאלה:
HeapAllocאוVirtualAllocנכשליםCreateFileנכשלCreateEventנכשלMapViewOfFileנכשל- הקצאת OLE/COM כמו
SysAllocStringנכשלת
זה נוח בהרבה מלנסות לגרום למחסור אמיתי בזיכרון ולסבל של המכונה כולה. יתרה מכך, אפשר גם להזריק כשל ממוקד רק ל-DLL מסוים. במבנה שבו wrapper עצמאי מתערבב עם SDK של ספק, כמו באפליקציית בקרת התקנים, זה מתאים מאוד לעבודה בפועל.
flowchart TB
accTitle: המנגנון של Low Resource Simulation
accDescr: תרשים המראה שקריאה מסוג מסוים ל-API נכשלת בכוונה בהסתברות קבועה, וכך עוברים ב-error path שבדרך כלל לא עוברים בו, ואפשר גם לצמצם את היעד ל-DLL מסוים.
call["קריאה ל-API"] --> judge{"פגעו בהסתברות?"}
judge -->|"כן"| fail["מחזירים כשל בכוונה"]
judge -->|"לא"| ok["עיבוד רגיל"]
fail --> path["מעבר ל-error path נדיר"]
path -.-> dll["אפשר לצמצם ל-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 הוא לא שרביט קסמים אלא כלי שמחליף להב לפי המצב.
flowchart TB
accTitle: זרימת בחירת page heap
accDescr: תרשים המראה שקודם פוגעים רחב עם Basics, ואם ה-heap חשוד עוברים ל-full page heap שעוצר ברגע ההשחתה, אם זה כבד מדי יורדים ל-light page heap, ובדיקה ארוכת טווח ברמת ייצור מסתכלת בעיקר על הלוג העצמי.
s1["פגיעה רחבה עם Basics"] --> s2{"ה-heap חשוד?"}
s2 -->|"כן"| s3["עצירה עם full page heap"]
s2 -->|"לא"| s7["בדיקה ארוכת טווח בלוג עצמי"]
s3 --> s4{"זה כבד מדי?"}
s4 -->|"כן"| s5["ירידה ל-light page heap"]
s4 -->|"לא"| s6["שחזור מקומי תחת 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 הזה ואיפה נסגר”.
flowchart TB
accTitle: הזרימה מ-verifier stop ועד חקירה
accDescr: תרשים המראה שגילוי שימוש חשוד מוציא verifier stop עם מספר, שתחת debugger עוצר במקום, ושאפשר לבדוק עם avrf את ההגדרות וה-stop ועם htrace את היסטוריית ה-handle.
bad["גילוי שימוש חשוד"] --> stop["verifier stop (עם מספר)"]
stop --> brk["עצירה במקום תחת debugger"]
brk --> avrf["avrf לבדיקת הגדרות ו-stop"]
brk --> ht["htrace לבדיקת היסטוריית handle"]
stop -.-> cont["יש 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בנתיב השמירה
מה שחשוב להדגיש הוא שהמטרה אינה לגרום לחריגה כשלעצמה, אלא שאפשר לקרוא איך זה נשבר בזמן החריגה.
flowchart TB
accTitle: תשתית התצפית שבודקים באמצעות fault injection
accDescr: תרשים המראה שדריכה בכוונה על כשלים באמצעות Low Resource Simulation נועדה לבדוק אם ההקשר נשאר בלוג, אם ה-clean up רץ, ואם ה-retry לא נשבר, כאשר המטרה היא לא לגרום לחריגה אלא לוודא שאפשר לקרוא איך זה נשבר.
inject["דריכה בכוונה על כשל"] --> q1["האם ההקשר נשאר בלוג"]
inject --> q2["האם ה-clean up רץ"]
inject --> q3["האם ה-retry לא נשבר"]
q1 --> goal["מצב שאפשר לקרוא איך זה נשבר"]
q2 --> goal
q3 --> goal
איור 11: המטרה של fault injection היא לא לגרום לחריגה, אלא לבדוק אם אפשר לקרוא איך זה נשבר בזמן החריגה.
4.3. לבדוק אם אפשר לעקוב כשמתרחשת חריגת handle
כמו דליפת ה-handle שהופיעה בפרק הראשון, גם בעולם ה-handle המקום שבו קורסים לבסוף נוטה להיות שונה מהגורם האמיתי.
לכן, מה שרצינו לוודא היה:
- כש-invalid handle stop יוצא, האם אפשר לעקוב עם
!htraceאחרי open / close - האם זה מתקשר ל-
resourceId/sessionId/phaseשל הלוג העצמי - האם
handle countחוזר אחרי כשל - כש-harness הוא תהליך קצר-חיים, האם קל לראות את ההפרש בדליפה
כשמגיעים עד לכאן, אפשר לעבור מסתם “יצא באג” עד “באיזו אחריות ניהול מחזור החיים התקלקל”.
flowchart TB
accTitle: בדיקת יכולת המעקב בזמן חריגת handle
accDescr: תרשים המראה שכש-invalid handle stop יוצא, בודקים אם htrace עוקב אחרי open ו-close, אם זה מתקשר להקשר הלוג העצמי, ואם handle count חוזר, ומגיעים לזיהוי האחריות שבה ניהול מחזור החיים התקלקל.
stop["invalid handle stop"] --> c1["מעקב open ו-close עם htrace"]
stop --> c2["קישור להקשר הלוג העצמי"]
stop --> c3["בדיקת החזרת handle count"]
c1 --> goal["זיהוי האחריות שבה ניהול מחזור החיים התקלקל"]
c2 --> goal
c3 --> goal
איור 12: בחריגת handle לא עוצרים ב”יצא באג”, אלא בודקים אם אפשר להגיע עד לאחריות שבה ניהול מחזור החיים התקלקל.
5. איך גורמים לתופעה שדומה למחסור בזיכרון או במשאבים
5.1. הרעיון מאחורי Low Resource Simulation
Low Resource Simulation הוא בעצם fault injection. זה לא ממש שחזור מדויק של סביבת משאבים דלים, אלא הכנסה מלאכותית של כשלי API אופייניים שקורים במחסור במשאבים.
לכן, מקומות השימוש די ברורים:
- אימות סיום נכון (clean up) ב-failure path
- בדיקת עמידות ה-retry / reconnect
- בדיקת אתחול שמשלב הצלחה חלקית וכשל חלקי
- לוודא שגם “כשל שבדרך כלל לא קורה” נשאר בלוג
הטריק כאן הוא לא לגרום להכול להיכשל מההתחלה. אם פותחים הכול בבת אחת, הלוג מתפוצץ ולא ברור “על מה בכלל מסתכלים”.
flowchart TB
accTitle: איך מצמצמים את fault injection
accDescr: תרשים המראה שאם גורמים להכול להיכשל מההתחלה הלוג מתפוצץ ולא ברור על מה מסתכלים, ולכן פותחים בצמצום רק את הכשלים שקרובים ל-failure path שרוצים לראות.
all["גרימת כשל לכול מההתחלה"] --> noise["הלוג מתפוצץ ובלתי קריא"]
narrow["פתיחת הכשלים הרצויים בלבד"] --> clear["ברור מה מסתכלים עליו"]
איור 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 הוא הזמן שבו לא מזריקים כשל מיד אחרי ההפעלה. ברירת המחדל משתנה לפי אופן ההגדרה, אז עדיף לבדוק בפלט הזה ולא להניח.
בתור דרך חשיבה, זה נראה כך:
- קודם מריצים את המסלול הרגיל רק עם
Basics - אחר כך מוסיפים את
Low Resource Simulationומריצים עם fault injection - במידת הצורך, נותנים הסתברות רק לכשלים שרוצים לראות, כמו
fileאוevent - אם רוצים לכוון ל-DLL מסוים, מצמצמים אליו בלבד
הקיצור /faults נוח, אבל בו לבד מתמקדים בעיקר ב-OLE_ALLOC ו-HEAP_ALLOC.
אם רוצים לראות את ה-failure path של CreateFile או CreateEvent, בטוח יותר לכתוב עד -enable lowres -with file=... event=....
באפליקציית בקרת התקנים, לרוב קל יותר לקרוא אם מצמצמים ל-DLL של ה-wrapper של המצלמה או נתיב השמירה, במקום לפזר כשלים על כל האפליקציה.
flowchart TB
accTitle: סדר הפעלת fault injection
accDescr: תרשים המראה שקודם מריצים את המסלול הרגיל רק עם Basics, ואז מוסיפים Low Resource Simulation ומריצים עם fault injection, נותנים הסתברות רק לכשלים הרצויים, ובמידת הצורך מצמצמים ל-DLL מסוים.
s1["מסלול רגיל עם Basics בלבד"] --> s2["הוספת Low Resource והרצה"]
s2 --> s3["הסתברות רק לכשלים הרצויים"]
s3 --> s4["צמצום ל-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. אם זה עדיין *, זה אומר שהתהליך כולו עדיין היעד.
flowchart TB
accTitle: פעולת fault injection שמצומצם ל-DLL
accDescr: תרשים המראה שכשמציינים הסתברות, זמן חסד ומודול יעד בארגומנט של faults, אחרי שעובר זמן החסד מההפעלה, רק פעולות שמתחילות מה-DLL שצוין נכשלות בהסתברות שצוינה, ואפשר לוודא את הצמצום בשורות Include ו-Exclude של query.
arg["ציון הסתברות, זמן חסד ושם DLL"] --> grace["ללא הזרקה בזמן החסד מההפעלה"]
grace --> target["כשל רק בפעולות שמתחילות מה-DLL שצוין"]
target --> check["אימות ב-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, לעצור מיד במקום. ההקדמה הזו מאוד עוזרת.
flowchart TB
accTitle: תקלות שבדיקת Handles מזהה
accDescr: תרשים המראה שתחת ה-verifier אפשר לעצור מיד תקלות כמו שימוש חוזר ב-handle סגור, ערך handle פגום, handle לא מאותחל עקב כשל באמצע, ושימוש שגוי מת'רד אחר עקב מחזור חיים שהתקלקל.
a1["שימוש חוזר ב-handle סגור"] --> stop["verifier stop במקום"]
a2["ערך handle פגום"] --> stop
a3["handle שלא אותחל"] --> stop
a4["שימוש שגוי בגלל מחזור חיים שהתקלקל"] --> stop
איור 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 הזה.
flowchart TB
accTitle: קריאת היסטוריית handle עם htrace
accDescr: תרשים המראה ש-htrace מציג OPEN, CLOSE ו-BAD REFERENCE כל אחד עם stack, ושאם אחרי CLOSE מגיע BAD REFERENCE זה שימוש חוזר ב-handle סגור, ושה-stack של OPEN מראה איפה הוא נוצר.
open["OPEN (איפה נוצר)"] --> close["CLOSE (איפה נסגר)"]
close --> bad["BAD REFERENCE"]
bad --> mean["מתברר שזה שימוש חוזר ב-handle סגור"]
open -.-> stack["לכל רשומה מצורף stack"]
איור 17: אופן הקריאה של htrace פשוט: אם אחרי CLOSE מופיע BAD REFERENCE, זה שימוש חוזר ב-handle סגור.
6.3. איך משלבים את זה עם הלוג העצמי
עם זאת, Application Verifier לבדו לא מספיק. בפרט, לבצע את כל חקירת הדליפה של EXE שרץ הפעלה ממושכת רק איתו זה די מתיש.
לכן, בעבודה בפועל משלבים את הבאים:
Handle CountתקופתיsessionIdresourceIdphase- לוג מחזור חיים של
create/openמולclose/dispose - dump ופלט debugger בזמן verifier stop
כך אפשר לעקוב, לדוגמה, כך:
- עם ה-heartbeat, מזהים שהשיפוע של
Handle Countחשוד - עם ה-lifecycle log, מצמצמים משאבים שיש להם
CreateבליClose - עם הרצת verifier, מוציאים מראש invalid handle או שימוש שגוי
- עם
!htrace, בודקים את ה-stack של open / close
השילוב הזה מקל מאוד על המעקב.
flowchart TB
accTitle: סדר השילוב בין הלוג העצמי ל-verifier
accDescr: תרשים המראה שה-heartbeat מגלה שיפוע חשוד ב-Handle Count, ה-lifecycle log מצמצם משאבים ללא Close, הרצת verifier מקדימה זיהוי שימוש שגוי, ו-htrace מציג את ה-stack של open ו-close, בסדר הזה.
s1["גילוי שיפוע חשוד ב-Handle Count"] --> s2["צמצום משאבים עם lifecycle log"]
s2 --> s3["הקדמת שימוש שגוי עם הרצת verifier"]
s3 --> s4["בדיקת stack עם htrace"]
איור 18: חלוקת עבודה שבה הלוג העצמי מזהה שיפוע וה-verifier מזהה שימוש שגוי, מחוברת בסדר הזה.
7. איך בונים תשתית בדיקות למקרי קצה
7.1. להטות את יחידת ההרצה לכיוון harness
אי אפשר להפעיל את Application Verifier בדיעבד על תהליך שכבר רץ. מגדירים ואז מפעילים.
יתרה מכך, ההגדרה נשארת עד שמוחקים אותה במפורש. לכן, בעבודה בפועל, נוח יותר להטות זאת ל-harness EXE ייעודי לבדיקות, ולא לאפליקציית הייצור עצמה.
לדוגמה, מבנה כזה:
flowchart LR
accTitle: מבנה ה-harness שמריץ תרחיש אחד בכל תהליך
accDescr: תרשים המראה ש-Scenario Runner מפעיל את CameraHarness.exe, שקורא ל-CameraSdkWrapper.dll שמתקשר ל-Vendor SDK, ומייצר גם Structured Log וגם Dump או פלט Debugger.
A["Scenario Runner"] --> B["CameraHarness.exe"]
B --> C["CameraSdkWrapper.dll"]
C --> D["Vendor SDK"]
B --> E["Structured Log"]
B --> F["Dump / 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. לחלק את תפריט הבדיקות
בתשתית בדיקות למקרי קצה, עדיף לא לעשות הכול בבת אחת. בערך חלוקה לשלושה מסלולים כאלה קלה לקריאה:
- מסלול רגיל + Basics
- לא מזריקים שום כשל
- מוודאים שלא יוצא verifier stop
- מסלול fault injection
Low Resource Simulation- גרימת כשל בכוונה ל-
event/file/heap_alloc/virtual_allocוכדומה
- מסלול חפירה ב-heap
Heaps- full page heap
- שחזור מקומי תחת debugger
כשמפרידים את זה, קל שלא להתבלבל בין “האם זה נשבר בשימוש הרגיל” ל“האם זה נשבר רק במחסור במשאבים”.
בפרט, נוכחות או היעדר fault injection משנה משמעותית את ה-code path שעוברים בו. לכן, כדאי להריץ גם run בלי fault וגם run עם fault.
flowchart TB
accTitle: תפריט הבדיקות המחולק לשלושה מסלולים
accDescr: תרשים המראה שמחלקים את הבדיקות למסלול רגיל פלוס Basics שבו מוודאים שלא יוצא stop, מסלול fault injection שמשתמש ב-Low Resource Simulation להזרקת כשל בכוונה, ומסלול חפירה ב-heap שמריץ full page heap תחת debugger לשחזור מקומי.
menu["איך מריצים בדיקות מקרי קצה"] --> m1["מסלול רגיל ו-Basics"]
menu --> m2["מסלול fault injection"]
menu --> m3["מסלול חפירה ב-heap"]
m1 -.-> p1["וידוא שלא יוצא stop"]
m2 -.-> p2["הזרקת הכשל שרוצים"]
m3 -.-> p3["שחזור מקומי תחת 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 - הכשל לא הופך ל”לא ברור מה קרה”
החשוב כאן הוא להעריך בנפרד את “לא נשבר” ואת “אפשר לעקוב כשזה נשבר”.
flowchart TB
accTitle: שני הצירים של תנאי ההצלחה
accDescr: תרשים המראה שתנאי ההצלחה מחולק לציר לא נשבר, שבו לא יוצא stop ומשאבים מסתדרים, ולציר אפשר לעקוב כשזה נשבר, שבו הכשל נשאר בלוג ואפשר לעקוב עם ה-stack.
pass["תנאי ההצלחה"] --> a["לא נשבר"]
pass --> b["אפשר לעקוב כשזה נשבר"]
a --> a1["לא יוצא stop"]
a --> a2["המשאבים מסתדרים"]
b --> b1["הכשל נשאר בלוג"]
b --> b2["אפשר לעקוב עם ה-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
חלוקת העבודה הזו הכי מתאימה לעבודה בפועל.
flowchart TB
accTitle: התמונה הכוללת של חלוקת החקירה
accDescr: תרשים המראה ששיפוע ארוך טווח נבדק בלוג עצמי ו-counters, שימוש שגוי בגבול native נבדק ב-Application Verifier, ושחזור הסיבתיות בזמן חריגה נבדק בלוג מובנה, dump ו-debugger.
q1["שיפוע ארוך טווח"] --> t1["לוג עצמי ו-counters"]
q2["שימוש שגוי בגבול native"] --> t2["Application Verifier"]
q3["שחזור הסיבתיות בזמן חריגה"] --> t3["לוג, 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. מקורות
- פרק ראשון: חקירת קריסה בהפעלה ממושכת של מצלמה תעשייתית — פרק דליפת ה-handle
- Application Verifier - Overview
- Application Verifier - Testing Applications
- Application Verifier - Tests within Application Verifier
- Application Verifier - Debugging Application Verifier Stops
- Application Verifier - Features
- !htrace (WinDbg)
- !avrf (WinDbg)
- Download Debugging Tools for Windows
- הורדת Windows SDK
- GetProcessHandleCount 関数 (processthreadsapi.h)
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
חקירת קריסה בהפעלה ממושכת של מצלמה תעשייתית — פרק דליפת ה-handle
איך בוחנים אפליקציית Windows שקורסת בפתאומיות אחרי הפעלה ממושכת: דרך מקרה של אפליקציית בקרת מצלמה תעשייתית, מסודר כאן איך למצוא דליפת han...
הסיבה לכך ששידור חוזר ב-TCP עוצר תקשורת עם מצלמה תעשייתית, ואיך מבודדים אותה
איך מבודדים עצירות של כמה שניות בתקשורת עם מצלמה תעשייתית שנגרמות משידור חוזר ב-TCP: אובדן מנות, RTO, חותמות הזמן של RFC1323 ונקודות הבדי...
תכנון שמירת יומנים ו-dump בקריסת יישום Windows
המאמר מסדר איך לשלב יומן רגיל, סמן קריסה סופי, WER LocalDumps ותהליך ניטור, כדי שגם כשיישום Windows קורס מחריגה בלתי צפויה או מבאג בתוכני...
מבוא לאיסוף crash dump ב-Windows - WER, ProcDump, WinDbg
המאמר מסדר את אופן הבחירה והשימוש ב-WER LocalDumps, ProcDump, MiniDumpWriteDump ו-WinDbg, כולל נקודות תפעוליות, כדי לחקור קריסות באפליק...
המלכודות של יישום תקשורת טורית - עד לתכנון חיבור מחדש ויומן
המאמר מסדר, מנקודת מבט מעשית, את המלכודות שכדאי להימנע מהן ביישום תקשורת טורית לחיבור ציוד ובקרת מכשירי מדידה - מבניית מסגרות, timeout, ...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
חקירת תקלות ובעיות בריצות ארוכות
תקלות לסירוגין, אבחון תקשורת, תקיעות בריצות ארוכות ובדיקת נתיבי כשל.
חקרי מקרה קשורים
חקרי המקרה האלה מציגים גישה דומה לניתוח, לתעדוף או לעיצוב מחדש.
תשתית לבדיקת נתיבי כשל עם Application Verifier
חקר מקרה על הקמת בסיס לבדיקת נתיבי כשל ולהקלת חקירות עתידיות.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
חקירת תקלות ואיתור גורמים
Application Verifier ותשתית בדיקות למקרי קצה הם נושא מרכזי בחקירת תקלות וניתוח סיבות, שמקדם שחזור תקלות וזיהוי גורמים.
ייעוץ טכני וסקירת תכנון
כאשר רוצים לסדר עד כמה לשלב בדיקות מקרי קצה ונקודות תצפית בתוך התכנון, אפשר לבחון זאת כייעוץ טכני וסקירת תכנון.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מהו 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 ייעודי לבדיקות, ולא לאפליקציית הייצור עצמה. הרצה של תרחיש אחד בכל תהליך מקלה גם על ראיית ההפרש בדליפות וגם על החלפת ההגדרה בין מופעל לכבוי.