לדלג לתוכן

9.1 שלבי הקומפילציה פתרון

פתרונות

פתרון 1

  1. לאחר הרצת gcc -E stages.c -o stages.i:
wc -l stages.i

נקבל בסביבות 700-800 שורות (תלוי בגרסת gcc ובמערכת). כל השורות האלה הן תוכן ה-header stdio.h וכל הקבצים שהוא עושה להם include. הקוד שלנו הוא רק 4-5 שורות בסוף.

  1. כשנחפש את main בתוך stages.i, נראה:
int main() {
    printf("Hello, %s! Year: %d\n", "Student", 2026);
    return 0;
}

המקרואים NAME ו-YEAR הוחלפו בערכים שלהם - "Student" ו-2026. זו החלפה טקסטואלית שהpreprocessor ביצע.

  1. בקובץ stages.s, המחרוזות נמצאות בסקשן .rodata (read-only data):
    .section .rodata
.LC0:
    .string "Hello, %s! Year: %d\n"
.LC1:
    .string "Student"
  1. הסקשנים ב-stages.o:
readelf -S stages.o

נראה סקשנים כמו: .text, .data, .bss, .rodata, .comment, .note.GNU-stack, .symtab, .strtab, .shstrtab, .rela.text.

  1. הסמלים שצריכים relocation:
readelf -r stages.o

נראה שהסמל printf (או puts אם הקומפיילר החליט לבצע אופטימיזציה) צריך relocation - כי ההגדרה שלו נמצאת ב-libc, לא בקובץ שלנו.


פתרון 2

  1. לאחר gcc -E preproc.c, השורה LOG("Debug mode is ON") הורחבה ל:
printf("[%s:%d] %s\n", "preproc.c", 8, "Debug mode is ON");

כי __FILE__ הוחלף בשם הקובץ, __LINE__ הוחלף במספר השורה (8), וההודעה נשארה כמו שהיא. כל המקרואים הורחבו באופן רקורסיבי.

  1. ללא #define DEBUG, כל הקוד בין #ifdef DEBUG ל-#endif נעלם לחלוטין מהפלט. הpreprocessor הסיר אותו. בפלט של gcc -E השורות האלה פשוט לא יופיעו.

  2. עם DEBUG:

    [preproc.c:8] Debug mode is ON
    MAX(5, 10) = 10
    SQUARE(5) = 25
    Compiled on: Mar  8 2026 at 14:30:00
    

בלי DEBUG:

SQUARE(5) = 25
Compiled on: Mar  8 2026 at 14:30:00

שורות הדיבוג לא מופיעות כי הקוד שלהן הוסר בשלב הpreprocessing.

  1. שימוש ב-gcc -DDEBUG מאפשר להגדיר מקרואים מבחוץ, בלי לשנות את קוד המקור. היתרון: אפשר להחליט בזמן build אם להפעיל דיבוג או לא, בלי לערוך קבצים. ב-Makefile זה נראה כך:
debug: CFLAGS += -DDEBUG
debug: my_program

release: my_program

פתרון 3

  1. קימפול:

    gcc -c utils.c
    gcc -c app.c
    

  2. פלט של nm utils.o:

0000000000000000 T increment
0000000000000000 t internal_helper
0000000000000000 D global_counter
0000000000000004 d local_counter
  • T (אות גדולה) = סמל גלובלי מוגדר בסקשן .text - נגיש מקבצים אחרים
  • t (אות קטנה) = סמל מקומי (static) בסקשן .text - נגיש רק בתוך הקובץ
  • D (אות גדולה) = סמל גלובלי מוגדר בסקשן .data
  • d (אות קטנה) = סמל מקומי (static) בסקשן .data

הפונקציה internal_helper והמשתנה local_counter הם static, ולכן סמלים מקומיים (אות קטנה). הלינקר לא יחשוף אותם לקבצים אחרים.

  1. פלט של nm app.o:
                 U global_counter
                 U increment
0000000000000000 T app_function
0000000000000014 T main
                 U printf

הסמלים מסוג U (undefined) הם: global_counter, increment, ו-printf. הם undefined כי הקובץ app.c משתמש בהם אבל לא מגדיר אותם - ההגדרה שלהם נמצאת בקבצים אחרים (utils.o ו-libc).

  1. הלינקוג' מצליח:
gcc utils.o app.o -o app
./app
# Result: 43

הלינקר מוצא את increment ו-global_counter ב-utils.o, ואת printf ב-libc.

  1. אם ננסה לגשת ל-local_counter מתוך app.c:
extern int local_counter;  // ננסה לגשת

נקבל שגיאת לינקר:

undefined reference to `local_counter`

כי local_counter הוגדר כ-static - הוא סמל מקומי (t/d) ולא נגיש מקבצים אחרים. הלינקר פשוט לא רואה אותו.


פתרון 4

  1. ללא אזהרות, הקוד מתקמפל (gcc מקמפל כברירת מחדל בלי אזהרות רבות). ייתכן שתופיע אזהרה על הגישה מחוץ לגבולות המערך, תלוי בגרסת gcc.

  2. עם -Wall נקבל אזהרות כמו:

warnings.c: In function 'calculate':
warning: 'result' may be used uninitialized [-Wmaybe-uninitialized]
warnings.c: In function 'main':
warning: implicit conversion from 'double' to 'int' [-Wfloat-conversion]
warning: format '%d' expects argument, but nothing provided [-Wformat]
  1. עם -Wall -Wextra ייתכנו אזהרות נוספות על פרמטרים לא מנוצלים או בדיקות נוספות.

  2. עם -Werror הקימפול נכשל - כל אזהרה הופכת לשגיאה. לא ניתן ליצור את קובץ ההרצה עד שנתקן את כל הבעיות.

  3. קוד מתוקן:

#include <stdio.h>

int calculate(int a, int b) {
    int result = 0;  // אתחול ברירת מחדל
    if (a > 0)
        result = a + b;
    return result;
}

int main() {
    int x = 3;  // הסרת המספר העשרוני
    int arr[5];
    arr[4] = 42;  // אינדקס חוקי

    printf("Result: %d\n", calculate(x, 2));
    printf("Value: %d\n", arr[4]);  // הוספת ארגומנט

    return 0;
}

פתרון 5

  1. הקימפול בארבע רמות מייצר ארבעה קבצי אסמבלי.

  2. הפונקציה dead_code ב--O0:

dead_code:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $7, -4(%rbp)       # x = 3 + 4 = 7 (קיפול קבועים גם ב-O0)
    movl    -4(%rbp), %eax
    addl    %eax, %eax         # y = x * 2
    movl    %eax, -8(%rbp)
    movl    $42, %eax          # return 42
    popq    %rbp
    ret

ב--O2:

dead_code:
    movl    $42, %eax
    ret

הקומפיילר ב-O2 זיהה ש-x ו-y הם dead code (התוצאה שלהם לא משמשת לכלום) והסיר אותם לחלוטין. נשאר רק return 42.

  1. ב-main ב--O2, הקומפיילר מבצע constant folding: 10 + 20 מחושב בזמן קומפילציה ל-30, ו-30 * 2 ל-60. באסמבלי נראה ישירות את הערך 60 (או שהקומפיילר יסיר גם את b אם הוא לא בשימוש).

  2. ב--O3, הקומפיילר עלול לבצע כמה דברים מתקדמים עם הלולאה ב-compute:

  3. לזהות שהתוצאה של sum += i * 2 + 1 היא בעצם n * n (סכום סדרה חשבונית) ולהחליף את כל הלולאה בנוסחה
  4. או לבצע פריסת לולאה (loop unrolling) - לבצע כמה איטרציות בכל סיבוב
  5. או לבצע וקטוריזציה עם הוראות SIMD

בהשוואה ל--O0 שבו הלולאה קיימת כפי שנכתבה, ב--O3 יש סיכוי טוב שהלולאה תיעלם לחלוטין ותוחלף בחישוב ישיר.

  1. השוואת גודל:
wc -l optimize_O*.s

פלט לדוגמה:

  45 optimize_O0.s
  30 optimize_O1.s
  25 optimize_O2.s
  35 optimize_O3.s

שימו לב ש-O3 עלול להיות גדול מ-O2 - כי אופטימיזציות אגרסיביות כמו loop unrolling מגדילות את כמות הקוד (אבל הקוד רץ מהר יותר). O2 נחשב לאיזון הטוב ביותר בין מהירות לגודל.