חדשות היום

ניתוח קוד סטאטי ובדיקות קוד דינאמיות כשיטות בדיקה משולבות ומשלימות להשגת קוד מקור איכותי ויציב, סקירה מקצועית של שיטות ודרכי פעולה

דניאל לייזרוביץ,
Engineering Software Lab

מערכות תוכנה מודרניות הינן לרוב בעלות מספר מאפיינים עיקריים: כמות עצומה של שורות קוד, שהייתה נחשבת לדמיונית רק לפני מספר שנים בודדות; שימוש רב בקוד שמקורו מחוץ לארגון, בין אם קוד מסחרי שנקנה מחברה מתמחה ובין אם קוד פתוח; ומספר רב של מפתחים ברמות שונות של ניסיון והכשרה.
דומה, כי רוב רובם של מפתחי התוכנה העובדים בפרויקטים מורכבים וגדולים, הכיר בצורך בשימוש בכלי בדיקה אוטומטים המסוגלים לסרוק, לנתח ולגלות שגיאות תכנות במאות אלפי שורות קוד בזמן קצר. כפועל יוצא, אנו מוצאים עצמנו מתלבטים מהו הכלי המתאים לנו ואיזה כלי ישיג את התמורה הטובה יותר יחסית להשקעה הכספית וזמן ההטמעה –  האם אחד מכלי ניתוח קוד סטאטי הקלים יחסית להטמעה ותפעול, או שמא עדיף האם כלי בדיקה דינמי, הקשה יותר להטמעה, אך יוכל לחשוף בעיות שונות? ואולי מספיק להיצמד לתקן כתיבה מסוים ( Coding Standard) כגון MISRA C או MISRA CPP ולהפעיל כלי, ה”אוכף” את התקן על המפתחים?
כאשר מדובר בפרויקטים הדורשים רשיון לתקנים, כגון DO178B/C בתחום התעופה האזרחית בארה”ב, הדרישות ברורות ואנו פועלים בהתאם להנחיות ה-  FAA(רשות התעופה הפדרלית) ללא יכולת ערעור. אם בדרגת הרישיון שלנו נדרשות בדיקות כיסוי (Code Coverage) מסוימות (למשל (MC/DC , נצטייד בכלי דינאמי מתאים ללא לבטים, וכך הלאה. עם זאת יש לזכור, שקוד “מרושיין” ל- DO178B/C הנו, ותמיד יהיה, חלק קטן מהקוד בארגון. ברוב המכריע של המקרים, ההתלבטות תהיה מתוך הצורך והרצון המובן מאליו של מחלקת הפיתוח לייצר קוד יציב, נקי ושלם ומתוך מודעות אירגונית לעלות האפסית של תיקון שגיאות תכנות בשלב הפיתוח, לעומת העלות המטפסת בצורה מעריכית של תיקון אותן שגיאות כאשר הקוד יגיע למחלקת ה-QA , לאינטגרציה ובדיקות מערכת או גרוע מכל, ללקוח.
“כשיש לך רק פטיש כל בעיה נראית כמו מסמר” אומר פתגם עממי. בהשלכה, נמצא בהיצע הקיים של כלי בדיקת קוד אוטומטיים (מסחריים, פרוייקטי קוד פתוח או פרוייקטים אקדמאיים), המסוגלים לבצע רק סוג מסוים ומוגדר של בדיקות, למשל, ניתוח קוד סטאטי; בדיקות דינאמיות וכיוצ”ב. הגופים העומדים מאחורי אותו כלי ינסו לשכנע, שביכולתם לספק פתרון מוחלט לכל צרכי הבדיקות באמצעות שיטה אחת בלבד. ואולם נראה, שאף אחד מהכלים אינו מסוגל לערוב להעדרן של סוגי השגיאות שאותן הוא מסוגל למצוא גם כאשר הבדיקה מדווחת כתקינה.
כפי שאנו למדים פעם אחר פעם, המציאות מורכבת יותר, וסוג בדיקות אחד לא מסוגל לתת מענה עקבי אפילו לשגיאות תכנות פשוטות. לצורך ההדגמה נבחן קטע קוד פשוט ביותר (שהוכן לצורך הדגמת הרעיון בלבד):

1.     #define global  /* possible
values are global, local,
checked_global */
2.     #if defined (checked_global)  ||
defined (global)
3.    int *foo_ptr=0x0;
4.    #endif
5.     void main(void)
6.     {
7.     #if defined (local)
8.     int *foo_ptr=0x0;
9.     #endif
10.   #if defined (checked_global)
11.   if (foo_ptr == 0x0)
12.   #endif
13.   (*foo_ptr)++;
14.   }

מדובר בדוגמא פשטנית ביותר של שגיאת התכנות הידועה כ-Null Pointer dereferencing. לכאורה,  כל כלי ניתוח קוד סטאטי אמור לאתר שגיאה זו בקלות, ואכן זה המצב אם המשתנה  *foo_ptr מוגדר בתוך הפונקציה (כאשר המאקרו  בשורה 1 מוגדר כ-local).

תמונה 1

תמונה 2

תמונה 3

המצב שונה לחלוטין כאשר אותו משתנה מוגדר מחוץ לפונקציה. אומנם במידה ויש בקוד בדיקת 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?
במקרה כזה יהיה נוח שאותו סט כלים, ששימש לניתוח קוד סטאטי ולבדיקות דינאמיות, יכלול בתוכו באופן מובנה גם בדיקות כיסוי ( Code Coverage). כלי Parasoft  אכן כוללים בדיקות כיסוי בנוסף לבדיקות דינמיות וניתוח קוד סטאטי.
בדיקות אלה מחייבות שינוי מסוים של התוכנית, הכנסת “סמנים” (Tags) לקוד כדי לעקוב אחרי מסלול הריצה. כלי 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 and unsigned integer types אותה מראה ה-Parasoft  בין יתר ההפרות של תקן הכתיבה המופיעות בתמונה 4.
הפעלנו 4 סוגי בדיקות שונות: ניתוח קוד סטאטי, בדיקות דינאמיות, בדיקות כיסוי ובדיקות תקני כתיבה (כלי ה-Parasoft  כולל גם בדיקות יחידה Unit Testing  אבל  עקב פשטנות קוד ספציפי זה נמנענו מהפעלת סוג בדיקות אלו) ואיתרנו שגיאות תכנות שונות בתנאים שונים.
אבל האם קיימת אפשרות להוכיח באופן מוחלט העדר שגיאות בקוד נתון?  עד לאחרונה התשובה לשאלה זו היתה שלילית. כלי בדיקת קוד אפשרו איתור שגיאות תכנות רבות, אך לא סיפקו הוכחה,  שאין שגיאות כאלו. הדבר נובע מאופי העבודה של כלי האנליזה המקובלים העובדים מלמעלה (נקודת הכניסה של התוכנית למשל main() ועושים דרכם במורד עץ הקריאות של התוכנית, עד נקודת היציאה.

תמונה 4

תמונה 5

כלי ניתוח קוד סטאטי חדשני בשם INFER, המיוצר על ידי חברת MONOIDICS, פועל בדרך הפוכה, ומתחיל את הבדיקות מנקודת היציאה של כל פרוצדורה בתוכנית, עד נקודת הכניסה הראשית  של התוכנית. כלי זה מסוגל לספק הוכחה מתמטית להעדר מספר סוגי שגיאות תכנות מוגדר מראש באמצעות הדרך הידועה במדעי המחשב בשם  Separation logic.
למה הכוונה?
ניצור שוב דוגמת קוד פשוטה:
#include <stdlib.h>

typedef struct node {
struct node* tl;
} T;

void free_list(T *h) {
T *tmp;
while (h!=NULL) {
tmp = h;
h=h->tl;
free(tmp);
}
}
האם קוד זה בטוח מבחינת ניהול הזיכרון? כלומר, האם ניתן לומר בוודאות שאין כאן שגיאות הקשורות לזיכרון כגון דליפות זיכרון וכמובן 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

תגובות סגורות