לדלג לתוכן

מוסכמות קריאה מוסברות - Calling Conventions

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

אז מה זה בעצם מוסכמת קריאה

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

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

איך פרמטרים עוברים בפועל - רגיסטרים מול מחסנית

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

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

מי מנקה את המחסנית - caller מול callee

עוד היבט חשוב של מוסכמת קריאה הוא מי אחראי לנקות אחרי הקריאה את המקום שתפסו הארגומנטים על המחסנית. יש שתי גישות עיקריות ברמה המושגית.

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

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

למה יש כמה מוסכמות שונות בכלל

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

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

למה זה קריטי להנדסה לאחור

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

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

לסיכום

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

בואו תבנו את היכולת הזו נכון, עם קהילה שכבר שם.

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