ברוכים הבאים ליחידת הלימוד בנושא "טיפול בשגיאות וניפוי באגים" בקורס "מעבדה בתכנות מערכות". יחידה זו חיונית לכל מתכנת מערכות, שכן היכולת לזהות, להבין ולתקן שגיאות היא אבן יסוד בכתיבת קוד אמין ויעיל. נלמד על סוגי השגיאות הנפוצים, נכיר כלים לניפוי באגים כמו GDB, ונדון בדרכים לטפל בשגיאות באופן מובנה בתוכנה שלנו.
סוגי שגיאות נפוצות בתכנות מערכות
בתכנות מערכות, אנו נתקלים במגוון רחב של שגיאות, שכל אחת מהן דורשת גישה שונה לזיהוי ולתיקון.
שגיאות קומפילציה וקישור
אלו השגיאות הראשונות שנפגוש בתהליך הפיתוח. הן מתרחשות לפני שהתוכנית שלנו מתחילה לרוץ.
דוגמאות נפוצות לשגיאות קומפילציה כוללות: חסר נקודה-פסיק, שימוש במשתנה לפני הכרזתו, או שגיאות הקלדה בשמות פונקציות. שגיאות קישור יופיעו לרוב כ-"undefined reference to..." כאשר פונקציה הוצהרה אך לא נמצא מימוש שלה, או "multiple definition of..." כאשר ישנם מספר מימושים לאותה פונקציה.
שגיאות זמן ריצה
שגיאות אלו הן המורכבות ביותר לאיתור, שכן הן מתרחשות רק בזמן שהתוכנית פועלת, ולעיתים קרובות בתנאים ספציפיים.
Segmentation Fault (Segfault)
מתרחשת כאשר תוכנית מנסה לגשת לאזור זיכרון שאינו מורשה לה לגשת אליו. זה יכול לקרות עקב גישה למצביע NULL, גישה מחוץ לגבולות מערך, ניסיון לכתוב לאזור זיכרון לקריאה בלבד, או שחרור זיכרון שכבר שוחרר (double free).
Bus Error
מתרחשת כאשר תוכנית מנסה לגשת לזיכרון בכתובת שאינה מיושרת (misaligned) כראוי, או כאשר יש בעיה חומרתית בגישה לזיכרון. פחות נפוצה מ-Segfault במערכות מודרניות, אך עדיין אפשרית, במיוחד בעבודה עם חומרה ספציפית או בגישה ישירה לכתובות זיכרון.
ניפוי באגים עם GDB
כאשר שגיאות הקומפילציה נפתרו, וכאשר אנו מתמודדים עם שגיאות זמן ריצה, כלי ניפוי באגים (debugger) הופך לחיוני.
כדי להשתמש ב-GDB, יש לקמפל את הקוד עם הדגל -g (לדוגמה: gcc -g myprogram.c -o myprogram), המצרף מידע ניפוי באגים לקובץ ההרצה.
פקודות GDB עיקריות:
break/break: הגדרת נקודת עצירה (breakpoint). התוכנית תעצור בנקודה זו.run: התחלת ביצוע התוכנית.next(אוn): מעבר לשורת הקוד הבאה, תוך דילוג על קריאות לפונקציות.step(אוs): מעבר לשורת הקוד הבאה, תוך כניסה לפונקציות.print(אוp): הדפסת ערכו של משתנה.display: הצגת ערכו של משתנה בכל עצירה.backtrace(אוbt): הצגת עקבת הקריאות (call stack) – רשימת הפונקציות הפעילות וסדר הקריאה שלהן.continue(אוc): המשך ביצוע התוכנית עד לנקודת העצירה הבאה או לסיומה.quit(אוq): יציאה מ-GDB.
טיפול בשגיאות תוכנתי (Programmatic Error Handling)
מעבר לתיקון באגים, תכנות מערכות דורש גם טיפול מובנה בשגיאות בתוך הקוד, כדי שהתוכנית תוכל להגיב בצורה נאותה למצבי כשל.
, המשמש לאחסון קוד שגיאה מספרי שהוחזר על ידי קריאת מערכת או פונקציית ספרייה במקרה של כשל. המדפיסה הודעת שגיאה תיאורית ל-stderr, המבוססת על הערך הנוכחי של errno, בתוספת מחרוזת מותאמת אישית שהועברה אליה.כאשר קריאת מערכת (כגון open(), read(), write(), malloc()) נכשלת, היא לרוב תחזיר ערך מיוחד (לדוגמה, -1 או NULL) ותגדיר את errno לקוד השגיאה המתאים. אנו כמתכנתים צריכים לבדוק את ערך ההחזרה של פונקציות אלו, ובמקרה של כשל, להשתמש ב-errno וב-perror() כדי להבין מה השתבש ולדווח על כך למשתמש או לטפל במצב.
לדוגמה:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h> // for errno
int main() {
FILE *fp = fopen("non_existent_file.txt", "r");
if (fp == NULL) {
perror("Error opening file"); // Prints "Error opening file: No such file or directory" (or similar)
exit(EXIT_FAILURE);
}
// ... file operations ...
fclose(fp);
return 0;
}
שאלות לדיון
- השוו והבדילו בין שגיאות קומפילציה, שגיאות קישור ושגיאות זמן ריצה. תנו דוגמה לכל אחת.
- תארו תרחיש שבו GDB יהיה כלי הכרחי לניפוי באגים, ופרטו את השלבים העיקריים שתנקטו.
- הסבירו כיצד המשתנה הגלובלי
errnoוהפונקציהperror()תורמים לטיפול אמין בשגיאות בתוכניות C. - מהן הסיבות הנפוצות ביותר ל-Segmentation Fault, וכיצד ניתן למנוע אותן באמצעות כתיבת קוד הגנתי?
נקודות לתשובת מודל
- השוואת שגיאות:
- קומפילציה: לפני הרצה, טעויות תחביר/טיפוסים. דוגמה: חסר נקודה-פסיק.
- קישור: לפני הרצה, בעיות בחיבור קבצים (פונקציות לא ממומשות, הגדרות כפולות). דוגמה: "undefined reference".
- זמן ריצה: במהלך הרצה, לרוב קריסות (Segfault, Bus Error). דוגמה: גישה למצביע NULL.
- תרחיש GDB:
- תרחיש: תוכנית קורסת עם Segfault, או נכנסת ללולאה אינסופית.
- שלבים: קומפילציה עם
-g. הרצה ב-GDB. הגדרת breakpoints בנקודות חשודות. שימוש ב-run,step/next,printלבדיקת משתנים,backtraceלהבנת עקבת הקריאות.
- errno ו-perror:
errno: משתנה גלובלי המכיל קוד שגיאה מספרי לאחר כשל של קריאת מערכת/פונקציית ספרייה.perror(): פונקציה הממירה את קוד השגיאה ב-errnoלהודעה מילולית ברורה ומוסיפה לה מחרוזת מותאמת אישית, ומדפיסה ל-stderr.- תרומה: מאפשרים לתוכנית לזהות כשלים, להגיב אליהם באופן מובנה, ולספק למשתמש מידע מועיל על מהות הכשל.
- מניעת Segmentation Fault:
- סיבות נפוצות: dereference של מצביע NULL, גישה מחוץ לגבולות מערך, שימוש במצביע לא מאותחל, שחרור זיכרון כפול (double free), כתיבה לאזור זיכרון לקריאה בלבד.
- מניעה: תמיד לאתחל מצביעים ל-NULL. לבדוק שמצביעים אינם NULL לפני dereference. לבדוק גבולות מערכים. להקצות ולשחרר זיכרון באופן מסודר ולוודא שכל זיכרון שהוקצה משוחרר פעם אחת בלבד.