ניתוח קוד סטאטי ובדיקות קוד דינאמיות כשיטות בדיקה משולבות ומשלימות להשגת קוד מקור איכותי ויציב ,סקירה מקצועית של שיטות ודרכי פעולה דניאל לייזרוביץ, Engineering Software Lab מערכות תוכנה מודרניות הינן לרוב בעלות מספר מאפיינים עיקריים :כמות עצומה של שורות קוד ,שהייתה נחשבת לדמיונית רק לפני מספר שנים בודדות; שימוש רב בקוד שמקורו מחוץ לארגון ,בין אם קוד מסחרי שנקנה מחברה מתמחה ובין אם קוד פתוח; ומספר רב של מפתחים ברמות שונות של ניסיון והכשרה. דומה ,כי רוב רובם של מפתחי התוכנה העובדים בפרויקטים מורכבים וגדולים, הכיר בצורך בשימוש בכלי בדיקה אוטומטים המסוגלים לסרוק ,לנתח ולגלות שגיאות תכנות במאות אלפי שורות קוד בזמן קצר .כפועל יוצא ,אנו מוצאים עצמנו מתלבטים מהו הכלי המתאים לנו
ואיזה כלי ישיג את התמורה הטובה יותר יחסית להשקעה הכספית וזמן ההטמעה האם אחד מכלי ניתוח קוד סטאטיהקלים יחסית להטמעה ותפעול ,או שמא עדיף האם כלי בדיקה דינמי ,הקשה יותר להטמעה ,אך יוכל לחשוף בעיות שונות? ואולי מספיק להיצמד לתקן כתיבה מסוים ( )Coding Standardכגון MISRA Cאו MISRA CPPולהפעיל כלי ,ה”אוכף” את התקן על המפתחים? כאשר מדובר בפרויקטים הדורשים רשיון לתקנים ,כגון DO178B/Cבתחום התעופה האזרחית בארה”ב ,הדרישות ברורות ואנו פועלים בהתאם להנחיות ה(FAA -רשות התעופה הפדרלית) ללא יכולת ערעור.
אם בדרגת הרישיון שלנו נדרשות בדיקות כיסוי ( )Code Coverageמסוימות (למשל ( , MC/DCנצטייד בכלי דינאמי מתאים ללא לבטים ,וכך הלאה .עם זאת יש לזכור, שקוד “מרושיין” ל DO178B/C -הנו, ותמיד יהיה ,חלק קטן מהקוד בארגון. ברוב המכריע של המקרים ,ההתלבטות תהיה מתוך הצורך והרצון המובן מאליו של מחלקת הפיתוח לייצר קוד יציב ,נקי ושלם ומתוך מודעות אירגונית לעלות האפסית של תיקון שגיאות תכנות בשלב הפיתוח ,לעומת העלות המטפסת בצורה מעריכית של תיקון אותן שגיאות כאשר הקוד יגיע למחלקת ה , QA-לאינטגרציה ובדיקות מערכת או גרוע מכל ,ללקוח.
“כשיש לך רק פטיש כל בעיה נראית כמו מסמר” אומר פתגם עממי .בהשלכה, נמצא בהיצע הקיים של כלי בדיקת קוד אוטומטיים (מסחריים ,פרוייקטי קוד פתוח או פרוייקטים אקדמאיים), המסוגלים לבצע רק סוג מסוים ומוגדר של בדיקות ,למשל ,ניתוח קוד סטאטי; בדיקות דינאמיות וכיוצ”ב .הגופים העומדים מאחורי אותו כלי ינסו לשכנע, שביכולתם לספק פתרון מוחלט לכל צרכי הבדיקות באמצעות שיטה אחת בלבד. ואולם נראה ,שאף אחד מהכלים אינו מסוגל לערוב להעדרן של סוגי השגיאות שאותן הוא מסוגל למצוא גם כאשר הבדיקה מדווחת כתקינה. כפי שאנו למדים פעם אחר פעם, המציאות מורכבת יותר ,וסוג בדיקות אחד לא מסוגל לתת מענה עקבי אפילו לשגיאות תכנות פשוטות .לצורך ההדגמה נבחן קטע קוד פשוט ביותר (שהוכן לצורך הדגמת הרעיון בלבד): #define global /* possible values are global, local, checked_global */ || )#if defined (checked_global )defined (global ;int *foo_ptr=0x0 #endif )void main(void { )#if defined (local ;int *foo_ptr=0x0 #endif )#if defined (checked_global )if (foo_ptr == 0x0 #endif ;(*foo_ptr)++ }
תמונה .1
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14.
מדובר בדוגמא פשטנית ביותר של שגיאת התכנות הידועה כNull Pointer- .dereferencingלכאורה ,כל כלי ניתוח קוד סטאטי אמור לאתר שגיאה זו בקלות, ואכן זה המצב אם המשתנה *foo_ptr מוגדר בתוך הפונקציה (כאשר המאקרו בשורה 1מוגדר כ.)local- המצב שונה לחלוטין כאשר אותו משתנה מוגדר מחוץ לפונקציה .אומנם במידה ויש בקוד בדיקת ( NULLשימו לב לשורה 11
המתקמפלת במידה והמאקרו בשורה 1 מוגדר כ )Checked_global-כלי ניתוח קוד סטאטי יזהו אותו מיד ויתנו תמיד התראה של . Possible Null Pointe Dereferencing במקרה הבא (מאקרו מוגדר כ global ) נגיע למגבלה עיקרית של כלי הניתוח הסטאטיים .במידה והמשתנה *foo_ptr מוגדר כמשתנה גלובלי ,לא תוצג התראה בכלי ניתוח קוד סטאטי קלאסיים, המבצעים , Program flow analysisשכן שיטת הפעולה של כלים אלו נמנעת מבדיקת המשתנה הגלובלי ,בנסיון למזער את כמות התראות השווא (התראות שווא הנם בעיה אינהרנטית בכלים אלו ומאמצים רבים מושקעים בהפחתתן) וזאת מכוון ש * foo_ptrיכול להשתנות על ידי כל קוד בכל זמן ,וניסיון למפות את כל האפשריות לשינוי ערכו של *foo_ptr בכל מקום בעץ הקוד יביא את אלגוריתם החישוב של הכלי במהירות לבעיית העצירה בניסיון למפות את כל הנתיבים האפשריים (כלומר אין אלגוריתם שמכריע עבור כל תוכנית Qוקלט Xהאם התוכנית עוצרת כאשר מופעלת על Xכאשר Xהינו מספר הנתיבים האפשריים בקוד שיכול לשנות את המצביע מחוץ לפונקציה) ,תוך כדי יצירת התראות שווא רבות.
לא היינו רוצים תוכנית הכוללת קוד זה, בה כל מעבר של הקוד על שורה 13כאשר * foo_ptrמצביע לכתובת הבלתי תקפה 0x0ותוביל להתרסקות התוכנית. (את המקרה הפשוט הזה היינו יכולים להציף גם בדיבאגר אשר היה מייצר Runtime Exception במעבר על קוד זאת ,אבל ללא הסבר על גורם הבעיה וללא ציון מראה מקום מדויק).
להדגמת בדיקה אופיינית ,המסוגלת דווקא כן לאתר תקלות מסוג זה, השתמשנו בכלי של חברת ,Parasoft שיחודו בהיותו כולל סט מלא ושלם של כלים סטאטיים ודינמיים מורכבים, הערוכים לתת מענה מספק לרוב דרישות הפרויקט ,כמו גם לדרישות רישוי פורמליות כגון DO178B/Cאו תקני כתיבת קוד כגון .MISRA C/C++ ניתן לראות שבדיקה דינאמית (בניגוד לאנליזת קוד סטאטית) ,המריצה את הקוד בפועל ,תזהה את שגיאת הNull- Pointer Dereferencingגם אם *foo_ptr מוגדר כמשתנה גלובלי. אך מה יקרה אם אותה שגיאת Null Pointer Dereferencingנמצאת בתוך תנאי ifשלא מתממש כרגע ושום הרצה שנבצע בשלב זה לא תגרום לנו להגיע לצד השני של ה?else-
תמונה .2
תמונה .3
כלי Parasoftמאפשרים להכניס את ה”סמנים” אוטומטית ברזולוציה של קובץ בודד או אפילו חלקי קובץ ,באופן שמקטין את השפעת ה”סמנים” על מהירות ואופי ריצת התוכנית למינימום שאיננו מורגש בפועל אלא ביישומי זמן אמת קיצונים בעלי זמני אחזור ()Latency קצרים ביותר. בתמונה 2נראה שכלי בדיקת הכיסוי מזהים ומסמנים באדום ששורת התנאי לא מאפשרת כרגע לגשת ולקדם את * .foo_ptrיתכן שלא קיימת בעיה במקום זה ,אבל הסימון מאפשר לנו לדעת שבפנינו קטע קוד שלא רץ ולא נבדק ,כך שעשויות להסתתר בו בעיות .שינוי התנאי או סקירה ידנית קפדנית של קטע קוד זה תסייע לזהות .Null Pointer Dereferencing להדגמת תכונה נוספת של הכלים ,נשנה את תנאי הלולאה ובמקום )while(i>0 נכתוב את השגיאה הקלאסית while ,)(i>=0כאשר אינדקס הלולאה הנו מטיפוס .unsigned intכלי ניתוח קוד סטאטי יזהה לולאה אינסופית ויראה שגיאה מתאימה כפי שרואים בתמונה :3 אך למה לפנינו לולאה אינסופית? שימוש בתכונה נוספת של הכלי תספק לנו הסבר מבלי שנצטרך להיזכר מדוע לא תקין לכתוב כך( .למי ששכח ,לאחר שהאינדקס מגיע ל ,0-הקוד ממשיך לנסות ולהפחית אותו ,מה שנותן למשתנה את הערך המקסימלי של טיפוס מסוג ,Unsignedומשם חוזר חלילה). נפעיל את מודל אכיפת תקן הכתיבה (Coding Standards enforcement x ונשתמש בתקן הכתיבה הקלאסי ) והמקובל ,MISRA Cשמקורו בתקני תוכנה לתעשיית הרכב אך כיום משמש בכל מקום שיש בו צורך בקוד איכותי. במודל זה הבדיקה הנה טקסטואלית, סמנטית ותלוית הקשר. סעיף MISRA2004-10_1_h-3של התקן אוסר המרה בין משתנה מטיפוס signed למשתנה מטיפוס unsignedכפי שקרה כאן ,כעולה מהודעת השגיאה Avoid implicit conversions between signed
במקרה כזה יהיה נוח שאותו סט כלים, ששימש לניתוח קוד סטאטי ולבדיקות דינאמיות ,יכלול בתוכו באופן מובנה גם בדיקות כיסוי ( .)Code Coverageכלי Parasoftאכן כוללים בדיקות כיסוי
בנוסף לבדיקות דינמיות וניתוח קוד סטאטי. בדיקות אלה מחייבות שינוי מסוים של התוכנית ,הכנסת “סמנים” ()Tags לקוד כדי לעקוב אחרי מסלול הריצה.
and unsigned integer typesאותה מראה ה Parasoft-בין יתר ההפרות של תקן הכתיבה המופיעות בתמונה .4 הפעלנו 4סוגי בדיקות שונות :ניתוח קוד סטאטי ,בדיקות דינאמיות ,בדיקות כיסוי
הוכחה ,שאין שגיאות כאלו .הדבר נובע מאופי העבודה של כלי האנליזה המקובלים העובדים מלמעלה (נקודת הכניסה של התוכנית למשל )(main ועושים דרכם במורד עץ הקריאות של התוכנית ,עד נקודת היציאה. כלי ניתוח קוד סטאטי חדשני בשם ,INFER המיוצר על ידי חברת ,MONOIDICSפועל בדרך הפוכה ,ומתחיל את הבדיקות מנקודת היציאה של כל פרוצדורה בתוכנית ,עד נקודת הכניסה הראשית של התוכנית .כלי זה מסוגל לספק הוכחה מתמטית להעדר מספר סוגי שגיאות תכנות מוגדר מראש באמצעות הדרך הידועה במדעי המחשב בשם .Separation logic למה הכוונה? ניצור שוב דוגמת קוד פשוטה: >#include <stdlib.h
תמונה .4
{ typedef struct node ;struct node* tl ;} T { )void free_list(T *h ;T *tmp { )while (h!=NULL ;tmp = h ;h=h->tl ;)free(tmp } }
תמונה .5 הParasoft-
ובדיקות תקני כתיבה (כלי כולל גם בדיקות יחידה אבל עקב פשטנות קוד ספציפי זה נמנענו מהפעלת סוג בדיקות אלו) ואיתרנו שגיאות תכנות שונות בתנאים שונים.
Unit Testing
אבל האם קיימת אפשרות להוכיח באופן מוחלט העדר שגיאות בקוד נתון? עד לאחרונה התשובה לשאלה זו היתה שלילית .כלי בדיקת קוד אפשרו איתור שגיאות תכנות רבות ,אך לא סיפקו
האם קוד זה בטוח מבחינת ניהול הזיכרון? כלומר ,האם ניתן לומר בוודאות שאין כאן שגיאות הקשורות לזיכרון כגון דליפות זיכרון וכמובן Null Pointer ?Dereferencing התשובה חיובית .זהו קוד בטוח כשהוא מופעל בדרך הנכונה ,היינו ,הפרוצדורה free_listמצפה לפרמטר hשיצביע לרשימה מקושרת בלתי מעגלית .אם נריץ את הפרוצדורה כאשר hמצביע לרשימה מקושרת כזו ,הרשימה תשוחרר ללא כל בעיות ודליפות זיכרון .לעומת זאת ,אם נריץ את הקוד כאשר hמצביע לרשימה מקושרת מעגלית -התוכנית תתרסק בנסיון לשחרר את האיבר הראשון ברשימה פעמיים תוך יצירת שגיאת התכנות הידועה בשם . Double Free ניתן להוכיח הן את העדרן של בעיות
הקשורות לזיכרון והן שבסיום הפרוצדורה כל האלמנטים של הרשימה שוחררו ,וזאת בהרצה על גבי רשימה לא- מעגלית. בהשאלה ניתן לתאר פעולה זו כאילו כלי ה INFER -מנסה את כל אפשרויות ההתנהגות בזמן ריצה (בניגוד לכלי ניתוח קוד סטאטי רגילים ,המוגבלים למספר נתון של אפשרויות ריצה). לצורך האנלוגיה ניקח את משפט פיתגורס האומר “בכל משולש בעל ישר זווית השטח של ריבוע המונח על היתר שווה לסכומם של שני ריבועים המונחים על הניצבים. אם נרצה להוכיח את המשפט נוכל לקחת מספר כלשהו של משולשים ונבדוק את הטענה אמפירית .סביר להניח שנקבל תוצאה נכונה ,אבל בכך לא הוכחנו את המשפט ,אלא מספר מקרים פרטיים בלבד. לעומת זאת ,אם נתבסס על הפשטה
מתמטית וסט של אקסיומות ,ניתן להוכיח שמשפט פיתגורס תקף לכל המשולשים ישרי הזווית. לפי אותה דוגמא ,שאר כלי ניתוח הקוד הסטאטי מספקים הוכחה של מקרים פרטיים באופן שאינו מספק תשובה מבוססת ,מוכחת וחד משמעית ,בעוד ששיטת העבודה של INFERמייצרת הוכחה שטובה לכל המקרים ,מסנתזת את תנאי היסוד (המקבילה לאקסיומות) ועל גביהן בונה את הוכחה ,התקפה לכל התנאים. בדוגמא שלמעלה תנאי היסוד הינו שמדובר ברשימה לא מעגלית ,ואז ניתן להוכיח באופן אוטומטי שבאף מקרה של לא יתבצע שחרור כפול . Double Free בתמונה 5נראה מבנה הוכחה טיפוסי (להעדרן של שגיאות) כפי שמוצגת למשתמש ועבור פרוצדורה בשם int _copy_siginfo_to_user32(compat
)siginfo_t __user *to, siginfo_t *from פרוצדורה זאת הנה מתוך קוד המקור של קרנל הלינוקס גרסה ( 2.6קובץ linux/ arch/x86_64/ia32/ia32_signal.cשהנו אחד הקבצים המקוריים שנכתבו על ידי לינוס טרוולדוס ב .) 1992 לסיכום ,לראשונה עומד לרשות מפתחי התוכנה כלי ,המסוגל לא רק לאתר שגיאות אלא גם לספק הוכחה להעדרן .מדובר בכלי שימושי ביותר ביישומים קריטיים, ואין פלא שהשימוש בו אומץ לבדיקת קוד בפרוייקטי תעופה כגון AirBusבבקרת מערכות רכבות ,כורים אטומים ויישומים קריטיים דומים אחרים. הסקירה נכתבה עלי ידי דניאל לייזרוביץ מחברת אי.אס.אל .מערכות תוכנה המתמחה בשרותי ביצוע ,פיתוח הטמעה וכלים לניתוח קוד סטאטי ובדיקות דינאמיות ,בשאלות נוספות הערות ובקשות מידע ניתן לפנות לdaniel.l@eswlab.com -