מהמקור לקובץ אובייקט¶
נניח ששינינו פונקציה, בנינו את הפרויקט והתקבלה הודעת unresolved external symbol. הקובץ שבו נמצאת הפונקציה נראה תקין, ואף אפשר לקמפל אותו לבד. כדי להבין את הכשל צריך להפסיק לחשוב על הבנייה כפעולה אחת. המהדר וה־linker רואים דברים שונים ומקבלים החלטות בשלבים שונים.
בשיעור הזה נבנה תוכנית קטנה בשלבים ונשמור את התוצרים. עובדים מתוך חלון x64 Native Tools של Visual Studio עם תמיכת C++. אין צורך בהרשאות מנהל, וכל הקבצים נשמרים בתיקיית ניסוי חדשה.
שלוש יחידות שאינן יודעות הכול¶
ניצור את הקבצים הבאים. ההצהרה המשותפת נמצאת ב־math.hpp.
המימוש נמצא ב־math.cpp.
#include "math.hpp"
#ifndef SCALE
#define SCALE 1
#endif
int combine(int left, int right) {
return left + right * SCALE;
}
הלקוח נמצא ב־main.cpp.
ה־header נותן למהדר מידע על סוגי הפרמטרים והערך המוחזר. הוא אינו מספק את קוד המכונה של המימוש. לכן המהדר יכול לבנות את main.obj עם הפניה לסמל שעדיין אין לו כתובת סופית.
cl /nologo /std:c++20 /EHsc /W4 /P /Fimath.i math.cpp
cl /nologo /std:c++20 /EHsc /W4 /c /Od /Zi /FAs /Famath.asm math.cpp
cl /nologo /std:c++20 /EHsc /W4 /c /Od /Zi main.cpp
dumpbin /symbols math.obj
dumpbin /relocations main.obj
link main.obj math.obj /DEBUG /OUT:combine.exe
.\combine.exe
הפקודה הראשונה שומרת את פלט preprocessing. חפשו בו את הגוף של combine: המקרו הוחלף לפני שלבי הניתוח המאוחרים. הפקודה עם /c עוצרת לפני link ומייצרת קובץ אובייקט. הקובץ אינו תוכנית שאפשר להפעיל, והוא יכול להכיל הפניות שעדיין לא נפתרו.
שם, כתובת והבטחה להשלמה¶
בקובץ COFF נמצאים sections, סמלים ו־relocations. סמל יכול להגדיר מיקום של פונקציה או לתאר הפניה למימוש חיצוני. רשומת relocation אומרת ל־linker שבמקום מסוים צריך לתקן ערך לאחר שהמיקום של יעד ההפניה ייקבע.
אין להניח שכל relocation הוא כתובת מוחלטת. סוג הרשומה קובע איך מחשבים את התיקון, ולעיתים מדובר בהפרש יחסי למיקום ההוראה. ב־x64 קריאה יחסית נפוצה יכולה להשתמש בהיסט בן 32 ביט גם כשהכתובות עצמן בנות 64 ביט.
ה־linker מאחד קלטים, בוחר sections רלוונטיים, פותר סמלים ומייצר image. בשלב זה הוא יכול לזהות שההצהרה שהלקוח השתמש בה אינה תואמת לשם שנוצר מהמימוש, או שמימוש מסוים כלל לא הגיע אליו.
מפעילים כשל שיש לו סיבה ברורה¶
נסו לקשר רק את main.obj. הכשל צפוי, משום שהלקוח מבקש את combine ואין קלט שמגדיר אותו. עכשיו שנו רק את המימוש כך שהפרמטר השני יהיה long, בלי לשנות את ה־header. קיבלתם עומס נוסף עם חתימה אחרת, ולא מימוש של ההצהרה המקורית.
ב־C++ שם הסמל המעוטר משקף מידע על הפונקציה, כולל טיפוסים לפי כללי ה־ABI של הכלי. לכן int ו־long יכולים להפיק סמלים שונים גם כאשר שניהם בני 32 ביט ב־Windows. שוויון גדלים אינו שוויון טיפוסים.
העתקת אותה הגדרה לא־inline לשני קובצי מקור יכולה לגרום לשגיאת סמל כפול. אין לפתור אותה על ידי הסתרת הודעה או מחיקת קובץ אקראי מהבנייה. צריך להחליט היכן נמצאת ההגדרה היחידה ומה כל יחידת תרגום אמורה לראות.
ספרייה סטטית היא ארכיון לבחירה¶
נארוז את המימוש בספרייה סטטית.
lib /OUT:math.lib math.obj
link main.obj math.lib /DEBUG /OUT:combine_static.exe
dumpbin /linkermember:1 math.lib
הספרייה מכילה קובצי אובייקט ומידע שמאפשר למצוא סמלים. היא אינה בהכרח מעתיקה כל קוד שלה לתוכנית. בחירת אובייקטים ו־sections תלויה בהפניות ובאפשרויות הקישור. לכן תוצר סופי יכול להיות קטן יותר מסכום הקלטים.
מנגנון COMDAT מאפשר לכלים לטפל בהגדרות שנוצרו בכמה יחידות תרגום, למשל מימושי תבניות ו־inline לפי הכללים המתאימים. הוא אינו מתיר לנו להגדיר שתי פונקציות שונות שאמורות להיות אותה ישות ולהניח שה־linker יבחר את הנכונה. הפרת ODR יכולה להתבטא גם בבנייה שמצליחה.
משנים בנייה בלי לשנות מקור¶
בנו שוב את math.cpp עם /DSCALE=4, קשרו מחדש ובדקו את התוצאה. המקור בדיסק לא השתנה, אבל קוד המכונה השתנה. מכאן שה־commit של המקור אינו מספיק לזיהוי בנייה; צריך לשמור גם הגדרות preprocessor, גרסת כלים ו־flags.
האפשרות /O2 יכולה לשנות פריסת קוד ושימוש ברגיסטרים. האפשרות /GL מעבירה מידע נוסף לשלב link-time code generation, ולכן אין לצפות שקובץ שנוצר איתה יהיה ניתן לניתוח בדיוק כמו COFF רגיל בכלי dumpbin. לתרגיל הבסיסי משאירים אותה כבויה.
מגדירים את הקשר בין התוצרים¶
אפשר לבנות את הדוגמה באמצעות CMake, אם הוא מותקן, עם הקובץ הבא.
cmake_minimum_required(VERSION 3.20)
project(BuildPath LANGUAGES CXX)
add_library(math STATIC math.cpp)
target_compile_features(math PUBLIC cxx_std_20)
add_executable(combine main.cpp)
target_link_libraries(combine PRIVATE math)
המודל הזה מתאר תלות ולא רק רשימת פקודות. שינוי ב־header אמור לגרום לבנייה מחדש של היחידות שתלויות בו. כשל שבו תוצר ישן נשאר בשימוש יכול להיראות כמו באג בקוד, ולכן חשוב לדעת לבדוק גם את גרף הבנייה ואת פקודות הבנייה בפועל.
לקריאה נוספת: מפרט PE ו־COFF של Microsoft והתיעוד על אופטימיזציה של תוכנית שלמה.