פורמט אלף וקישור תוכנית¶
נניח שהקוד מתקמפל, אבל הלינקר מדווח על סימבול חסר. בשלב הזה תיקון הלולאה בראסט לא יעזור לנו. אנחנו צריכים לדעת איזה קובץ אובייקט מגדיר את השם, מי דורש אותו ואיך הלינקר החליט אילו חלקים להכניס לבינארי. קובץ ELF מכיל מידע שמאפשר לענות על השאלות האלה בלי לנחש מתוך הודעת השגיאה בלבד.
נבנה ספרייה קטנה ונשאיר סימבול אחד גלוי. שמרו בקובץ scale.c את המימוש הבא.
התוכנית הבאה בקובץ main.rs קוראת לפונקציה דרך ABI של סי.
unsafe extern "C" { fn scale_value(value: u32) -> u64; }
fn main() {
let result = unsafe { scale_value(6) };
println!("{result}");
}
בנו תחילה רק את קובצי האובייקט ובדקו את הסימבולים. הפקודה nm מציגה בין השאר אם סימבול מוגדר בקובץ או נדרש מקובץ אחר.
cc -c scale.c -o scale.o
nm scale.o
rustc --edition=2024 --emit=obj main.rs -o main.o
nm -u main.o
ar rcs libscale.a scale.o
rustc --edition=2024 main.rs -L . -l static=scale -o static-demo
readelf -h -S -l static-demo
השם static-demo מציין שקישרנו את הספרייה הקטנה שלנו מתוך ארכיון סטטי, ואינו מבטיח שכל הבינארי מקושר סטטית. בדקו את רשימת הסגמנטים ואת החלק הדינמי לפני שאתם מסיקים מה עוד נטען בזמן ההרצה. גם ארכיון .a הוא אוסף של קובצי אובייקט, ולא ספרייה שהקרנל ממפה כיחידה בזמן הרצה.
ההבחנה בין sections לבין segments עוזרת לקרוא את התוצאה. מקטעי section כמו טבלת סימבולים, קוד ונתוני דיבוג משרתים את כלי הבנייה והאבחון. רשומות program header מתארות בין השאר טווחים שהטוען צריך למפות ואת ההרשאות שלהם. כמה sections עשויים להיכלל באותו segment, וחלק מנתוני הדיבוג כלל אינם נדרשים בזיכרון התהליך.
נבנה כעת ספרייה משותפת ונקשר את אותה תוכנית אליה.
cc -fPIC -shared scale.c -o libscale.so
rustc --edition=2024 main.rs -L . -l dylib=scale -o dynamic-demo
readelf -d dynamic-demo
readelf -Ws libscale.so
LD_LIBRARY_PATH="$PWD" ./dynamic-demo
הדגל -L בזמן הבנייה אומר ללינקר היכן לחפש את הספרייה, ואינו מבטיח שהטוען הדינמי יחפש באותה תיקייה בזמן הרצה. זאת הסיבה שתוכנית יכולה להיקשר בהצלחה ולהיכשל בהפעלתה. המשתנה LD_LIBRARY_PATH מתאים לניסוי מקומי, אבל אינו תחליף למדיניות התקנה והרשאות ברורה של מוצר.
הטבלאות GOT ו־PLT משתתפות בחלק ממנגנוני הגישה לסימבולים דינמיים. במקום לקבע כל כתובת בספרייה בזמן בנייה, הקוד יכול להגיע אליה דרך מקום שהטוען מעדכן. פרטי ההוראות והאם מתבצע lazy binding תלויים בארכיטקטורה ובאפשרויות הקישור. חפשו relocation שמתייחס ל־scale_value, ואז חברו אותו להוראת הקריאה בפלט objdump -d.
שנו עכשיו את שם הפונקציה בסי בלבד ובנו מחדש. הודעת undefined reference אומרת שהדרישה עדיין קיימת בצד ראסט, אבל אין הגדרה מתאימה בין הקלטים שהלינקר קיבל. החזרת השם בכותרת של ראסט בלי לתקן את הסמנטיקה אינה פתרון אם חתימת הפונקציה השתנתה. הלינקר בדרך כלל אינו משווה טיפוסים של פרמטרים כפי שהמהדר של השפה עושה.
הסתרת סימבולים יכולה לצמצם את הממשק הבינארי של ספרייה. אם פונקציה פנימית נשארת גלויה, משתמשים יכולים להתחיל להסתמך עליה גם אם לא התכוונו לכך. בדקו את הטבלה הדינמית ולא רק את טבלת הסימבולים המלאה, משום שנתוני דיבוג ושמות פנימיים אינם בהכרח חלק מהממשק שהטוען פותר.
אל תריצו ldd על בינארי בלתי מהימן כדי לחקור אותו. עבור הקבצים שבנינו כאן אפשר להשתמש בכלים הרגילים, אבל בחקירה של קלט חיצוני עדיף להתחיל בקריאת המטא־נתונים באמצעות readelf. ההפרדה בין קריאת קובץ לבין הפעלת הטוען חשובה בדיוק משום שאנחנו מנסים להבין מה עוד יכול לרוץ בזמן האתחול.
הסבר מבנה הפורמט נמצא בעמוד ELF של לינוקס, וכללי חיפוש ספריות מתועדים בעמוד הטוען הדינמי. השתמשו במסמכים כדי לתת שם לטבלה שאתם רואים, ואז הוכיחו את הקשר באמצעות הבינארי שבניתם.