Functions & Calling Conventions

המדריך שלפניכם מציג בתמצית את נושא מוסכמות הקריאה בשפת C ואת השפעתן על יעילות הקוד.
תגלו כיצד cdecl מעבירה פרמטרים בערמת המחסנית, ואילו fastcall מנצלת אוגרים לחיסכון בזמן ריצה.
ננתח את המחיר של קריאה לפונקציה ואת היתרונות והחסרונות של מילת המפתח inline, ונראה מתי כדאי להפוך פונקציה ל-static inline כדי למנוע חשיפת סמבולים.
נבין כיצד מצביעי פונקציה ומערכי VTable מאפשרים קריאות דינמיות, ונביט ב-syscall ישיר להשגת שליטה מלאה במערכת.
מושגים כמו אזור ה-red zone ב-x86_64 ו-debugger חיוניים להעמקת ההבנה.
לבסוף נסקור את השפעת החזרת מבנים גדולים ואת תפקיד ה-registerים בשמירת ערכים.

‪C‬ ‪Functions‬ ‪Calling‬ ‪Conventions‬

זהו מדריך מקיף בעברית, מיושר לימין, בנושא קריאות פונקציות בשפת ‪C‬ והאופן שבו מוסכמות הקריאה (‪Calling‬ ‪Conventions‬) משפיעות על ביצועי התוכנה, על ארגון הקוד ועל הדרך בה מתבצעות קריאות לפונקציות.
מטרת הטקסט לספק הבנה מעמיקה שתסייע לכם לענות על שאלות ותופעות שכיחות הקשורות בתהליך הקריאה וההחזרה מפונקציות, וכן לבחון לעומק מושגים כמו ‪inline‬, ‪fastcall‬, ‪cdecl‬, מצביעים לפונקציות והיבטים מתקדמים נוספים.

מוסכמות קריאה (‪Calling‬ ‪Conventions‬) ומדוע הן חשובות

מוסכמת הקריאה מגדירה כיצד מועברים הפרמטרים לפונקציה, כיצד מתבצעת החזרה של ערכים, ומי אחראי לנקות את המחסנית לאחר הקריאה.
בשפת ‪C‬, הבחירה במוסכמות קריאה יכולה להשפיע על ביצועי התוכנית ועל הדרך בה הקוד הגולמי (‪Assembly‬) ייראה.
לשימוש נכון במוסכמות הקריאה יש חשיבות מיוחדת בתוכניות מערכות, בדרייברים ובקריאות מערכת (‪System‬ ‪Calls‬), שבהן נדרשת אופטימיזציה נמוכה ככל האפשר ותיאום עם מערכת ההפעלה.

מוסכמת ‪cdecl‬

מוסכמת הקריאה הנפוצה ביותר בסביבות רבות של ‪C‬ היא ‪cdecl‬.
זוהי מוסכמה שבה כל הפרמטרים נדחפים למחסנית בסדר מסוים, והקורא (‪Caller‬) הוא זה שאחראי לנקות את המחסנית לאחר חזרת הפונקציה.
ברוב המקרים, ‪cdecl‬ היא ברירת המחדל בקומפיילרים מודרניים כיוון שהיא גמישה לשימוש עם מספר פרמטרים משתנה.
כאשר מחזירים ערך רגיל (כמו ‪int‬ או ‪double‬), הערך מוחזר בדרך כלל באוגר (‪Register‬) המתאים, אך אם מדובר במבנה (‪struct‬) גדול, נהוג להעביר מאחורי הקלעים מצביע אל מקום אחסון שאליו תיכתב התוצאה.

מוסכמת ‪fastcall‬

מוסכמת ‪fastcall‬ באה לייעל את העברת הפרמטרים על ידי שימוש באוגרים עבור חלק מהארגומנטים הראשונים.
כך ניתן לחסוך בגישה למחסנית ולהקטין את הזמן הדרוש לדחיפת פרמטרים ושליפתם, במיוחד כאשר מספר הארגומנטים קטן.
עם זאת, ברגע שחורגים ממספר מסוים של ארגומנטים (לרוב שניים), השאר מועברים בכל זאת דרך המחסנית, בדומה ל-‪cdecl‬.
השילוב בין שימוש באוגרים למחסנית דורש תשומת לב מיוחדת בקוד ‪Assembly‬ ידני או בעת הגדרת מצביעים לפונקציה, מפני שהקומפיילר מצפה שהפרמטרים הראשונים יימצאו באוגרים המתאימים.

‪overhead‬ בקריאות פונקציה והקשר ל-‪inline‬

בכל קריאה לפונקציה נוצרת עלות מסוימת (‪overhead‬), הכוללת דחיפת פרמטרים וכתובת החזרה למחסנית, חישוב הכתובת אליה קופצים ולאחר מכן ניקוי המחסנית עם החזרה לפונקציה הקוראת.
ה-‪overhead‬ הופך משמעותי אם קוראים לפונקציה פעמים רבות, במיוחד בפונקציות קצרות שחוזרות על עצמן בלולאות.
כדי להפחית את ההוצאות הללו, משתמשים לעיתים במילת המפתח ‪inline‬ בשפת ‪C‬, שמציעה לקומפיילר להטמיע (‪Inline‬ ‪Expansion‬) את גוף הפונקציה בקוד הקורא לה, במקום לבצע קריאה בפועל.

יתרונות וחסרונות של ‪inline‬

כאשר קומפיילר מקבל הצעה לבצע ‪inline‬ לפונקציה, הדבר עשוי לחסוך את מנגנון הקריאה והחזרה, משום שהקוד של הפונקציה משובץ ישירות במקום הקריאה.
זה עשוי לתרום לביצועים אם מדובר בפונקציות קטנות או בכאלה הנקראות פעמים רבות מאוד.
מצד שני, שימוש נרחב ב-‪inline‬, במיוחד לפונקציות גדולות, עלול להגדיל את גודל הקוד הסופי ולהעמיס על מטמון ההוראות (‪Instruction‬ ‪Cache‬).
גידול יתר בגודל הקוד עשוי לגרום להאטה במקום האצה, כך שהקומפיילר רשאי להתעלם מהצעת ‪inline‬ כאשר הוא מתעדף אופטימיזציה טובה יותר של הקוד.

‪static‬ ‪inline‬ והיבטי קישור (‪Linking‬)

כאשר מגדירים פונקציה כ-‪inline‬ ‪static‬, מונעים ממנה להפוך לסימבול גלובלי בשלב הקישור.
היא נשארת בהיקף הקובץ הנוכחי (‪Translation‬ ‪Unit‬), וכל קריאה אליה באותו קובץ עשויה להיות מוטמעת ישירות בקוד.
אם הקומפיילר מחליט בכל זאת שלא להטמיע את הפונקציה, היא תקיים הגדרה פנימית (‪Static‬) רק באותו קובץ, ללא חשיפה חיצונית לשאר הקבצים בתוכנית.

הבדלים נוספים בין ‪cdecl‬ ו-‪fastcall‬

ב-‪cdecl‬, הקורא דוחף את כל הפרמטרים למחסנית ומנקה אותה לאחר הקריאה, ואילו בפונקציות ‪fastcall‬, חלק מהפרמטרים מוקצים לאוגרים ייעודיים (כמו ‪ECX‬, ‪EDX‬ בסביבות 32 ביט) לפני פנייה למחסנית עבור שאר הארגומנטים.
מעצם כך, משתנה סדר הפרמטרים וכמות הקוד האסמבלרי הדרושה לקריאה.
אם קוראים לפונקציה שהוגדרה כ-‪fastcall‬, אך עושים זאת כאילו היא ‪cdecl‬, הפרמטרים לא יגיעו כצפוי, והדבר עלול לגרום לשיבוש ערכים או לקריסה.
לכן חשוב לשמור על עקביות ולציין במדויק את מוסכמת הקריאה בהגדרת הפונקציה ובמצביע אליה.

מצביעים לפונקציות (‪Function‬ ‪Pointers‬)

מצביע לפונקציה בשפת ‪C‬ מאפשר לאחסן כתובת של פונקציה במשתנה וכך לקרוא לה באופן דינמי.
לדוגמה, כדי להגדיר מצביע לפונקציה שמקבלת שני ‪int‬ ומחזירה ‪int‬, יש לכתוב ‪int‬ (*f)(‪int‬, ‪int‬).
שימוש במצביעים לפונקציה נפוץ במימושי ‪Callbacks‬, במבני נתונים גמישים وبמערכות המחקות תכנות מונחה-עצמים בשפת ‪C‬, באמצעות ‪VTable‬ ידני.
לעיתים יוצרים מערך של מצביעים לפונקציות וכך עוברים בין מימושים שונים דרך אינדקסים, במקום לעבור דרך offsets של שדות בתוך ‪struct‬.

שימוש במערכי מצביעי פונקציה ובניית ‪VTable‬ ידני

במקום לשמור ‪VTable‬ כ-‪struct‬ מורחב, פעמים רבות מציבים במערך סטטי של מצביעי פונקציה את רשימת הפעולות האפשריות.
ניתן להחליף אינדקסים במערך בהתאם לתפקיד שרוצים לבצע, ובכך להשיג גמישות מבלי לנבור באופסטים של מבנה.
שיטה זו שימושית בתוכנות נמוכות-רמה או כאשר רוצים כמה “מחלקות” (בדומה ל־‪OOP‬) המתנהגות שונה, אך חולקות ממשק זהה.

קריאות מערכת (‪System‬ ‪Calls‬)

בתוכניות ‪C‬ רבות, ובעיקר במערכות הפעלה כמו לינוקס, קריאות מערכת מאפשרות גישה לפעולות ליבה הדורשות הרשאות מיוחדות, לדוגמה פתיחת קובץ, כתיבת נתונים ברשת או הקצאת זיכרון ברמת המערכת.
ספריות סטנדרטיות כמו ‪libc‬ מספקות מעטפת נוחה (למשל ‪fopen‬, ‪fopen_s‬), אך לעיתים נעדיף לפנות ישירות ל־‪syscall‬ מסוים לצורך שליטה טובה יותר בסוג הפרמטרים, בדגלי הפעלה ייחודיים או לשם אופטימיזציה הדוקה יותר.
בעת כתיבת קריאת מערכת ישירה יש לזכור את סדר הפרמטרים ואת האופן שבו הם עוברים למערכת ההפעלה, בהתאם למוסכמה הנהוגה בארכיטקטורה (למשל, מה מועבר באוגרים ומה נדחף למחסנית).

מנגנון ה-“‪red‬ ‪zone‬” ב־‪x86_64‬ ‪SysV‬ ‪ABI‬

בסביבות ‪x86_64‬ שרצות על לינוקס, מוגדר מנגנון המכונה “‪red‬ ‪zone‬”.
זהו אזור זיכרון של 128 בתים מתחת לערך הנוכחי של מצביע המחסנית (‪RSP‬), המאפשר לפונקציה להשתמש בו זמנית מבלי להזיז את ‪RSP‬.
שימוש ב־‪red‬ ‪zone‬ חוסך כמה פקודות ‪Assembly‬ של הקצאה על המחסנית, ובכך מייעל כתיבת קוד עבור משתנים מקומיים קטנים.
אולם יש לשים לב שאם הקוד יוצא מגבולות אזור זה או נעשה מעבר בין גדלים שונים של מסגרות מחסנית, עלולים להיווצר באגים.

החזרת מבנים (‪struct‬) גדולים

כאשר רוצים להחזיר מבנה גדול מפונקציה, נהוג להעביר פרמטר סמוי ראשון שהוא מצביע למקום שבו המבנה יאוחסן.
הפונקציה מעדכנת את התוכן באזור שאליו המצביע מצביע, ולאחר מכן מחזירה ערך רגיל (למשל ‪int‬ המייצג קוד הצלחה) או מחזירה את הכתובת עצמה בהתאם לפלטפורמה.
זוהי אחת הסיבות שב־‪C‬ מקובל לעיתים להחזיר מצביע למבנה או להחזיק את המבנה אצל הקורא ולהעבירו לפונקציה כפרמטר.
כך נחסך קוד ‪Assembly‬ מורכב והעלויות המשתמעות ממנו.

שיקולים בעבודה עם ‪Inline‬ ‪Assembly‬

בכתיבת ‪Inline‬ ‪Assembly‬, בפרט כאשר משתמשים במוסכמות כמו ‪fastcall‬, יש להתחשב באילו אוגרים נפגעים (‪Clobbered‬).
יש להצהיר עליהם בקוד ה-‪Assembly‬ המוטמע כדי שהקומפיילר ישמור את ערכי האוגרים הדרושים לפונקציה בטרם הכניסה לקטע ה-‪Assembly‬, וישחזר אותם לאחר מכן.
התעלמות מהגדרת ‪clobber‬ עשויה לגרום לערכי הפרמטרים “להידרס” באוגרים או לערכים לחזור בצורה שגויה.

חשוב לוודא שבפונקציות המוגדרות כ-‪fastcall‬, גם הקריאה אליהן וגם כתיבת ה-‪Inline‬ ‪Assembly‬ תואמת את המוסכמה, כדי שהפרמטרים ייטענו באוגרים הנכונים.
אי-התאמה בין הגדרת פונקציה למוסכמת הקריאה בצד הקורא תגרום לשיבושים חמורים בשלב הריצה.

מתי להשתמש ב־‪syscall‬ ישיר במקום בספריות סטנדרטיות

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

התנסות, בדיקה והמשך העמקה

לימוד מעשי של קריאות פונקציות ומוסכמות קריאה מתחיל מקוד בסיסי בשפת ‪C‬, דרך קריאת ה־‪Assembly‬ שהקומפיילר מייצר, ועד ביצוע שינויים קטנים ובדיקת ההשפעות על הביצועים ועל גודל הקוד.
העמקה נוספת בנושאי ספריות מערכת, הרצה תחת ‪Debugger‬ וניתוח התנהגות בתרחישים מורכבים, תסייע לתכנן תוכניות מהירות ואמינות יותר.

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

קריאות פונקציה, אם כן, הן אבן יסוד של תכנות בשפת ‪C‬ ושל תכנות מערכות בכלל.
ההחלטה באיזו מוסכמת קריאה להשתמש, מתי לבצע ‪inline‬ ומתי להימנע מכך, וכיצד לכתוב מצביעי פונקציה – היא חלק מהאמנות של פיתוח יעיל.
שילוב של ידע תאורטי ותרגול מעשי בתכנות נמוך-רמה מכין את התשתית לכתיבת תוכנות מורכבות, לאופטימיזציה ולמציאת דרכים חדשניות להשיג שליטה מלאה במה שקורה “מאחורי הקלעים”.

תודה! בזכותכם נוכל להשתפר