9.2 - הלינקר - הרצאה
הקדמה¶
בפרק 9.1 ראינו שהקומפילציה היא תהליך של ארבעה שלבים, ושהלינקר הוא השלב האחרון - זה שלוקח את כל קבצי האובייקט ומחבר אותם לקובץ הרצה. ראינו גם שקבצי אובייקט מכילים "חורים" (relocation entries) שהלינקר צריך למלא.
עכשיו נצלול לעומק ונבין בדיוק מה הלינקר עושה, איך הוא פותר symbols, איך הוא עובד עם ספריות, ואילו שגיאות נפוצות עלולות לצוץ.
אם למדתם את פרק 5.10 (ספריות משותפות) ופרק 5.11 (פורמט ELF), הרבה מהמושגים כאן יהיו מוכרים. ההבדל הוא שעכשיו אנחנו מבינים את הצד של הלינקר - לא רק את התוצאה, אלא את התהליך.
שתי המשימות של הלינקר¶
הלינקר עושה שני דברים עיקריים:
- פתרון symbols - symbol resolution - מוצא את ההגדרה של כל symbol (פונקציה, משתנה גלובלי) שמשתמשים בו
- רילוקיישן - relocation - קובע כתובות סופיות לכל הסקשנים ולכל הsymbols, ומתקן את כל רשומות הrelocation
נעבור על כל אחד מהם.
פתרון symbols - symbol resolution¶
כל קובץ אובייקט (.o) מכיל טבלת symbols (symbol table) שמפרטת:
- הsymbols מוגדרים - פונקציות ומשתנים גלובליים שהקובץ מגדיר
- הsymbols לא מוגדרים - פונקציות ומשתנים שהקובץ משתמש בהם אבל לא מגדיר
משימת הלינקר: לכל symbol לא מוגדר, למצוא את הקובץ שמגדיר אותו.
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;)
הכללים:
- שני symbols חזקים עם אותו שם - שגיאת לינקר:
multiple definition of X - symbol חזק וsymbol חלש עם אותו שם - הsymbol החזק מנצח
- שני symbols חלשים עם אותו שם - הלינקר בוחר אחד (בדרך כלל הגדול יותר)
דוגמה לכלל 1 - שגיאה:
דוגמה לכלל 2 - הsymbol החזק מנצח:
הsymbol החזק (x = 5 מ-file1.c) מנצח את הsymbol החלש (x לא מאותחל מ-file2.c).
זה מקור נפוץ לבאגים קשים לאיתור. אם יש לכם משתנה גלובלי
int count;בשני קבצים שונים, הלינקר לא ייתן שגיאה - הוא פשוט ימזג אותם למשתנה אחד. שני הקבצים ישתפו אותו בלי שידעו על זה.
רילוקיישן - relocation¶
אחרי שהלינקר יודע איפה כל symbol מוגדר, הוא צריך:
- למזג סקשנים - כל הסקשנים מסוג
.textמכל קבצי האובייקט מחוברים לסקשן.textאחד. אותו דבר ל-.data,.bssוכו'. - לקבוע כתובות - כל symbol מקבל כתובת סופית בזיכרון.
- לתקן הפניות - כל מקום שמפנה ל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 מהר)
שימוש בספריה סטטית¶
-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- - נתיב חיפוש ספריות¶
מוסיף את /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- - כפיית לינקוג' סטטי¶
מכריח את הלינקר להשתמש רק בספריות סטטיות (.a). התוצאה: קובץ הרצה גדול אבל עצמאי, שלא תלוי בשום ספריה חיצונית.
קובץ מפה - map file¶
קובץ המפה מראה בדיוק איפה כל symbol הוצב בזיכרון:
.text 0x0000000000401000 0x150
.text 0x0000000000401000 0x25 main.o
.text 0x0000000000401030 0x12 utils.o
שימושי לדיבוג ולהבנה של פריסת הזיכרון.
הlinker script של ברירת המחדל¶
הלינקר לא סתם "זורק" סקשנים לקובץ - הוא עוקב אחרי סקריפט שאומר לו איך לסדר הכל. כל לינקר מגיע עם linker script של ברירת מחדל שאפשר לראות:
הסקריפט קובע:
- סדר הסגמנטים - 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¶
משמעות: הלינקר מצא שימוש ב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¶
משמעות: שני קבצי אובייקט (או יותר) מגדירים את אותו 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 - בעיית סדר ספריות¶
למרות שהספריה libmath.a מכילה את add, הלינקר לא מצא אותו. הסיבה: הלינקר עבר על libmath.a לפני main.o, וכשהוא הגיע לספריה הוא עדיין לא ידע שהוא צריך את add.
פתרון:
או, אם יש תלויות מעגליות בין ספריות:
הדגלים --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 משלנו - ולשלוט בדיוק איך הלינקר מסדר את הזיכרון.