ניתוח קוד סטאטי ובדיקות דינאמיות

Page 1

‫ניתוח קוד סטאטי ובדיקות קוד דינאמיות כשיטות‬ ‫בדיקה משולבות ומשלימות להשגת קוד מקור איכותי‬ ‫ויציב‪ ,‬סקירה מקצועית של שיטות ודרכי פעולה‬ ‫דניאל לייזרוביץ‪,‬‬ ‫‪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 -‬‬


Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.