לדלג לתוכן

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

כמעט כל תוכנית שאתם מריצים בלינוקס תלויה בקוד שהיא לא כתבה בעצמה - פונקציות בסיסיות כמו printf או malloc יושבות בספרייה חיצונית, לא בקובץ ההרצה עצמו. איך בדיוק הקוד הזה "מגיע" לתוכנית שלכם ברגע שהיא רצה, ולמה כמה תוכניות שרצות במקביל יכולות לחלוק בפועל את אותו עותק זיכרון של אותה ספרייה, זה בדיוק מה שספריות משותפות פותרות.

סטטי מול משותף - שני מודלים שונים לגמרי

יש שתי דרכים עיקריות לחבר קוד חיצוני לתוכנית. קישור סטטי - static linking, מעתיק את כל הקוד הנדרש מהספרייה, ומדביק אותו ישירות לתוך קובץ ההרצה הסופי, כבר בשלב הבנייה. התוצאה היא קובץ אחד, גדול יותר, שלא תלוי בשום דבר חיצוני בזמן ריצה - אבל אם עשרה תהליכים שונים משתמשים באותה ספרייה, כל אחד מהם סוחב עותק שלם ונפרד שלה.

קישור דינמי - dynamic linking, לעומת זאת, לא מעתיק כלום לתוך קובץ ההרצה. הוא רק רושם שהתוכנית תלויה בספרייה מסוימת, וקוד הספרייה עצמו נטען בנפרד, בזמן ריצה, מקובץ עם סיומת .so - shared object. אם עשרה תהליכים שונים משתמשים באותה ספרייה משותפת, הקרנל טוען אותה לזיכרון פעם אחת בלבד, וכולם משתמשים באותו עותק פיזי, בזכות אותו מנגנון מיפוי זיכרון שכבר פגשנו.

מי טוען את הספרייה ומתי

ברגע שתוכנית עם תלויות דינמיות מתחילה לרוץ, כמו שראינו כשדיברנו על טעינת תהליכים, הקרנל לא מעביר שליטה ישר לתוכנית - הוא מעביר אותה קודם למקשר הדינמי - dynamic linker, לרוב קובץ בשם דומה ל-ld-linux.so. תפקידו הבלעדי הוא לאתר את כל הספריות המשותפות שהתוכנית זקוקה להן, לטעון אותן לזיכרון, ולפתור את הכתובות של כל הפונקציות החיצוניות שהתוכנית קוראת להן.

אפשר לראות בדיוק אילו ספריות תוכנית זקוקה להן עם הפקודה ldd, שמריצה למעשה את אותו תהליך פתרון תלויות, ומציגה את הרשימה. וכשספרייה מסוימת חסרה או לא נמצאת, זו בדיוק הסיבה לשגיאת "library not found" שרוב המתכנתים נתקלים בה במוקדם או במאוחר.

PLT ו-GOT - איך קריאה לפונקציה חיצונית עובדת בפועל

יש בעיה מעניינת שקישור דינמי צריך לפתור - בזמן שהתוכנית מקומפלת, אף אחד לא יודע עדיין לאיזו כתובת בדיוק תיטען הספרייה בזמן ריצה. הפתרון הוא שני מבני נתונים, טבלת קישור פרוצדורלי - PLT, וטבלת תזוזות גלובלית - GOT. במקום לקפוץ ישירות לכתובת של פונקציה חיצונית, שעדיין לא ידועה בזמן הקומפילציה, התוכנית קופצת דרך רשומה ב-PLT, שמפנה לרשומה מתאימה ב-GOT, שרק בזמן ריצה, אחרי שהמקשר הדינמי סיים לעבוד, מכילה את הכתובת האמיתית של הפונקציה.

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

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

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

למה זה חשוב מעבר לתכנות רגיל

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

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

ואם קישור ספריות גרם לכם לשגיאה מוזרה בעבר, יש קהילה שכבר פתרה כמה כאלה.

הצטרפו לקהילה בדיסקורד

לסיכום

ספרייה משותפת מאפשרת לכמה תוכניות לחלוק אותו עותק זיכרון של קוד חיצוני, במקום שכל אחת תסחוב עותק משלה. המקשר הדינמי טוען את הספריות בזמן ריצה, ומנגנון PLT ו-GOT פותר את כתובות הפונקציות רק ברגע שבאמת צריך אותן. זה מודל שחוסך משאבים ומאפשר עדכונים בלי קומפילציה מחדש, במחיר קטן של זמן טעינה נוסף בכל הפעלה.