אסמבלי מוטבע בשפת C מוסבר - Inline Assembly¶
קומפיילרים מודרניים חכמים מאוד. הם מייצרים קוד מכונה יעיל, מבצעים אופטימיזציות שקשה לדמיין שבן אדם יעשה ידנית, ובדרך כלל הכי נכון זה לתת להם לעשות את העבודה. אז למה בכלל יש מצב שבו מתכנת מחליט לעקוף את כל זה, ולכתוב פקודות אסמבלי ממש בתוך קובץ C? זו בדיוק השאלה שהפוסט הזה עונה עליה.
מה זה בעצם אסמבלי מוטבע¶
אסמבלי מוטבע, או Inline Assembly, הוא מנגנון בשפת C (ובעוד שפות ברמה נמוכה) שמאפשר להטמיע בלוק של פקודות אסמבלי ישירות בתוך פונקציה כתובה ב-C. במקום לכתוב פונקציה שלמה בקובץ אסמבלי נפרד ולקשר אותה, אתם כותבים כמה שורות אסמבלי בדיוק במקום שבו אתם צריכים אותן, בתוך הקוד הרגיל שלכם. בקומפיילר GCC ו-Clang זה נעשה עם מילת המפתח asm או __asm__, ואז בלוק טקסט שמכיל את פקודות המעבד הרצויות.
חשוב להבין שזה לא איזה טריק אקזוטי - זה כלי לגיטימי שנמצא בשימוש בקוד קרנל, בדרייברים של חומרה, ובספריות ביצועים קריטיות. אבל הוא כלי שצריך לדעת מתי בדיוק להוציא מהארגז.
למה בכלל צריך את זה כשיש קומפיילר¶
ישנן כמה סיבות אמיתיות שבגללן מתכנתים פונים לאסמבלי מוטבע, וכולן קשורות למקום שבו C, כשפה, פשוט לא מגיעה.
הסיבה הראשונה היא גישה לפקודות מעבד שאין להן מקבילה ישירה ב-C. יש פקודות אסמבלי ספציפיות - למשל פקודות שמדברות ישירות עם רגיסטרים מיוחדים של המעבד, פקודות סנכרון ברמת חומרה, או פקודות שמבצעות פעולות אטומיות מיוחדות - שאין להן ביטוי טבעי בתחביר של C. אם אתם צריכים בדיוק את הפקודה הזאת, אין ברירה אלא לכתוב אותה כמו שהיא.
הסיבה השנייה היא קוד שדורש שליטה מדויקת ברגיסטרים או בכתובות זיכרון ספציפיות. בקוד bootloader, למשל, לפני שיש בכלל מערכת הפעלה או ניהול זיכרון מסודר, לפעמים חייבים לקבוע בדיוק אילו ערכים נמצאים באילו רגיסטרים ברגע נתון. גם בחלקים מסוימים בקרנל של מערכת הפעלה, כמו בקוד שמטפל בהחלפת הקשר בין תהליכים (context switch), יש צורך בשליטה ברמה כזאת שרק אסמבלי מספק.
הסיבה השלישית, הנדירה יותר, היא אופטימיזציה ידנית. במקרים חריגים, מתכנת מנוסה מאוד שמכיר לעומק את ארכיטקטורת המעבד יכול לכתוב רצף פקודות אסמבלי שמהיר יותר ממה שהקומפיילר מייצר. זה קורה בעיקר בספריות ביצועים קריטיות כמו עיבוד אותות או קריפטוגרפיה, ובדרך כלל אחרי מדידה ובדיקה, לא כניחוש.
המושג של Constraints - איך אומרים לקומפיילר מה קורה¶
אחד הדברים המבלבלים ביותר בהתחלה הוא איך בכלל הקומפיילר "יודע" אילו משתנים מהקוד ה-C שלכם צריכים להיכנס לאילו רגיסטרים בתוך בלוק האסמבלי. כאן נכנס מנגנון שנקרא constraints, או אילוצים.
כשכותבים אסמבלי מוטבע מורחב (extended inline assembly), מציינים לקומפיילר במפורש אילו משתנים הם קלט, אילו הם פלט, ואילו רגיסטרים או סוגי מיקום מותרים לכל אחד מהם. לדוגמה, אפשר לומר "המשתנה הזה צריך להיות ברגיסטר כללי כלשהו" או "המשתנה הזה חייב להיות ברגיסטר ספציפי". הקומפיילר לוקח את המידע הזה ודואג להעביר את הערכים הנכונים למקומות הנכונים לפני שהאסמבלי רץ, ולקחת את התוצאה בחזרה אחרי.
זה בעצם חוזה בין הקוד שלכם לקומפיילר - אתם אומרים לו "אני עומד לגעת ברגיסטרים האלה ובזיכרון הזה", והוא דואג שכל שאר הקוד סביב הבלוק הזה יתאים את עצמו נכון, כולל שמירה של ערכים שלא רציתם שישתנו.
הסיכונים שכדאי להכיר לפני שקופצים לזה¶
השימוש באסמבלי מוטבע לא בא בחינם, ויש כמה סיכונים אמיתיים שכדאי להכיר.
- חוסר פורטביליות. קוד אסמבלי כתוב לארכיטקטורת מעבד ספציפית. בלוק שעובד מצוין על x86 פשוט לא יעבוד על ARM, וצריך לכתוב גרסה נפרדת לכל ארכיטקטורה שרוצים לתמוך בה.
- שבירת אופטימיזציות של הקומפיילר. כשיש בלוק אסמבלי בתוך פונקציה, הקומפיילר מוגבל ביכולת שלו לבצע אופטימיזציות סביבו, כי הוא לא באמת "מבין" מה קורה שם. אם ה-constraints לא מדויקים, אפשר בקלות ליצור באגים עדינים שקשה לאתר.
- קושי בתחזוקה. קוד אסמבלי הרבה יותר קשה לקרוא ולהבין ממתכנתים אחרים, ובהמשך גם ממי שכתב אותו. כל שינוי דורש הבנה עמוקה של הארכיטקטורה, לא רק של הלוגיקה של התוכנית.
בגלל כל זה, הכלל הזהב הוא להשתמש באסמבלי מוטבע רק כשבאמת אין דרך אחרת, ולתעד היטב למה הוא שם.
לסיכום¶
אסמבלי מוטבע הוא כלי חד וממוקד - הוא נותן לכם גישה ישירה לחומרה במקומות שבהם C לא מספיקה, אבל המחיר הוא איבוד הנוחות והביטחון שהקומפיילר נותן. ההבנה מתי להשתמש בו, ואיך להשתמש בו נכון דרך constraints, היא בדיוק הסוג של ידע שמבדיל בין מי שמכיר C ברמה שטחית לבין מי שמבין באמת מה קורה מתחת למכסה המנוע. בקורס ליבת המחשב אנחנו יורדים בדיוק לרמות האלה - הבנה אמיתית של מה שקורה בין שפת התכנות לחומרה עצמה.
מוזמנים להביא שאלות, קטעי קוד וספקות לקהילה - זה בדיוק המקום לדבר על דברים ברמה נמוכה כזאת.