לדלג לתוכן

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

הקדמה

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

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

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


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

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

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

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


פתרון symbols - symbol resolution

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

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

# show the symbols of an object file
nm main.o
                 U printf       # U = undefined - used but not defined
0000000000000000 T main         # T = defined in .text - defines

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

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

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

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

הכללים:

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

דוגמה לכלל 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 - הsymbol החזק מנצח:

// file1.c
int x = 5;  // strong (initialized)

// file2.c
int x;      // weak (uninitialized)
gcc file1.o file2.o -o program
# works! x = 5

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

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


רילוקיישן - relocation

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

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

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

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

                    after linking:

            executable
  +---------------------------+
  | .text                     |
  |   0x401000: main()        |
  |   0x40100a: call 0x401050 | <-- the address was replaced!
  |   ...                     |
  |   0x401050: add()         |
  +---------------------------+

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


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

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

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

# compile the source files into object files
gcc -c add.c -o add.o
gcc -c multiply.c -o multiply.o
gcc -c divide.c -o divide.o

# create the library
ar rcs libmath.a add.o multiply.o divide.o

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

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

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

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

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

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

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

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

# correct - main.o first, the library after
gcc main.o -lmath -o program

# wrong - the library before the file that needs it
gcc -lmath main.o -o program
# may cause: undefined reference to 'add'

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

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


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

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

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

# create a dynamic library
gcc -fPIC -shared -o libmath.so add.c multiply.c divide.c

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

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


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

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

gcc main.c -lm -o program    # links libm (math library)
gcc main.c -lpthread -o program  # links 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     # show detailed info from the linker
gcc main.c -Wl,-Map=output.map -o program  # create a map file

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

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

gcc -static main.c -o program

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

קובץ מפה - map file

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

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

.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 (defined in crt0.o / crt1.o)
  -> __libc_start_main (defined in libc)
       -> main (our function)
       -> exit (after main returns)

הפונקציה _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'

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

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

# we forgot utils.o:
gcc main.o -o program
# error: undefined reference to 'add'

# fix:
gcc main.o utils.o -o program

שגיאה 2 - multiple definition

multiple definition of `counter'

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

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

// bad_header.h - this is not how you do it!
int helper(int x) { return x + 1; }  // definition in header

// if two files do #include "bad_header.h",
// both will contain a definition of helper -> multiple definition

// solutions:
static int helper(int x) { return x + 1; }  // static - local to each file
// or:
static inline int helper(int x) { return x + 1; }  // inline
// or: put only the declaration in the header and the definition in the .c file

שגיאה 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 אומרים ללינקר לעבור על הספריות שביניהם שוב ושוב עד שכל הsymbols נפתרים.


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

# show all symbols in an object file/executable
nm program

# show undefined symbols only
nm -u main.o

# show the contents of a static library
ar -t libmath.a

# show symbols in a static library
nm libmath.a

# detailed info from the linker
gcc main.o -Wl,--verbose -o program

# show dynamic libraries an executable needs
ldd program

# show sections
readelf -S program

# show segments
readelf -l program

סיכום

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

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

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