תשתית לבדיקות failure path ב-Windows עם Application Verifier
· עודכן בתאריך: · Go Komura · פיתוח Windows, חקירת באגים, מצלמה תעשייתית, Application Verifier, בדיקות failure path, handle leak
היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 11 Mar 2026)
- פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22173363)
מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.
Go Komura (2026). תשתית לבדיקות failure path ב-Windows עם Application Verifier. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173363 https://comcomponent.com/he/blog/application-verifier-abnormal-test-foundation-part2/
- DOI (הגרסה האחרונה)
- 10.5281/zenodo.22173363
- DOI (הגרסה הזו)
- 10.5281/zenodo.22173364
Application Verifier הוא כלי חזק כשרוצים לחשוף מוקדם תקלות שצצות בקוד native של Windows או בגבול ה-Win32. במיוחד כשרוצים לבדוק בעיות handle, heap corruption, ו-failure path במצב של מחסור במשאבים, אפשר להוציא החוצה מהר בעיות שבבדיקות happy path פשוט לא רואים.
בחלק הראשון, חקירת קריסה בהרצה ארוכה של מצלמה תעשייתית — handle leak, תיארנו מקרה של אפליקציית בקרה שקרסה אחרי הרצה ארוכה, והגורם היה handle leak. אבל לחזק לוגים זה רק חצי מהעבודה. מה שבאמת צריך זה לבדוק מראש שאם בעתיד תהיה טעות תכנות לא צפויה — memory leak, handle leak, כשל באמצע, או שחרור משאב שפספסנו — עדיין נהיה במצב שבו אפשר להבין מה קרה.
לשם כך השתמשנו ב-Application Verifier. זה כלי שמאפשר להוסיף בדיקות ו-fault injection בזמן ריצה לקוד שרץ ב-native של Windows או בגבול ה-Win32. מה שנוח במיוחד בפועל: אפשר לגרום מוקדם להתנהגות שמדמה מחסור בזיכרון או במשאבים, בלי באמת לרוקן את הזיכרון של המכונה.
בחלק השני נסביר מה זה Application Verifier, מה אפשר לעשות איתו, ואיך משלבים אותו בתשתית לבדיקות failure path, בהקשר של אפליקציית בקרת מצלמה תעשייתית.
תוכן עניינים
- המסקנה בקצרה
- מה זה 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 misuse כשזה קורה
- איך גורמים למצב שמדמה מחסור בזיכרון או במשאבים
- 5.1. איך Low Resource Simulation עובד
- 5.2. אילו API אפשר להכשיל
- 5.3. איך מיישמים את זה בפועל
- איך בודקים בעיות handle
- 6.1. בדיקת
Handles - 6.2. stack של open / close עם
!htrace - 6.3. שילוב עם לוגים פנימיים
- 6.1. בדיקת
- איך בונים תשתית לבדיקות failure path
- 7.1. להריץ דרך harness, לא דרך אפליקציית הייצור
- 7.2. לפצל את תפריט הבדיקות
- 7.3. מה אוספים
- 7.4. תנאי pass
- 7.5. מגבלות ונקודות לשים לב
- מתי להשתמש במה
- סיכום
- מקורות
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 17, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
1. המסקנה בקצרה
- Application Verifier מקל למצוא בזמן ריצה misuse שקורה בגבול ה-unmanaged / native של Windows
- מה ששימושי זה לא רק “למצוא באג”, אלא לגרום מוקדם ל-failure path שבדרך כלל כמעט לא מופיע
- ב-
Handlesמזהים invalid handle, ב-Heapsחושפים heap corruption, וב-Low Resource Simulationעושים fault injection למצב שמדמה מחסור בזיכרון או במשאבים - להשאיר חקירת leak של EXE שרץ לאורך זמן רק ל-Application Verifier זו גישה גרועה; בפועל משלבים
Handle Countולוגים פנימיים של resource lifecycle - בתשתית לבדיקות failure path, קריא יותר להפריד בין הרצת verifier על happy path לבין הרצה עם fault injection
- גם כשרוצים לבדוק DLL, היעד שמפעילים עליו את Application Verifier הוא ה-EXE של הבדיקה שמריץ את ה-DLL בפועל
בקיצור, Application Verifier הוא כלי שמוציא החוצה באגים מציקים מסביב ל-native / Win32 של Windows. במיוחד בעולם שבו native SDK, P/Invoke ו-Win32 API מתערבבים כדבר שבשגרה — כמו באפליקציית בקרת ציוד — ההתאמה טובה.
flowchart TB
accTitle: שני התפקידים של Application Verifier
accDescr: Application Verifier מזהה misuse בגבול native, וגם חושף מוקדם failure path נדיר. אפשר לזהות invalid handle ו-heap corruption, ולהזריק מצב שמדמה מחסור בזיכרון.
av["Application Verifier"] --> detect["זיהוי misuse בגבול native"]
av --> inject["חשיפה מוקדמת של failure path נדיר"]
detect --> d1["invalid handle או heap corruption"]
inject --> d2["injection של מצב שמדמה מחסור בזיכרון"]
איור 1: ל-Application Verifier יש שני תפקידים: זיהוי misuse, וחשיפה מוקדמת של failure path.
2. מה זה Application Verifier
2.1. בקצרה
Application Verifier הוא כלי runtime verification לאפליקציות user-mode ב-Windows. הוא מנטר בזמן ריצה איך האפליקציה משתמשת ב-OS API ואיך היא מנהלת משאבים, מזהה שימוש חשוד, ואפשר גם להזריק כשל בכוונה.
בניגוד ל-static analysis או unit tests, זה כלי שמראה איך הקוד נשבר בפועל כשעוברים ב-code path הזה. לכן הוא מתאים לחשוף failure path שלא רואים בבדיקות פונקציונליות רגילות.
flowchart LR
A[test harness] --> B[אפליקציית בקרה / SDK wrapper]
B --> C[Application Verifier]
C --> D[Win32 API / native DLL / משאבי OS]
C --> E[verifier stop]
C --> F[debugger output]
C --> G[AppVerifier logs]
B --> H[structured log פנימי]
איור 2: ה-harness מריץ את אפליקציית הבקרה, Application Verifier מנטר אותה, והתוצאות נשארות כ-verifier stop, פלט debugger ולוגים.
2.2. מתי זה באמת עוזר
זה עוזר במיוחד במצבים כאלה:
- קוראים ל-native DLL או ל-SDK של מצלמה
- חוצים גבול של P/Invoke או COM
- משתמשים הרבה, במישרין או בעקיפין, ב-handle, heap, lock ו-virtual memory
- ב-happy path כמעט אף פעם לא קורס, אבל נראה שניהול ה-lifetime מתפרק דווקא ב-failure path
- “לפעמים מחזיר כשל מוזר” מופיע לפני “קורס”
מצד שני, זה לא כלי למעקב אחרי object graph בעולם managed טהור. לכן גם באפליקציית C#, אם הגבול מול native SDK או Win32 עבה, זה יעיל מאוד. אבל זה לא כלי שרואה לבד את כל ה-managed heap leak.
flowchart TB
accTitle: מתי Application Verifier עוזר ומתי לא
accDescr: אם הבעיה ב-native SDK או בגבול Win32, Application Verifier עוזר. object graph של managed טהור הוא מחוץ לתחום, וצריך כלי אחר.
q{"באיזו שכבה הבעיה"}
q -->|"native SDK או גבול Win32"| yes["Application Verifier עוזר"]
q -->|"object graph של managed טהור"| no["מחוץ לתחום (כלי אחר)"]
איור 3: קו הגבול הוא עובי שכבת ה-native / Win32. זה לא כלי שרואה לבד את עולם ה-managed הטהור.
2.3. למה זה שימושי בפועל
בפועל, שלושת היתרונות המרכזיים הם בערך אלה:
- אפשר לעצור מוקדם misuse בגבול ה-native
- invalid handle
- heap corruption
- lock misuse
- שימוש שגוי ב-API של virtual memory, ועוד
- אפשר לחשוף מוקדם שבירה שמופיעה רק במחסור במשאבים
- המקבילה ל-
mallocנכשלת מדי פעם CreateEventאוCreateFileנכשלים מדי פעםVirtualAllocנכשל
- המקבילה ל-
- קל לעקוב בשילוב עם debugger
!avrf!htrace!heap -p -a- לוג של verifier stop
מה שמקשה באפליקציית בקרת ציוד הוא “לא לדעת מה קרה ב-failure path”. Application Verifier מצמצם את חוסר הבהירות הזה.
flowchart TB
accTitle: שלושה יתרונות בפועל
accDescr: אפשר לעצור misuse מוקדם, לחשוף מוקדם שבירה שמופיעה רק במחסור במשאבים, ולעקוב בקלות בשילוב עם debugger.
av["Application Verifier"] --> b1["עצירה מוקדמת של misuse"]
av --> b2["חשיפה מוקדמת של אופן השבירה"]
av --> b3["קל לעקוב עם debugger"]
b3 -.-> t["הרחבות כמו avrf ו-htrace"]
איור 4: בפועל זה מתכנס לשלושה דברים: זיהוי מוקדם, חשיפה מוקדמת של failure path, ושילוב עם debugger.
2.4. התקנה והפעלה מקומית
קודם סוגרים את ההתקנה. בלי זה, כל מה שאחר כך נשאר תיאורטי.
Application Verifier מגיע כחלק מ-Windows SDK. הוא לא מותקן עם Windows עצמו, אז מריצים את ה-installer של ה-SDK ומסמנים את התיבה “Application Verifier” במסך בחירת הרכיבים. שם קובץ ההרצה הוא appverif.exe.
יש שלוש דרישות קדם:
- המשתמש שמריץ צריך להיות חבר בקבוצת Administrators על אותה מכונה
- ARM64EC לא נתמך
- היעד לאימות הוא קוד unmanaged (native)
היחס בין ה-GUI לשורת הפקודה פשוט, וזה מונע בלבול:
| מה זה עושה | |
|---|---|
GUI (appverif.exe) |
כותב ל-Registry את שם ה-EXE היעד ואת שילוב הבדיקות שמדליקים |
שורת פקודה (appverif -enable ...) |
כותב בפקודה בדיוק את אותה הגדרת Registry |
| בזמן ריצה | כשה-EXE היעד עולה, ה-DLL של ה-verifier נטען לפי ההגדרה, ונכנס hook ל-Win32 API |
כלומר בשתי הדרכים עושים אותו דבר. בדרך כלל GUI בפעם הראשונה הידנית, ושורת פקודה ב-CI או בסקריפט.
flowchart TB
accTitle: הקשר בין GUI לשורת הפקודה
accDescr: גם GUI וגם שורת הפקודה רק כותבים את אותה הגדרת Registry. כשה-EXE היעד עולה, ה-DLL של ה-verifier נטען לפי ההגדרה ונכנס hook ל-Win32 API.
gui["GUI (appverif.exe)"] --> reg["כתיבת ההגדרה ל-Registry"]
cli["שורת פקודה"] --> reg
reg --> boot["קריאה בהפעלת ה-EXE היעד"]
boot --> hook["טעינת DLL של verifier ו-hook"]
איור 5: גם GUI וגם שורת הפקודה רק כותבים את אותה הגדרת Registry. ה-hook נכנס רק בהפעלת ה-EXE היעד.
ב-GUI: קליק ימני על אזור Applications משמאל, “Add Application”, מוסיפים את ה-EXE היעד, מסמנים למשל Basics באזור Tests מימין, ולוחצים “Save”. לביטול: קליק ימני על אותו אזור Applications, “Delete Application”, ואז “Save”.
מכאן נובעות שתי מגבלות חשובות:
- אי אפשר להפעיל את זה על process שכבר רץ. ה-hook נכנס בזמן טעינת ה-DLL, לכן הסדר הוא להגדיר ואז להפעיל.
- ההגדרה נשארת עד שמוחקים אותה במפורש. אם משאירים אותה אחרי “ניסיתי פעם אחת”, אותו EXE ימשיך לעלות תמיד תחת ה-verifier על אותה מכונה.
לוג הזיהוי נשמר כברירת מחדל בפורמט בינארי תחת %USERPROFILE%\AppVerifierLogs, ואפשר להמיר אותו ל-XML מה-GUI או משורת הפקודה כדי לרכז אותו.
flowchart TB
accTitle: סדר ההפעלה ומה נשאר אחריה
accDescr: ה-hook נכנס בזמן טעינת ה-DLL, לכן אי אפשר להוסיף אותו ל-process שכבר רץ. מגדירים ואז מפעילים, וההגדרה נשארת עד שמוחקים אותה במפורש.
set["כותבים את ההגדרה"] --> launch["מפעילים את ה-EXE היעד"]
launch --> on["רץ תחת verifier"]
on --> keep["ההגדרה נשארת עד שמוחקים"]
keep -.-> warn["אם משאירים, תמיד עולה תחת verifier"]
running["process שכבר רץ"] -.-> ng["אי אפשר להפעיל בדיעבד"]
איור 6: אי אפשר לשבור את הסדר “מגדירים ואז מפעילים”, וההגדרה נשארת על המכונה עד שמוחקים אותה במפורש.
3. מה אפשר לעשות עם Application Verifier
3.1. Basics: Handles / Heaps / Locks / Memory / TLS ועוד
הסט הבסיסי של Application Verifier הוא Basics.
כאן מרוכזות הבדיקות שמשתמשים בהן הרבה בפועל.
| שכבה | מה נבדק | שימוש בהקשר הזה |
|---|---|---|
Handles |
שימוש ב-invalid handle | האם דורכים על handle שכבר נסגר / פגום |
Heaps |
heap corruption | לחשוף buffer overrun או use-after-free בגבול ה-native SDK |
Leak |
משאבים שלא שוחררו עד unload של ה-DLL | בדיקת harness קצר-חיים, או מקרים שכוללים unload |
Locks / SRWLock |
lock misuse | בדיקת race בין reconnect ל-shutdown |
Memory |
שימוש שגוי ב-VirtualAlloc / MapViewOfFile וכדומה |
בדיקת חריגות סביב buffer גדול או shared memory |
TLS |
שימוש שגוי ב-API של Thread Local Storage | רשת ביטחון לקוד native עם גבולות thread מורכבים |
Threadpool |
עקביות של threadpool API ומצב ה-worker | עזר כשיש הרבה callback או עיבוד async |
הנקודה היא לא “קוראים אחרי הקריסה ומבינים”, אלא “עוצרים שימוש חשוד במקום, מיד”. בתקלות מסוג הרצה ארוכה, החשיפה המוקדמת הזו מאוד יעילה.
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נכשלת
זה נוח בהרבה מלנסות לגרום למחסור אמיתי בזיכרון ולסחוב את כל המכונה איתו. בנוסף, אפשר לכוון fault injection רק ל-DLL ספציפי. במבנה שבו wrapper פנימי מתערבב עם vendor 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 corruption, השילוב של 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 שאי אפשר להמשיך אחריו (חייבים לסיים את ה-process).
Application Verifier לא נעצר אחרי ה-stop. יש debugger extensions ולוגים, ולכן קל יותר לעקוב אחרי מה שקרה.
!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 זה עושה break במקום. ב-avrf בודקים הגדרות ו-stop, וב-htrace את היסטוריית ה-handle.
bad["זיהוי שימוש לא תקין"] --> stop["verifier stop (עם מספר)"]
stop --> brk["break במקום תחת debugger"]
brk --> avrf["avrf לבדיקת הגדרות ו-stop"]
brk --> ht["htrace לבדיקת היסטוריית handle"]
stop -.-> cont["יש stop שאפשר להמשיך ממנו ויש שלא"]
איור 10: verifier stop הוא לא סתם שורת לוג. תחת debugger הוא עושה break במקום והופך לנקודת התחלה לחקירה.
4. למה הכנסנו את זה הפעם
4.1. המטרה היא לא רק למצוא באג
המטרה הפעם לא הייתה סתם “למצוא באג אחד עם AppVerifier”. בניסוח מעשי יותר, מה שרצינו לבדוק היה:
- אם בעתיד תהיה דליפת משאב ב-failure path אחר
- האם ההקשר נשאר בלוג כמו שצריך
- האם אפשר לעקוב עד הסוף יחד עם מידע מה-debugger
- שהמצב לא יהפוך ל”לא ברור מה קרה”
כלומר, השתמשנו בזה לא רק כגלאי, אלא כבדיקה של תשתית התצפית.
4.2. לגרום למצב שמדמה מחסור בזיכרון
לגרום למחסור אמיתי בזיכרון על מכונת פיתוח רגילה זה די מסובך. ואם כל המכונה נהיית לא יציבה, הבדיקה עצמה מתמלאת ברעש.
לכן השתמשנו ב-Low Resource Simulation כדי לדרוך בכוונה על ה-failure path שסביר שיופיע במחסור בזיכרון או במשאבים.
כך קל יותר לענות על שאלות כאלה:
- אם
CreateEventנכשל, האםcameraIdו-phaseנשארים בלוג - האם cleanup רץ כמו שצריך אחרי אתחול חלקי
- אם
VirtualAllocנכשל, האם ה-retry לא נשבר - האם ה-handle חוזר כש-
CreateFileנכשל בנתיב השמירה
חשוב להדגיש: המטרה היא לא לגרום לכשל כשלעצמו, אלא לוודא שאפשר לקרוא איך זה נשבר כשהכשל קורה.
flowchart TB
accTitle: בדיקת תשתית התצפית עם fault injection
accDescr: דורכים בכוונה על כשלים עם Low Resource Simulation כדי לבדוק אם ההקשר נשאר בלוג, אם cleanup רץ, ואם retry לא נשבר. המטרה היא לא לגרום לכשל, אלא לוודא שאפשר לקרוא איך זה נשבר.
inject["דריכה בכוונה על כשל"] --> q1["האם ההקשר נשאר בלוג"]
inject --> q2["האם cleanup רץ"]
inject --> q3["האם retry לא נשבר"]
q1 --> goal["מצב שבו אפשר לקרוא איך זה נשבר"]
q2 --> goal
q3 --> goal
איור 11: המטרה של fault injection היא לא לגרום לכשל, אלא לבדוק שאפשר לקרוא איך זה נשבר כשהכשל קורה.
4.3. לבדוק שאפשר לעקוב אחרי handle misuse כשזה קורה
כמו ה-handle leak בחלק הראשון, גם סביב handle המקום שבו קורסים בסוף נוטה להיות שונה מהגורם האמיתי.
לכן רצינו לוודא:
- כשיוצא invalid handle stop, האם אפשר לעקוב ב-
!htraceאחרי open / close - האם זה מתחבר ל-
resourceId/sessionId/phaseבלוגים הפנימיים - האם Handle Count חוזר אחרי כשל
- כשה-harness הוא process קצר-חיים, האם קל לראות את ה-diff של ה-leak
כשמגיעים עד כאן, עוברים מסתם “יצא באג” עד “באיזו אחריות ניהול ה-lifetime התפרק”.
flowchart TB
accTitle: בדיקה שאפשר לעקוב אחרי handle misuse
accDescr: כשיוצא invalid handle stop, בודקים אם htrace עוקב אחרי open ו-close, אם זה מתחבר להקשר בלוגים הפנימיים, ואם Handle Count חוזר. משם מגיעים לזיהוי האחריות שבה ניהול ה-lifetime התפרק.
stop["invalid handle stop"] --> c1["מעקב open ו-close עם htrace"]
stop --> c2["חיבור להקשר בלוגים הפנימיים"]
stop --> c3["בדיקה ש-Handle Count חוזר"]
c1 --> goal["זיהוי האחריות שבה ניהול ה-lifetime התפרק"]
c2 --> goal
c3 --> goal
איור 12: בבעיות handle לא עוצרים ב”יצא באג”. בודקים שאפשר להגיע עד האחריות שבה ניהול ה-lifetime התפרק.
5. איך גורמים למצב שמדמה מחסור בזיכרון או במשאבים
5.1. איך Low Resource Simulation עובד
Low Resource Simulation הוא בעצם fault injection. זה פחות שחזור מדויק של סביבת low-resource, ויותר ערבוב מלאכותי של כשלי API אופייניים שקורים במחסור במשאבים.
לכן מקומות השימוש די ברורים:
- בדיקת cleanup ב-failure path
- בדיקת חוזק של retry / reconnect
- בדיקת אתחול שבו הצלחה חלקית מתערבבת עם כשל חלקי
- לוודא שגם “כשל שבדרך כלל לא קורה” נשאר בלוג
הטריק כאן: לא להכשיל הכול מההתחלה. אם פותחים הכול בבת אחת, הלוג מתפוצץ ולא ברור על מה בכלל מסתכלים.
flowchart TB
accTitle: איך מצמצמים fault injection
accDescr: אם מכשילים הכול מההתחלה הלוג מתפוצץ ולא ברור על מה מסתכלים. לכן פותחים קודם רק כשלים שקרובים ל-failure path שרוצים לראות.
all["הכול נכשל מההתחלה"] --> noise["הלוג מתפוצץ ובלתי קריא"]
narrow["פותחים רק את הכשלים שרוצים"] --> clear["ברור על מה מסתכלים"]
איור 13: ב-fault injection לא פותחים הכול בבת אחת. מצמצמים לכשלים שקרובים ל-failure path שרוצים לראות.
5.2. אילו API אפשר להכשיל
ב-Low Resource Simulation אפשר לגרום באופן טיפוסי לסוגי ה-API הבאים להיכשל בהסתברות:
| סוג | דוגמה | דוגמה באפליקציית בקרת ציוד |
|---|---|---|
Heap_Alloc |
הקצאת heap | buffer זמני, metadata של תמונה, הקצאה פנימית ב-wrapper של ה-SDK |
Virtual_Alloc |
הקצאת virtual memory | frame buffer גדול, ring buffer |
File |
CreateFile וכדומה |
פתיחת נתיב שמירה או קובץ לוג |
Event |
CreateEvent וכדומה |
התראת frame ready, סנכרון stop/reconnect |
MapView |
CreateMapView וכדומה |
shared memory או memory mapped file |
Ole_Alloc |
SysAllocString וכדומה |
גבול COM / OLE |
Wait |
משפחת WaitForXXX |
סביב כשל בהמתנה סינכרונית |
Registry |
גישה ל-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 של Registry ושל קבצים להיכשל ב-2%. - אחרי
/faultsאפשר לרשום הסתברות, grace period ושמות DLL. התחביר הוא/faults [הסתברות [grace period ב-ms [DLL ...]]]. אם משמיטים את ההסתברות ברירת המחדל היא 5%, ואם משמיטים את ה-grace period ברירת המחדל היא 500 ms. grace period פירושו “לא להזריק fault בפרק הזמן הזה מתחילת ה-process”, כדי למנוע מצב שבו ה-startup עצמו נכשל ואי אפשר לבדוק כלום. /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 הוא הזמן אחרי ההפעלה שבו לא מזריקים fault. ברירת המחדל משתנה לפי איך מכניסים את ההגדרה, אז עדיף לבדוק בפלט הזה ולא להניח.
דרך החשיבה היא בערך כזו:
- קודם מריצים happy path רק עם
Basics - אחר כך מוסיפים
Low Resource Simulationומריצים עם fault injection - במידת הצורך נותנים הסתברות רק לכשלים שרוצים לראות, כמו
fileאוevent - אם רוצים לכוון ל-DLL ספציפי, מצמצמים אליו בלבד
הקיצור /faults נוח, אבל לבד הוא ממקד בעיקר ב-OLE_ALLOC ו-HEAP_ALLOC.
אם רוצים לראות את ה-failure path של CreateFile או CreateEvent, בטוח יותר לכתוב עד -enable lowres -with file=... event=....
באפליקציית בקרת ציוד, לרוב קריא יותר לצמצם ל-DLL של ה-camera wrapper או לנתיב השמירה, במקום לפזר fault על כל האפליקציה.
flowchart TB
accTitle: הסדר להפעלת fault injection
accDescr: קודם happy path רק עם Basics, אחר כך מוסיפים Low Resource Simulation ומריצים עם fault injection, נותנים הסתברות רק לכשלים שרוצים, ובמידת הצורך מצמצמים ל-DLL ספציפי.
s1["happy path רק עם Basics"] --> s2["מוסיפים Low Resource ומריצים"]
s2 --> s3["הסתברות רק לכשלים שרוצים"]
s3 --> s4["מצמצמים ל-DLL ספציפי"]
איור 14: לא יורים ישר למטרה. מתקדמים בהדרגה מ-happy path עם Basics, ואז מצמצמים את ה-fault injection.
גם הכתיבה הקונקרטית של “צמצום ל-DLL”: הארגומנט השלישי ואילך של /faults הוא ציון מודול היעד.
appverif /verify CameraHarness.exe /faults 50000 1000 CameraSdkWrapper.dll
כך, כשמפעילים את CameraHarness.exe, אחרי שעוברות 1000 ms מההפעלה, רק פעולות שהתחילו מ-CameraSdkWrapper.dll נכשלות בהסתברות 5% (50000 / 1,000,000). כותבים את שם המודול כולל הסיומת, בלי נתיב. אפשר לציין לא רק .dll, אלא גם מודולים נטענים אחרים כמו .ocx.
בודקים אם הצמצום באמת עובד בשורות Include ו-Exclude של appverif -query lowres -for CameraHarness.exe. אם זה עדיין *, כל ה-process עדיין היעד.
flowchart TB
accTitle: fault injection מצומצם ל-DLL
accDescr: כשמציינים הסתברות, grace period ומודול יעד בארגומנטים של faults, אחרי שה-grace period מההפעלה עובר, רק פעולות שהתחילו מה-DLL שצוין נכשלות בהסתברות שצוינה. מאמתים את הצמצום ב-Include ו-Exclude של query.
arg["מציינים הסתברות, grace period ושם DLL"] --> grace["בלי injection ב-grace period מההפעלה"]
grace --> target["נכשלות רק פעולות שהתחילו מה-DLL שצוין"]
target --> check["אימות ב-Include ו-Exclude של query"]
איור 15: fault injection שמצומצם ל-DLL מכשיל, רק אחרי ה-grace period, פעולות שהתחילו מהמודול שצוין.
אם רצים תחת 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 של המתנה
בבדיקות happy path רגילות כמעט אי אפשר לדרוך על התרחישים האלה. דווקא לכן יש ערך לדרוך עליהם בכוונה.
6. איך בודקים בעיות handle
6.1. בדיקת Handles
סביב handle, קודם משתמשים ב-Handles.
זה מקל לזהות שימוש ב-invalid handle.
התקלות האופייניות שזה תופס:
- שימוש חוזר ב-handle שכבר נסגר
- העברת ערך handle פגום
- שימוש ב-handle שלא אותחל בגלל כשל באמצע
- ה-lifetime התפרק ו-thread אחר נוגע ב-handle
בהרצה ארוכה, מה שנראה בדרך כלל רק כ”לפעמים יוצאת שגיאה מוזרה” עשוי, תחת ה-verifier, לעצור במקום. החשיפה המוקדמת הזו מאוד עוזרת.
flowchart TB
accTitle: תקלות שבדיקת Handles תופסת
accDescr: תחת ה-verifier אפשר לעצור במקום תקלות כמו שימוש חוזר ב-handle סגור, ערך handle פגום, handle לא מאותחל בגלל כשל באמצע, ו-misuse מ-thread אחר אחרי ש-lifetime התפרק.
a1["שימוש חוזר ב-handle סגור"] --> stop["verifier stop במקום"]
a2["ערך handle פגום"] --> stop
a3["handle לא מאותחל"] --> stop
a4["misuse בגלל lifetime שהתפרק"] --> stop
איור 16: בהרצה ארוכה תקלה שהייתה נראית רק כ”שגיאה מוזרה מדי פעם” — בדיקת Handles עוצרת אותה במקום.
6.2. stack של open / close עם !htrace
מה שנוח ב-Handles הוא שהוא משתלב טוב עם handle tracing.
מכאן משתמשים ב-debugger, אז קודם מתקינים WinDbg. הוא מופץ כחלק מ-Debugging Tools for Windows, ואפשר להתקין אותו מאותו installer של Windows SDK כמו Application Verifier.
windbg -xd av -xd ch -xd sov CameraHarness.exe
!avrf
!htrace 0x00000ABC
האופציות בשורה הראשונה הן לא נוסחת קסם. יש שלושה סוגי exception ש-Application Verifier זורק בזמן זיהוי:
| אופציה | exception | מתי זה יוצא |
|---|---|---|
av |
access violation (0xC0000005) |
כשמזוהה חריגה מגבולות buffer ב-heap |
ch |
invalid handle (0xC0000008) |
כשמזוהה שימוש ב-invalid handle |
sov |
stack overflow (0xC00000FD) |
כשנקבע שה-stack ההתחלתי לא מספיק |
ו--xd אומר לתפוס את אותו exception ב-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 leak או handle misuse הוא שה-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 לבדו לא מספיק. במיוחד, לעשות את כל חקירת ה-leak של EXE שרץ לאורך זמן רק איתו זה די מתיש.
לכן בפועל משלבים את אלה:
Handle CountתקופתיsessionIdresourceIdphase- lifecycle log של create/open מול close/dispose
- dump ופלט debugger בזמן verifier stop
כך אפשר לעקוב, למשל, כך:
- ב-heartbeat מזהים ש-trend של
Handle Countחשוד - ב-lifecycle log מצמצמים משאבים שיש להם
CreateבליClose - בהרצת verifier מוציאים מוקדם invalid handle או misuse
- ב-
!htraceרואים את ה-stack של open / close
השילוב הזה מקל מאוד על המעקב.
flowchart TB
accTitle: סדר השילוב בין לוגים פנימיים ל-verifier
accDescr: heartbeat מגלה trend חשוד ב-Handle Count, lifecycle log מצמצם משאבים בלי Close, הרצת verifier חושפת misuse מוקדם, ו-htrace מציג את ה-stack של open ו-close, בסדר הזה.
s1["מזהים trend חשוד ב-Handle Count"] --> s2["מצמצמים משאבים עם lifecycle log"]
s2 --> s3["חושפים misuse מוקדם בהרצת verifier"]
s3 --> s4["רואים stack עם htrace"]
איור 18: חלוקת עבודה שבה הלוגים הפנימיים מזהים trend וה-verifier מזהה misuse, מחוברת בסדר הזה.
7. איך בונים תשתית לבדיקות failure path
7.1. להריץ דרך harness, לא דרך אפליקציית הייצור
אי אפשר להפעיל את Application Verifier על process שכבר רץ. מגדירים ואז מפעילים.
בנוסף, ההגדרה נשארת עד שמוחקים אותה במפורש. לכן בפועל נוח יותר לעבוד עם harness EXE ייעודי לבדיקות, לא עם אפליקציית הייצור עצמה.
למשל מבנה כזה:
flowchart LR
A[Scenario Runner] --> B[CameraHarness.exe]
B --> C[CameraSdkWrapper.dll]
C --> D[Vendor SDK]
B --> E[Structured Log]
B --> F[Dump / Debugger]
איור 19: מבנה harness שמריץ תרחיש אחד לכל process. היעד של ה-verifier הוא לא ה-DLL, אלא ה-harness EXE שמריץ אותו.
כך מקבלים:
- אפשר להריץ תרחיש אחד לכל process
- קל לראות diff של leak
- קל להדליק ולכבות הגדרת AppVerifier
- גם בדיקת DLL אפשר לטפל בה מצד ה-EXE
הפקודות נראות כך:
appverif /verify CameraHarness.exe
appverif /n CameraHarness.exe
/verify מדליק Basics, ו-/n מוחק את ההגדרה (ראו גם את הטבלה בסעיף 5.3).
ההפעלה לפני ה-startup, והביטול במפורש.
כשמריצים את זה מתוך הנחת יסוד של harness, גם קל יותר להפחית תקלות הגדרה.
7.2. לפצל את תפריט הבדיקות
בתשתית לבדיקות failure path עדיף לא לעשות הכול בבת אחת. בערך חלוקה לשלושה מסלולים כאלה קלה לקריאה:
- happy path + 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: מחלקים את הבדיקות ל-happy path פלוס Basics שבו מוודאים שלא יוצא stop, למסלול fault injection עם Low Resource Simulation שמכשיל בכוונה, ולמסלול העמקה ב-heap שמריץ full page heap תחת debugger לשחזור מקומי.
menu["איך מריצים בדיקות failure path"] --> m1["happy path ו-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 |
| מצב ה-process | Handle Count, Private Bytes, Thread Count |
| מידע debugger | !avrf, !htrace, ובמידת הצורך !heap -p -a |
| dump | בזמן verifier stop, או בסיום לא תקין |
| לוג AppVerifier | תיעוד ה-stop, ובמידת הצורך המרה ל-XML לריכוז |
במידת הצורך אפשר להמיר גם את לוג AppVerifier ל-XML ולרכז אותו. אבל לרוב הסתכלות רק עליו לא סוגרת את הגורם, לכן בפועל קוראים אותו לצד הלוגים הפנימיים.
הרבה לוג לבדו לא שווה הרבה. החשוב הוא שאחר כך אפשר לחבר סיבתיות.
7.4. תנאי pass
גם תנאי ה-pass חלש אם הוא רק “לא קרס”. בהקשר הזה נדרשו לפחות אלה:
- לא יוצא verifier stop ב-happy path + Basics
- גם עם fault injection, הכשל שציפינו לו נשאר בלוג
- משאבים שאותחלו רק חלקית מתנקים כמו שצריך
- אחרי reconnect / retry,
Handle Countחוזר קרוב ל-baseline - כשיוצא verifier stop, אפשר לעקוב לפי
sessionId/phaseוה-stack - הכשל לא הופך ל”לא ברור מה קרה”
החשוב כאן הוא להעריך בנפרד את “לא נשבר” ואת “אפשר לעקוב כשזה נשבר”.
flowchart TB
accTitle: שני הצירים של תנאי pass
accDescr: תנאי pass מחולק לציר לא נשבר, שבו לא יוצא stop ומשאבים מתנקים, ולציר אפשר לעקוב כשזה נשבר, שבו הכשל נשאר בלוג ואפשר לעקוב עם ה-stack.
pass["תנאי 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 שעוברים בו שונה מאוד בין run עם fault injection לבין run בלי
- זה לא כלי שעושה לבד את כל חקירת ה-managed heap leak
לכן החלוקה היא כזו:
- trend לאורך זמן — לוגים פנימיים ו-counters
- misuse בגבול native — Application Verifier
- שחזור סיבתיות בזמן כשל — structured log + dump + debugger
חלוקת העבודה הזו הכי מתאימה בפועל.
flowchart TB
accTitle: חלוקת העבודה בחקירה
accDescr: trend לאורך זמן נבדק בלוגים פנימיים וב-counters, misuse בגבול native נבדק ב-Application Verifier, ושחזור סיבתיות בזמן כשל נבדק ב-structured log, dump ו-debugger.
q1["trend לאורך זמן"] --> t1["לוגים פנימיים ו-counters"]
q2["misuse בגבול 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, אפשר לגרום למצב שמדמה מחסור בזיכרון או במשאבים בלי לשבור את כל המכונה, ואפשר היה לבדוק אם הלוגים הפנימיים באמת מועילים באותו רגע.
בפועל מפרידים בין happy path + Basics לבין מסלול fault injection, מכינים harness EXE ומריצים תרחישים ב-process קצר-חיים. מעל זה משלבים לוגים פנימיים, dump ומידע debugger, ואת ה-trend עצמו של leak ארוך בודקים ב-counters פנימיים. זו חלוקת העבודה.
Application Verifier הוא כלי ש במקום לחכות במקרה לכשל נדיר, מביא אותו אליכם.
באפליקציית בקרת ציוד חשוב שלא יישבר, אבל חשוב באותה מידה להסביר מה קרה כשזה כן נשבר. במובן הזה, זה כלי שמתאים מאוד לעבודה בפועל.
חלק ראשון: חקירת קריסה בהרצה ארוכה של מצלמה תעשייתית — handle leak
10. מקורות
- חלק ראשון: חקירת קריסה בהרצה ארוכה של מצלמה תעשייתית — handle leak
- 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 function (processthreadsapi.h)
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
Handle leak באפליקציית מצלמה תעשייתית שקורסת אחרי ריצה ארוכה
איך בוחנים אפליקציית Windows שקורסת פתאום אחרי ריצה ארוכה, דרך מקרה של אפליקציית בקרת מצלמה תעשייתית: איך מוצאים handle leak ואיך מתכננים...
Time Travel Debugging — להקליט ולהריץ אחורה באגים שלא משתחררים באפליקציות ארוכות-ריצה
באג פעם בחודש משאיר ב-crash dump רק את התוצאה. מקליטים ומריצים אחורה את הביצוע עם WinDbg Time Travel Debugging (TTD): TTD.exe, ring buffe...
TCP Retransmission שעוצר תקשורת עם מצלמה תעשייתית — איך מפרקים את זה
איך מפרקים עצירה של כמה שניות בתקשורת עם מצלמה תעשייתית שנגרמת מ-TCP retransmission: packet loss, RTO, timestamps של RFC1323, ונקודות בדי...
הרשת עובדת אבל Windows מציג "אין אינטרנט" — לבודד NCSI, DNS, Proxy ו-VPN ב-Windows
למה Windows מציג "אין אינטרנט" בזמן שהרשת עובדת — נקודת המוצא היא קביעת הקישוריות של NCSI. כאן מפרידים בין DNS, proxy, VPN ו-captive port...
דיסק ב-100%: מה באמת צריך לעצור? — להפריד בין SysMain, Windows Search ו-Defender
מבודדים שימוש של 100% בדיסק ב-Windows לפי throughput, זמן תגובה וקבצים. עוצרים את SysMain בבטחה, מצמצמים את Windows Search, ומנתחים את De...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
חקירת תקלות ובעיות בריצות ארוכות
תקלות לסירוגין, אבחון תקשורת, תקיעות בריצות ארוכות ובדיקת נתיבי כשל.
חקרי מקרה קשורים
חקרי המקרה האלה מציגים גישה דומה לניתוח, לתעדוף או לעיצוב מחדש.
תשתית לבדיקת נתיבי כשל עם Application Verifier
חקר מקרה על הקמת בסיס לבדיקת נתיבי כשל ולהקלת חקירות עתידיות.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
חקירת תקלות ואיתור גורמים
Application Verifier ותשתית לבדיקות failure path הם נושא מרכזי בחקירת באגים ובניתוח שורש הבעיה: שחזור התקלה וזיהוי הגורם.
ייעוץ טכני וסקירת תכנון
אם רוצים להחליט עד כמה לשלב בדיקות failure path ונקודות תצפית כבר בתכנון, אפשר לגשת לזה כייעוץ טכני או design review.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה זה Application Verifier?
- כלי runtime verification לאפליקציות user-mode ב-Windows. הוא מנטר בזמן ריצה איך האפליקציה משתמשת ב-OS API ואיך היא מנהלת משאבים, מזהה שימוש חשוד כמו invalid handle או heap corruption, ואפשר גם להזריק כשל בכוונה. בניגוד ל-static analysis או unit tests, זה כלי שמראה איך הקוד נשבר בפועל כשעוברים ב-code path הזה. לכן הוא מתאים לחשוף failure path שלא רואים בבדיקות פונקציונליות רגילות.
- אפשר לשחזר מחסור בזיכרון עם Application Verifier?
- עם Low Resource Simulation אפשר לגרום מוקדם לתופעה שמדמה מחסור בזיכרון או במשאבים, בלי באמת לרוקן את ה-RAM של המכונה. המנגנון הוא fault injection: קריאות API כמו HeapAlloc, VirtualAlloc, CreateFile ו-CreateEvent נכשלות בכוונה בהסתברות קבועה. אפשר גם לכוון את ה-injection ל-DLL ספציפי, וזה נוח כשיש wrapper פנימי מעורבב עם vendor SDK. אם פותחים הכול מההתחלה הלוגים הופכים לבלתי קריאים, אז מתחילים רק ממה שקרוב ל-failure path שרוצים לראות.
- אפשר להשתמש ב-Application Verifier לחקירת handle leak?
- כשמפעילים את בדיקת Handles אפשר לזהות שימוש ב-invalid handle, למשל שימוש חוזר ב-handle שכבר נסגר. בנוסף, handle tracing נדלק אוטומטית, ואפשר לראות ב-!htrace את ה-stack של open / close לאותו handle. אבל לא ריאלי להשאיר חקירת leak של EXE שרץ לאורך זמן רק ל-Application Verifier. בפועל משלבים דגימה תקופתית של Handle Count ולוגים פנימיים של resource lifecycle: את ה-trend מזהים בלוגים הפנימיים, ואת ה-misuse מזהים ב-verifier.
- איך משתמשים ב-Application Verifier לבדיקת DLL?
- היעד שמפעילים עליו את Application Verifier הוא ה-EXE של הבדיקה שמריץ בפועל את ה-DLL. אי אפשר להפעיל את זה אחרי שהתהליך כבר רץ; מגדירים ואז מפעילים את ה-EXE. ההגדרה גם נשארת עד שמוחקים אותה במפורש, לכן עדיף לכוון harness EXE ייעודי לבדיקות ולא את אפליקציית הייצור. תרחיש אחד לכל process מקל גם לראות diff של leak וגם להדליק ולכבות את ההגדרה.