לדלג לתוכן

10.2 התנהגות לא מוגדרת תרגול

תרגול - התנהגות לא מוגדרת - undefined behavior

תרגול 1 - זיהוי UB בקוד

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

קטע א:

int arr[5] = {1, 2, 3, 4, 5};
int sum = 0;
for (int i = 0; i <= 5; i++)
    sum += arr[i];

קטע ב:

unsigned int x = UINT_MAX;
x = x + 1;
printf("%u\n", x);

קטע ג:

int x = 100;
int y = x << 33;

קטע ד:

char *s = "hello";
s[0] = 'H';

קטע ה:

int *p = malloc(sizeof(int));
*p = 42;
free(p);
free(p);

קטע ו:

int a = 5;
int b = a++ + a++;


תרגול 2 - signed overflow ואופטימיזציות

  1. כתבו את הפונקציה הבאה:
    int check_overflow(int x) {
        if (x + 1 > x)
            return 1;
        return 0;
    }
    
  2. קמפלו ללא אופטימיזציות (gcc -O0) והריצו עם x = INT_MAX. מה התוצאה?
  3. קמפלו עם אופטימיזציות (gcc -O2) והריצו עם x = INT_MAX. מה התוצאה?
  4. הסבירו את ההבדל.
  5. כתבו גרסה בטוחה של הבדיקה שעובדת נכון גם עם INT_MAX (רמז: בדקו לפני החיבור אם הוא יגלוש).

תרגול 3 - שימוש ב-sanitizers

  1. כתבו תוכנית קצרה שמכילה את כל סוגי הUB הבאים (כל אחד בפונקציה נפרדת):
  2. גלישת signed integer
  3. גישה מחוץ לגבולות מערך
  4. שימוש במשתנה לא מאותחל
  5. חילוק באפס
  6. קמפלו עם -fsanitize=undefined והריצו. שימו לב להודעות השגיאה - הsanitizer מראה בדיוק באיזו שורה ומאיזה סוג הUB.
  7. קמפלו עם -fsanitize=address והוסיפו פונקציה עם use-after-free. הריצו וראו את הפלט.

תרגול 4 - strict aliasing

  1. כתבו את הקוד הבא:
    #include <stdio.h>
    #include <string.h>
    
    float int_bits_to_float_bad(int i) {
        return *(float *)&i;  // strict aliasing violation!
    }
    
    float int_bits_to_float_good(int i) {
        float f;
        memcpy(&f, &i, sizeof(f));
        return f;
    }
    
  2. קמפלו עם -O2 -fstrict-aliasing -Wall ובדקו אם יש אזהרות.
  3. כתבו תוכנית שמשתמשת בשתי הגרסאות כדי להמיר את הerך 0x40490FDB (שזה הייצוג הבינארי של pi ב-IEEE 754) ל-float. האם שתי הגרסאות נותנות את אותו התוצאה?
  4. הסבירו למה הגרסה עם memcpy בטוחה ולמה הגרסה עם cast היא UB.

תרגול 5 - כתיבת קוד בטוח

כתבו את הפונקציות הבאות באופן בטוח, ללא שום UB:

  1. int safe_add(int a, int b, int *result) - חיבור בטוח שמחזיר 0 בהצלחה ו-(-1) אם יש גלישה (בדקו לפני החיבור).
  2. int safe_div(int a, int b, int *result) - חילוק בטוח שמחזיר (-1) אם b הוא 0 או אם מדובר ב-INT_MIN / (-1) (שגם הוא UB).
  3. int safe_shift(int val, int shift, int *result) - הזזה בטוחה שמחזירה (-1) אם ה-shift לא חוקי.

קמפלו עם -fsanitize=undefined ובדקו שאף אחת מהפונקציות לא מפעילה UB, גם עם קלטים קיצוניים כמו INT_MAX, INT_MIN, 0, ומספרים שליליים.