1.1 - סביבת עבודה ומודל ביצוע - הרצאה
מה בדיוק אנחנו בונים¶
בליבת המחשב ראינו איך קוד בשפת C הופך להוראות שהמעבד מבצע. עכשיו נשתמש באותו רעיון כדי ללמוד C++ ולבנות תוכנות שמבינות את Windows. ההתחלה אינה חלון עם כפתור, אלא תוכנית קטנה שאפשר להסביר בה כל ערך וכל שינוי בזיכרון.
נניח שאנחנו רוצים פונקציה שמגדילה מונה. הקוד הבא הוא תוכנית מלאה, שנשמור בקובץ בשם main.cpp:
#include <cstdio>
namespace counters {
void increment(int& value) {
++value;
}
}
int main() {
int count = 7;
counters::increment(count);
std::printf("count=%d\n", count);
std::printf("int=%zu long=%zu pointer=%zu\n",
sizeof(int), sizeof(long), sizeof(void*));
}
התחביר של namespace נותן לנו מרחב שמות. השם המלא של הפונקציה הוא counters::increment, כך שפונקציה אחרת בשם increment יכולה להימצא במרחב אחר. השינוי המעניין יותר נמצא בפרמטר int&.
הפרמטר הוא reference אל אובייקט קיים. בקריאה הזאת הוא מתייחס ל־count, ולכן ההגדלה משנה את המונה המקורי. המחשב אינו חייב להקצות אובייקט נפרד שמייצג את הreference בכל מצב, למרות שבממשק בינארי אפשר לראות לעיתים כתובת עוברת ברגיסטר. החוזה של השפה והמימוש שהמהדר בחר הם שתי שאלות שונות.
הכנת כלי הבנייה¶
הדוגמאות בפרק מיועדות ל־MSVC ול־C++20 על Windows x64. סביבת הבנייה צריכה לכלול את כלי C++ ואת Windows SDK. פותחים את ה־x64 Native Tools Command Prompt של ההתקנה, כדי שמשתני הסביבה יצביעו גם על headers וגם על libraries.
הפקודה הראשונה בודקת שאנחנו אכן מגיעים למהדר:
הפלט כולל את גרסת המהדר. כדאי לשמור אותה לצד הקוד, מפני שתצפית על אסמבלי בלי גרסת מהדר ובלי flags קשה לשחזר. הודעה שלפיה הפקודה אינה מוכרת אינה שגיאה בקוד שלנו; היא מעידה שהכלי חסר או שסביבת הפיתוח לא אותחלה.
הפקודה הבאה בונה גרסת עבודה שנוחה לבדיקה:
הדגל /std:c++20 בוחר את גרסת השפה, והדגל /W4 מפעיל אזהרות שימושיות. הדגל /Od מבטל אופטימיזציות, והדגל /Zi מפיק מידע לדיבוג. הדגל /EHsc מגדיר טיפול בחריגות C++ במודל המקובל של MSVC; נלמד בהמשך את המשמעות והגבולות שלו.
הערך הראשון שהקוד צריך להדפיס הוא count=8. בבניית Windows x64 עם MSVC, הגדלים הצפויים הם ארבעה bytes עבור int, ארבעה עבור long ושמונה עבור מצביע. אלה גדלים של הטיפוסים במימוש הזה, ולא כלל שאומר שכל טיפוס במחשב 64 ביט חייב להכיל 64 ביט.
שינוי קטן שמשנה חוזה¶
נחליף את הפרמטר ב־int value, ונבנה שוב. הפונקציה תקבל העתק של הערך, ולכן שינוי הפרמטר לא ישנה את count. אין כאן בעיית cache או פעולה שאבדה בדרך; פשוט שינינו את האובייקט שעליו הפונקציה עובדת.
הדוגמה הבאה אינה תוכנית נוספת, אלא שתי חתימות להשוואה:
החתימה הראשונה מאפשרת לפונקציה לצפות בערך דרך reference שאינו מאפשר שינוי שלו. ההבטחה אינה הופכת כל alias אפשרי לאובייקט לקריאה בלבד, ואינה אומרת שהנתון בטוח לשימוש מכל thread. החתימה השנייה מאפשרת לשנות את האובייקט שנקשר לפרמטר.
כדאי לקרוא חתימה כחלק מהסבר הבעלות וההרשאות של הפונקציה. בהמשך נוסיף גם שאלות של lifetime: כמה זמן האובייקט חי, מי רשאי לסיים את חייו והאם reference עדיין מתייחס אליו.
גרסת עבודה מול גרסה ממוטבת¶
נבנה את אותו קוד עם אופטימיזציה ונבקש גם קובץ אסמבלי:
cl /nologo /std:c++20 /EHsc /W4 /Zi /O2 /FAcs /Fa:optimized.asm main.cpp /Fe:optimized.exe
optimized.exe
המהדר עשוי להטמיע את increment בתוך הקורא ואף לחשב מראש שהערך המודפס הוא שמונה. במקרה כזה לא חייבת להישאר קריאת פונקציה בקוד המכונה. ההוראות השתנו, אבל ההתנהגות שהשפה דורשת מהתוכנית הזאת נשמרה.
הדיבאגר עלול להראות שמשתנה מסוים אינו זמין או שאין שורת אסמבלי נפרדת לכל שורת מקור. זאת תוצאה אפשרית של אופטימיזציה, ולא הוכחה שמידע הדיבוג שבור. מתחילים מההתנהגות הנדרשת, ורק אז שואלים כיצד המהדר הצליח לממש אותה.
הטעות של מחשב 64 ביט¶
המודל של Windows x64 נקרא LLP64. הטיפוס long נשאר בן 32 ביט, בעוד שמצביע הוא בן 64 ביט. בלינוקס x86-64 נפוץ מודל LP64 שבו גם long הוא בן 64 ביט, ולכן העברת הנחה מפלטפורמה אחת לאחרת עלולה לקטוע ערך.
הגודל של מצביע אינו אומר שכל מרחב הכתובות האפשרי זמין לתהליך. המעבד, מערכת ההפעלה וה־ABI מוסיפים מגבלות משלהם. כרגע חשוב להפריד בין ייצוג של מצביע בתוכנית לבין כתובת פיזית של byte ב־RAM.
אפשר לנסח הנחה של המעבדה בצורה שהמהדר יבדוק:
static_assert(sizeof(int) == 4);
static_assert(sizeof(long) == 4);
static_assert(sizeof(void*) == 8);
הבדיקות האלה מתאימות ליעד שבחרנו. אם בונים בטעות ל־x86, הבדיקה של המצביע אמורה להיכשל. התיקון הוא לברר אם היעד שגוי או שההנחה צריכה להשתנות; מחיקת הבדיקה בלי בירור רק מסתירה את אי־ההתאמה.
מה שומרים מהניסוי¶
שומרים את הקוד, פקודות הבנייה, גרסת הכלים, הפלט וקובץ האסמבלי. מסמנים אילו מסקנות נובעות מחוזה השפה ואילו מתצורת Windows שנבדקה. ההבחנה הזאת תלווה אותנו גם כשנחקור heaps, threadים וקריאות לקרנל.
לקריאה נוספת אפשר להשתמש במדריך Microsoft לבנייה משורת הפקודה ובתיעוד מוסכמות x64.