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