לתוכנה לא אמור להיות אכפת היכן היא הותקנה
קחו תוכנית שנבנתה לחיות ב-/usr והעתיקו אותה ל-/opt/myapp. ביוניקס, לרוב, היא נשברת. היא אינה
מוצאת את הספריות המשותפות שלה, או את ההגדרות שלה, או את קבצי הנתונים שלה, או את התוספים שלה, מפני
שכל אחד מהנתיבים האלה נאפה לתוך הקובץ הבינארי בזמן הקומפילציה כמחרוזת מוחלטת. זה כל כך נורמלי
ביוניקס שהפסקנו לשים לב שזה פגם. זה פגם, ותמיד אפשר היה לתקן אותו.
מה נאפה פנימה
בנייה טיפוסית מקבעת שלושה סוגי מיקום. נתיב החיפוש של הספריות: היכן המקשר הדינמי מחפש את קבצי ה-.so
שהתוכנית צריכה. תחילית הנתונים: היכן התוכנית מחפשת את הסמלים, התרגומים, התבניות וקבצי העזרה שלה.
נתיב ההגדרות: מהיכן היא קוראת את ההגדרות שלה. הגדירו עם --prefix=/usr ושלושתם הופכים למחרוזות
מילוליות בתוך קובץ ההרצה.
הזיזו את קובץ ההרצה וכל אחת מהמחרוזות האלה שגויה עכשיו. התוכנית לא הותקנה; היא רותכה.
נתיב הספריות: $ORIGIN
המקשר הדינמי תומך במיקום מחדש כבר זמן רב, ורוב האנשים מעולם לא השתמשו בזה. כשקובץ בינארי מקושר עם
rpath, המקשר מחפש ספריות בנתיב הזה. ה-rpath יכול להכיל את האסימון $ORIGIN, שהמקשר מחליף בזמן ריצה
בספרייה שבה חי קובץ ההרצה עצמו:
gcc -o myapp main.o -L./lib -lmylib -Wl,-rpath,'$ORIGIN/../lib'
עכשיו הקובץ הבינארי מוצא את lib/libmylib.so ביחס למיקומו שלו, יהיה המיקום הזה אשר יהיה. שימו את
העץ ב-/usr, ב-/opt, בספריית הבית שלכם, על התקן USB: הספריות נמצאות. הגרשיים הבודדים חשובים, כדי
שהמעטפת לא תנסה להרחיב את $ORIGIN בעצמה. אפשר לבדוק מה קובץ בינארי נושא עם:
readelf -d myapp | grep -i rpathנתיב הנתונים: מצאו את עצמכם בזמן ריצה
לכל השאר התוכנית צריכה לחשב את התחילית שלה במקום להניח אותה. בלינוקס הנתיב של קובץ ההרצה עצמו זמין
דרך /proc:
char exe[PATH_MAX];
ssize_t n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
exe[n] = '\0';
/* dirname(exe) is bin/, so the prefix is one level up */
משם share/, etc/ ו-lib/ הם נתיבים יחסיים. העץ כולו הופך ליחידה עצמאית שעובדת מכל מקום. תוכניות
Windows ו-macOS עושות זאת מאז ומתמיד, וזו הסיבה שיישום שם הוא תיקייה שאפשר לגרור; תוכניות יוניקס
יכלו לעשות זאת כל הזמן וברובן בחרו שלא.
יש פרט של ניידות: /proc/self/exe הוא לינוקס. במערכות אחרות יש מקבילים, ו-argv[0] בתוספת PATH
הוא הפתרון האחרון. פונקציה קטנה בהפעלת התוכנית מטפלת בכל זה פעם אחת.
מדוע יוניקס נלחמה בזה
תקן היררכיית מערכת הקבצים תוכנן לבעיה אחרת. הוא רצה כל קובץ בינארי ב-/usr/bin, כל ספרייה
ב-/usr/lib, כל קובץ הגדרות ב-/etc, כדי שמנהל מערכת יוכל למצוא כל דבר בלי לדעת מאיזו חבילה הוא
הגיע, לשתף /usr לקריאה בלבד בין מכונות, ולשים את /var על דיסק שיכול להתמלא בלי להפיל את
המערכת. אלה היו מטרות טובות למכונה מרובת משתמשים עם מנהל אחד ומספר קטן של חבילות, כולן מותקנות
ממקור אחד.
ההיררכיה מיטבה את נקודת המבט של המנהל על המכונה, במחיר עצמאותה של התוכנית ממנה. ברגע שחבילה מפוזרת על פני שש ספריות, היא יכולה לעבוד רק בדיוק בתצורה הזו, ומנהל החבילות הופך לדבר היחיד שיודע איך להרכיב אותה מחדש. זה בסדר כל עוד יש מנהל חבילות אחד והכול עובר דרכו. זה מפסיק להיות בסדר ברגע שאתם רוצים שתי גרסאות של משהו, או גרסה שההפצה אינה מספקת, או למסור תוכנית למישהו כדבר אחד.
מה בא אחר כך
הלחץ לא נעלם, והתשובות שהופיעו מאוחר יותר הן כולן צורות של מיקום מחדש. חבילות יישום עצמאיות
ללינוקס, עץ שלם בקובץ אחד שמעגן את עצמו ורץ מהמקום שבו הוא נמצא, הן $ORIGIN בתוספת תחילית בזמן
ריצה שנלקחו עד סופם. מנהלי חבילות פונקציונליים שמתקינים כל חבילה בספרייה מגובבת משלה ולעולם לא
ל-/usr הם אותו רעיון מהצד השני: דבר אינו נמצא אף פעם במיקום ידוע, ולכן הכול חייב למצוא את מה שהוא
צריך לפי הפניה ולא לפי מוסכמה. קונטיינרים מוותרים על השאלה לחלוטין ושולחים את מערכת הקבצים כולה.
כל אחד מאלה קיים מפני שההחלטה המקורית, שתוכנית רשאית להניח היכן היא חיה, הייתה שגויה, והתיקון זול.
קשרו עם $ORIGIN. מצאו את הנתיב שלכם בהפעלה. לעולם אל תכתבו נתיב מוחלט לתוך קובץ בינארי. תוכנית
שעושה את שלושת הדברים האלה מותקנת בהעתקה, מוסרת במחיקה, ורצה בכמה גרסאות זו לצד זו שרק תרצו
לשמור. זה מה שהתקנה הייתה אמורה להיות.