מוסכמת הקריאה של Windows¶
נניח ששני קובצי מקור מסכימים שפונקציה מקבלת חמישה מספרים. עדיין נדרש הסכם נמוך יותר: היכן מונח כל מספר, מי שומר רגיסטרים, איך המחסנית מיושרת ואיפה נמצא ערך החזרה. ההסכם הזה מאפשר לקוד שנבנה בנפרד לעבוד יחד.
נתמקד ב־Windows x64 ובמוסכמת הקריאה הרגילה. הכללים אינם זהים ל־System V שנפוץ בלינוקס, ואינם זהים למוסכמות x86. גם הרחבות כמו __vectorcall הן חוזה אחר. אסור לקחת מספר רגיסטרים שזוכרים מפלטפורמה אחת ולהחיל אותו על כולן.
עוצרים במקום שבו החוזה נהיה גלוי¶
ניצור calls.cpp.
#include <cstdio>
__declspec(noinline)
long long sum5(long long a, long long b, long long c,
long long d, long long e) {
return a + b + c + d + e;
}
int main() {
const auto result = sum5(11, 22, 33, 44, 55);
std::printf("%lld\n", result);
}
המילה noinline היא הרחבה של MSVC שמקלה על הניסוי באמצעות שמירת גבול קריאה. היא אינה חלק משפת C++ התקנית. פותחים את האסמבלי או עוצרים בדיבאגר בהוראה הראשונה של sum5, לפני שה־prolog משנה את המחסנית.
ארבעת הארגומנטים השלמים הראשונים עוברים לפי מיקום ב־RCX, RDX, R8 ו־R9. הארגומנט החמישי נמצא במחסנית. ברגע הכניסה לפונקציה, אחרי שהוראת call הניחה כתובת חזרה, הוא נמצא בדרך כלל ב־[rsp+28h] לפי פריסת הקריאה המתועדת.
הכתובת הזו מתארת את נקודת הכניסה. אחרי הקטנת RSP לצורך משתנים מקומיים, ההיסט משתנה. זו הסיבה שצילום של הוראה אקראית באמצע פונקציה אינו מספיק כדי להסיק היכן נמצא פרמטר.
אזור שהקורא מקצה בשביל הנקרא¶
הקורא מקצה 32 בתים של shadow space עבור ארבעת מיקומי הארגומנטים ברגיסטרים. האזור קיים גם אם הפונקציה מקבלת פחות מארבעה פרמטרים. הנקרא יכול להשתמש בו לשמירת פרמטרים, אך אינו מחויב לעשות זאת.
לכן קריאת ארבעה ערכים מתוך shadow space בדיבאגר אינה מבטיחה שאלה ערכי הפרמטרים. בהתחלה ייתכן שהמידע התקף נמצא רק ברגיסטרים. הסדר שבו המהדר שומר ערכים תלוי בקוד ובאופטימיזציה.
היישור של RSP נשמר לפי כללי ABI בנקודות שבהן הכלל חל. הפעולה call מוסיפה כתובת חזרה, ולכן צריך להביא אותה בחשבון כשמנתחים את ה־prolog. כתיבת אסמבלי ידנית שמפחיתה רק 32 בתים בלי לבדוק את מצב היישור יכולה לשבור פונקציה נקראת שמשתמשת בהוראות המניחות יישור.
מיקום שלם ומיקום נקודה צפה¶
נחליף את פונקציית הניסוי בחתימה הבאה ונבחן את הקריאה.
__declspec(noinline)
double mixed(int a, double b, int c, float d, int e) {
return a + b + c + d + e;
}
הבחירה ברגיסטר נעשית לפי מיקום הארגומנט. הפרמטר הראשון השלם משתמש ב־RCX, השני משתמש ב־XMM1, השלישי ב־R8 והרביעי ב־XMM3. אין דחיסה נפרדת של כל המספרים השלמים לרשימה אחת ושל כל ערכי הנקודה הצפה לאחרת.
בפונקציית varargs חלים כללים נוספים, ובפרט שכפול ערכי נקודה צפה במיקומי הארגומנטים הרלוונטיים גם לרגיסטרים כלליים. לכן cast של מצביע לפונקציה לחתימה אחרת אינו מתקן אי־התאמה. הוא רק מונע מהמהדר להזהיר על החוזה שהפרנו.
מי רשאי לשנות רגיסטר¶
חלק מהרגיסטרים הם volatile מבחינת ABI: הנקרא רשאי לשנות אותם בלי לשחזר את הערך הישן. אחרים הם nonvolatile: פונקציה שמשנה אותם חייבת לשמר ולשחזר את המצב לפי החוזה.
המילה volatile כאן אינה המגדיר volatile בשפת C++. מדובר בהסכם בין פונקציות. המהדר יכול לשמור ערך שחייב לשרוד קריאה ברגיסטר nonvolatile או במחסנית. כאשר רואים הוראות push ו־pop ב־prolog וב־epilog, צריך לבדוק אם הן שומרות רגיסטר כזה, ולא להניח שכל push שייך לפרמטר.
הערך המוחזר של מספר שלם רגיל נמצא ב־RAX, ושל ערך נקודה צפה ב־XMM0. החזרת מבנים תלויה בגודל ובתכונות הטיפוס. במקרים מסוימים הקורא מספק מצביע נסתר לאחסון התוצאה, והוא משנה את מיקומי שאר הפרמטרים.
מבנה שמזיז את המפה¶
נוסיף את הדוגמה הבאה ונבחן את הקריאה בלי לנחש מראש את הייצוג.
struct Triple {
long long x, y, z;
};
__declspec(noinline)
Triple make_triple(long long seed) {
return {seed, seed + 1, seed + 2};
}
הטיפוס גדול מספיק כדי שהחזרה רגילה שלו תשתמש באחסון שסיפק הקורא במוסכמה הנלמדת. מחפשים באסמבלי את כתובת האחסון ואת הכתיבות אליה. הפרמטר הגלוי seed אינו בהכרח הארגומנט הראשון ברמת המכונה.
אין להסיק שמבנה קטן תמיד חוזר ברגיסטר לפי גודלו בלבד. תכונות מחלקה מסוימות משפיעות על כלל ההחזרה. הניתוח מתחיל מהחתימה המדויקת ומכללי ה־ABI, ואז נבדק מול התוצר.
כשלים שנראים אקראיים¶
קריאה דרך typedef שגוי של פונקציה מ־DLL יכולה לעבוד עם קלט מסוים ולהיכשל עם אחר. לעיתים הרגיסטר הלא נכון במקרה מכיל ערך סביר. לעיתים debug שומר ערכים במחסנית ו־release אינו עושה זאת. אף אחת מהתצפיות אינה הופכת את הקריאה לחוקית.
בעת תחקיר כשל כזה משווים שלושה דברים: ההצהרה שהלקוח קימפל, ההגדרה שהספרייה קימפלה וההוראות שמבוצעות בגבול. התיקון הוא הסכם זהה, לא שינוי אקראי של cast או הוספת השהיה.
לקריאה נוספת: מוסכמת הקריאה של Windows x64.