Smart-World Surf

יחידה 12: טיפול בשגיאות וניפוי באגים

זיהוי ותיקון שגיאות נפוצות בתכנות מערכות.
שגיאות קומפילציה וקישורשגיאות זמן ריצה (Segmentation FaultBus Error)שימוש ב-debugger (gdb)טיפול בשגיאות (errnoperror)

ברוכים הבאים ליחידת הלימוד בנושא "טיפול בשגיאות וניפוי באגים" בקורס "מעבדה בתכנות מערכות". יחידה זו חיונית לכל מתכנת מערכות, שכן היכולת לזהות, להבין ולתקן שגיאות היא אבן יסוד בכתיבת קוד אמין ויעיל. נלמד על סוגי השגיאות הנפוצים, נכיר כלים לניפוי באגים כמו GDB, ונדון בדרכים לטפל בשגיאות באופן מובנה בתוכנה שלנו.

סוגי שגיאות נפוצות בתכנות מערכות

בתכנות מערכות, אנו נתקלים במגוון רחב של שגיאות, שכל אחת מהן דורשת גישה שונה לזיהוי ולתיקון.

שגיאות קומפילציה וקישור

אלו השגיאות הראשונות שנפגוש בתהליך הפיתוח. הן מתרחשות לפני שהתוכנית שלנו מתחילה לרוץ.

שגיאת קומפילציה: שגיאה המתגלה על ידי המהדר (compiler) במהלך תרגום קוד המקור לשפת מכונה. לרוב נובעת מטעויות תחביר, שימוש במשתנים לא מוכרזים, או אי התאמת טיפוסים.
שגיאת קישור: שגיאה המתגלה על ידי המקשר (linker) במהלך חיבור קבצי אובייקט (object files) לספריית קוד (library) או לקובץ הרצה (executable). לרוב נובעת מחוסר בפונקציות או משתנים שהוצהרו אך לא מומשו, או מהגדרות כפולות.

דוגמאות נפוצות לשגיאות קומפילציה כוללות: חסר נקודה-פסיק, שימוש במשתנה לפני הכרזתו, או שגיאות הקלדה בשמות פונקציות. שגיאות קישור יופיעו לרוב כ-"undefined reference to..." כאשר פונקציה הוצהרה אך לא נמצא מימוש שלה, או "multiple definition of..." כאשר ישנם מספר מימושים לאותה פונקציה.

שגיאות זמן ריצה

שגיאות אלו הן המורכבות ביותר לאיתור, שכן הן מתרחשות רק בזמן שהתוכנית פועלת, ולעיתים קרובות בתנאים ספציפיים.

Segmentation Fault (Segfault)

מתרחשת כאשר תוכנית מנסה לגשת לאזור זיכרון שאינו מורשה לה לגשת אליו. זה יכול לקרות עקב גישה למצביע NULL, גישה מחוץ לגבולות מערך, ניסיון לכתוב לאזור זיכרון לקריאה בלבד, או שחרור זיכרון שכבר שוחרר (double free).

Bus Error

מתרחשת כאשר תוכנית מנסה לגשת לזיכרון בכתובת שאינה מיושרת (misaligned) כראוי, או כאשר יש בעיה חומרתית בגישה לזיכרון. פחות נפוצה מ-Segfault במערכות מודרניות, אך עדיין אפשרית, במיוחד בעבודה עם חומרה ספציפית או בגישה ישירה לכתובות זיכרון.

Segmentation Fault: זוהי אחת משגיאות זמן הריצה הנפוצות והמתסכלות ביותר בתכנות C. היא מצביעה על כשל קריטי בניהול הזיכרון של התוכנית. הבנה מעמיקה של מצביעים, הקצאת זיכרון דינמית (malloc/free) וגבולות מערכים היא קריטית למניעתה. בבחינה, סביר להניח שתתבקשו לזהות קוד שיוביל ל-Segfault או להסביר כיצד לתקן אותו.

ניפוי באגים עם GDB

כאשר שגיאות הקומפילציה נפתרו, וכאשר אנו מתמודדים עם שגיאות זמן ריצה, כלי ניפוי באגים (debugger) הופך לחיוני.

GDB (GNU 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)

מעבר לתיקון באגים, תכנות מערכות דורש גם טיפול מובנה בשגיאות בתוך הקוד, כדי שהתוכנית תוכל להגיב בצורה נאותה למצבי כשל.

errno: משתנה גלובלי (או מאקרו) המוגדר ב-, המשמש לאחסון קוד שגיאה מספרי שהוחזר על ידי קריאת מערכת או פונקציית ספרייה במקרה של כשל.
perror(): פונקציה המוגדרת ב- המדפיסה הודעת שגיאה תיאורית ל-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. לבדוק גבולות מערכים. להקצות ולשחרר זיכרון באופן מסודר ולוודא שכל זיכרון שהוקצה משוחרר פעם אחת בלבד.
מצאתם טעות או שחסר משהו?
→ הקודמת
אלגוריתמים בסיסיים וניתוח סיבוכיות