לדלג לתוכן

9.2 הלינקר הרצאה

הקדמה

בפרק 9.1 ראינו שהקומפילציה היא תהליך של ארבעה שלבים, ושהלינקר הוא השלב האחרון - זה שלוקח את כל קבצי האובייקט ומחבר אותם לקובץ הרצה. ראינו גם שקבצי אובייקט מכילים "חורים" (relocation entries) שהלינקר צריך למלא.

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

אם למדתם את פרק 5.10 (ספריות משותפות) ופרק 5.11 (פורמט ELF), הרבה מהמושגים כאן יהיו מוכרים. ההבדל הוא שעכשיו אנחנו מבינים את הצד של הלינקר - לא רק את התוצאה, אלא את התהליך.


שתי המשימות של הלינקר

הלינקר עושה שני דברים עיקריים:

  1. פתרון סמלים - symbol resolution - מוצא את ההגדרה של כל סמל (פונקציה, משתנה גלובלי) שמשתמשים בו
  2. רילוקיישן - relocation - קובע כתובות סופיות לכל הסקשנים ולכל הסמלים, ומתקן את כל רשומות הrelocation

נעבור על כל אחד מהם.


פתרון סמלים - symbol resolution

כל קובץ אובייקט (.o) מכיל טבלת סמלים (symbol table) שמפרטת:
- סמלים מוגדרים - פונקציות ומשתנים גלובליים שהקובץ מגדיר
- סמלים לא מוגדרים - פונקציות ומשתנים שהקובץ משתמש בהם אבל לא מגדיר

משימת הלינקר: לכל סמל לא מוגדר, למצוא את הקובץ שמגדיר אותו.

# נראה את הסמלים של קובץ אובייקט
nm main.o
                 U printf       # U = undefined - משתמש אבל לא מגדיר
0000000000000000 T main         # T = defined in .text - מגדיר

סמלים חזקים וחלשים - strong vs weak symbols

הלינקר מבדיל בין שני סוגי סמלים:

סמלים חזקים - strong symbols:
- פונקציות (כל פונקציה שהגדרנו)
- משתנים גלובליים מאותחלים (int x = 5;)

סמלים חלשים - weak symbols:
- משתנים גלובליים לא מאותחלים (int x;)

הכללים:

  1. שני סמלים חזקים עם אותו שם - שגיאת לינקר: multiple definition of X
  2. סמל חזק וסמל חלש עם אותו שם - הסמל החזק מנצח
  3. שני סמלים חלשים עם אותו שם - הלינקר בוחר אחד (בדרך כלל הגדול יותר)

דוגמה לכלל 1 - שגיאה:

// file1.c
int x = 5;  // strong

// file2.c
int x = 10; // strong
gcc file1.o file2.o -o program
# error: multiple definition of 'x'

דוגמה לכלל 2 - הסמל החזק מנצח:

// file1.c
int x = 5;  // strong (מאותחל)

// file2.c
int x;      // weak (לא מאותחל)
gcc file1.o file2.o -o program
# עובד! x = 5

הסמל החזק (x = 5 מ-file1.c) מנצח את הסמל החלש (x לא מאותחל מ-file2.c).

זה מקור נפוץ לבאגים קשים לאיתור. אם יש לכם משתנה גלובלי int count; בשני קבצים שונים, הלינקר לא ייתן שגיאה - הוא פשוט ימזג אותם למשתנה אחד. שני הקבצים ישתפו אותו בלי שידעו על זה.


רילוקיישן - relocation

אחרי שהלינקר יודע איפה כל סמל מוגדר, הוא צריך:

  1. למזג סקשנים - כל הסקשנים מסוג .text מכל קבצי האובייקט מחוברים לסקשן .text אחד. אותו דבר ל-.data, .bss וכו'.
  2. לקבוע כתובות - כל סמל מקבל כתובת סופית בזיכרון.
  3. לתקן הפניות - כל מקום שמפנה לסמל מתעדכן עם הכתובת הסופית.

דוגמה ויזואלית:

       main.o                    utils.o
  +-------------+          +-------------+
  | .text       |          | .text       |
  |   main()    |          |   add()     |
  |   call ???  | <-- relocation         |
  +-------------+          +-------------+

                    אחרי לינקוג':

            executable
  +---------------------------+
  | .text                     |
  |   0x401000: main()        |
  |   0x40100a: call 0x401050 | <-- הכתובת הוחלפה!
  |   ...                     |
  |   0x401050: add()         |
  +---------------------------+

הלינקר ראה שב-main.o יש רשומת relocation שאומרת "ב-offset הזה, שים את הכתובת של add". הוא מצא את add ב-utils.o, חישב את הכתובת הסופית שלו (0x401050), ומילא אותה.


ספריות סטטיות - static libraries

ספריה סטטית היא פשוט ארכיון של קבצי אובייקט. הסיומת היא .a (מלשון archive).

יצירת ספריה סטטית

# קימפול קבצי המקור לקבצי אובייקט
gcc -c add.c -o add.o
gcc -c multiply.c -o multiply.o
gcc -c divide.c -o divide.o

# יצירת הספריה
ar rcs libmath.a add.o multiply.o divide.o

הפקודה ar (archiver) יוצרת ארכיון. הדגלים:
- r - הכנס/החלף קבצים בארכיון
- c - צור ארכיון חדש אם לא קיים
- s - צור אינדקס (כדי שהלינקר יוכל לחפש סמלים מהר)

שימוש בספריה סטטית

gcc main.c -L. -lmath -o program
  • -L. - חפש ספריות גם בתיקייה הנוכחית
  • -lmath - חבר את הספריה libmath.a (gcc מוסיף את הקידומת lib והסיומת .a אוטומטית)

נקודה חשובה - הלינקר לא מכניס את כל הספריה

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

למשל, אם main.c קורא רק ל-add(), הלינקר ייקח מהספריה רק את add.o. הקבצים multiply.o ו-divide.o לא ייכללו בקובץ ההרצה.

סדר ספריות חשוב

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

# נכון - main.o קודם, הספריה אחריו
gcc main.o -lmath -o program

# שגוי - הספריה לפני הקובץ שצריך אותה
gcc -lmath main.o -o program
# עלול לגרום ל: undefined reference to 'add'

למה? כי כשהלינקר מגיע לספריה, הוא מסתכל אילו סמלים undefined יש לו כרגע. אם הוא עדיין לא ראה את main.o, הוא לא יודע שהוא צריך את add - ולכן הוא לא ייקח את add.o מהספריה. אחר כך כשהוא מגיע ל-main.o, הסמל add הוא undefined ואין עוד ספריות לחפש בהן.

הכלל: הספריה באה אחרי הקובץ שמשתמש בה.


ספריות דינמיות - dynamic libraries (תזכורת)

את הנושא הזה כיסינו בפרק 5.10, אז רק תזכורת קצרה:

ספריות דינמיות (.so - shared objects) שונות מסטטיות בכך שהקוד שלהן לא מועתק לתוך קובץ ההרצה. במקום זה:
- קובץ ההרצה מכיל רשימת ספריות שהוא צריך
- בזמן ריצה, הdynamic linker (ld-linux.so) טוען את הספריות לזיכרון
- מנגנוני PLT ו-GOT מאפשרים קריאה לפונקציות מהספריה

# יצירת ספריה דינמית
gcc -fPIC -shared -o libmath.so add.c multiply.c divide.c

# שימוש
gcc main.c -L. -lmath -o program
LD_LIBRARY_PATH=. ./program
  • -fPIC - יצירת קוד Position Independent (נחוץ לספריות משותפות)
  • -shared - הנחיה ללינקר ליצור shared object ולא executable

ההבדל העיקרי מבחינת הלינקר: בלינקוג' דינמי, הלינקר לא מעתיק קוד. הוא רק רושם את שם הספריה ואת הסמלים שצריך - הפתרון בפועל קורה בזמן ריצה (lazy binding דרך PLT/GOT).


דגלי לינקר חשובים

דגל l- - חיבור ספריה

gcc main.c -lm -o program    # מחבר את libm (ספריית מתמטיקה)
gcc main.c -lpthread -o program  # מחבר את libpthread

הדגל -lXYZ אומר ללינקר לחפש את הספריה libXYZ.so (דינמית) או libXYZ.a (סטטית).

דגל L- - נתיב חיפוש ספריות

gcc main.c -L/opt/mylibs -lmylib -o program

מוסיף את /opt/mylibs לרשימת התיקיות שהלינקר מחפש בהן ספריות.

דגל Wl - העברת אופציות ישירות ללינקר

gcc main.c -Wl,--verbose -o program     # הצגת מידע מפורט מהלינקר
gcc main.c -Wl,-Map=output.map -o program  # יצירת קובץ מפה

הדגל -Wl,option מעביר את option ישירות ללינקר (ld), בלי ש-gcc יתערב.

דגל static- - כפיית לינקוג' סטטי

gcc -static main.c -o program

מכריח את הלינקר להשתמש רק בספריות סטטיות (.a). התוצאה: קובץ הרצה גדול אבל עצמאי, שלא תלוי בשום ספריה חיצונית.

קובץ מפה - map file

gcc main.c -Wl,-Map=program.map -o program

קובץ המפה מראה בדיוק איפה כל סמל הוצב בזיכרון:

.text           0x0000000000401000      0x150
 .text          0x0000000000401000       0x25 main.o
 .text          0x0000000000401030       0x12 utils.o

שימושי לדיבוג ולהבנה של פריסת הזיכרון.


הlinker script של ברירת המחדל

הלינקר לא סתם "זורק" סקשנים לקובץ - הוא עוקב אחרי סקריפט שאומר לו איך לסדר הכל. כל לינקר מגיע עם linker script של ברירת מחדל שאפשר לראות:

ld --verbose

הסקריפט קובע:
- סדר הסגמנטים - text (קוד) לפני data (נתונים) לפני bss
- כתובת התחלה - איפה הקוד מתחיל בזיכרון הוירטואלי
- נקודת הכניסה - entry point - הכתובת הראשונה שהמעבד מריץ

נקודת הכניסה - לא main!

נקודת הכניסה של תוכנית C היא לא main. היא _start. רצף ההפעלה:

_start (מוגדר ב-crt0.o / crt1.o)
  -> __libc_start_main (מוגדר ב-libc)
       -> main (הפונקציה שלנו)
       -> exit (אחרי ש-main חוזר)

הפונקציה _start מסופקת על ידי ספריית ההפעלה של C (C Runtime - crt). היא מכינה את הסביבה (ארגומנטים של שורת הפקודה, משתני סביבה) ואז קוראת ל-__libc_start_main שמאתחלת את libc ובסוף קוראת ל-main.

נוכל לראות את זה:

readelf -h program | grep "Entry"
# Entry point address: 0x401040

nm program | grep "_start"
# 0x401040 T _start

נקודת הכניסה (entry point) היא _start, לא main. בפרק 9.3 נלמד איך לכתוב linker script משלנו ולשנות את נקודת הכניסה.


שגיאות לינקר נפוצות

שגיאה 1 - undefined reference

undefined reference to `my_function'

משמעות: הלינקר מצא שימוש בסמל my_function אבל לא מצא הגדרה בשום קובץ אובייקט או ספריה.

סיבות נפוצות:
- שכחנו לקמפל את הקובץ שמגדיר את הפונקציה
- שכחנו לחבר ספריה (-lm למשל)
- שגיאת כתיב בשם הפונקציה
- סדר ספריות שגוי (הספריה לפני הקובץ שמשתמש בה)

# שכחנו את utils.o:
gcc main.o -o program
# error: undefined reference to 'add'

# תיקון:
gcc main.o utils.o -o program

שגיאה 2 - multiple definition

multiple definition of `counter'

משמעות: שני קבצי אובייקט (או יותר) מגדירים את אותו סמל חזק.

סיבות נפוצות:
- הגדרת פונקציה בheader file בלי static או inline
- הגדרת משתנה גלובלי מאותחל בheader file
- שני קבצי מקור שמגדירים פונקציה עם אותו שם

// bad_header.h - כך לא עושים!
int helper(int x) { return x + 1; }  // הגדרה בheader

// אם שני קבצים עושים #include "bad_header.h",
// שניהם יכילו הגדרה של helper -> multiple definition

// פתרונות:
static int helper(int x) { return x + 1; }  // static - מקומי לכל קובץ
// או:
static inline int helper(int x) { return x + 1; }  // inline
// או: לשים רק הצהרה בheader וההגדרה בקובץ .c

שגיאה 3 - בעיית סדר ספריות

gcc -lmath main.o -o program
# undefined reference to 'add'

למרות שהספריה libmath.a מכילה את add, הלינקר לא מצא אותו. הסיבה: הלינקר עבר על libmath.a לפני main.o, וכשהוא הגיע לספריה הוא עדיין לא ידע שהוא צריך את add.

פתרון:

gcc main.o -lmath -o program

או, אם יש תלויות מעגליות בין ספריות:

gcc main.o -Wl,--start-group -lfoo -lbar -Wl,--end-group -o program

הדגלים --start-group ו---end-group אומרים ללינקר לעבור על הספריות שביניהם שוב ושוב עד שכל הסמלים נפתרים.


כלים לדיבוג בעיות לינקוג'

# הצגת כל הסמלים בקובץ אובייקט/הרצה
nm program

# הצגת סמלים undefined בלבד
nm -u main.o

# הצגת תוכן ספריה סטטית
ar -t libmath.a

# הצגת סמלים בספריה סטטית
nm libmath.a

# מידע מפורט מהלינקר
gcc main.o -Wl,--verbose -o program

# הצגת ספריות דינמיות שקובץ הרצה צריך
ldd program

# הצגת סקשנים
readelf -S program

# הצגת סגמנטים
readelf -l program

סיכום

הלינקר הוא חלק קריטי בתהליך הקומפילציה שלרוב לא חושבים עליו - עד שמקבלים שגיאה. הנקודות המרכזיות:

  • הלינקר פותר סמלים (מתאים שימוש להגדרה) ומבצע relocation (קובע כתובות סופיות)
  • ספריות סטטיות (.a) הן ארכיונים של קבצי .o - הלינקר לוקח רק מה שצריך
  • ספריות דינמיות (.so) לא מועתקות - הקישור קורה בזמן ריצה
  • סדר ספריות בשורת הפקודה חשוב - הספריה אחרי הקובץ שמשתמש בה
  • נקודת הכניסה היא _start, לא main
  • הלינקר עובד לפי linker script שקובע את פריסת הזיכרון

בפרק 9.3 נלמד לכתוב linker scripts משלנו - ולשלוט בדיוק איך הלינקר מסדר את הזיכרון.