לדלג לתוכן

10.6 - varargs מתחת למכסה המנוע - פתרון

תרגיל 1 - פתרון: חיזוי מיקום הארגומנטים

1. חלוקת הרגיסטרים. עוברים על הארגומנטים לפי הסדר, ומחלקים אותם לשני תורים נפרדים - אחד לשלמים ומצביעים, אחד לממשיים:

הארגומנט הטיפוס הרגיסטר
"stats" מצביע rdi
3 int rsi
42 int rdx
2.5 double xmm0
"done" מצביע rcx
7 int r8

הנקודה החשובה: 2.5 לא תופס מקום בתור של המספרים השלמים. הוא הולך ל-xmm0, ולכן "done" מקבל את rcx - הרגיסטר הפנוי הבא בתור השלמים, ולא את זה שאחריו.

2. הערך של al יהיה 1, כי הקורא השתמש ברגיסטר וקטור אחד בלבד (xmm0).

3. הערכים ההתחלתיים יהיו gp_offset = 16 ו-fp_offset = 48.

יש שני פרמטרים רגילים עם שם (tag ו-count), והם תפסו את rdi ו-rsi - שני הסלוטים הראשונים. לכן הסלוט הפנוי הראשון הוא של rdx, בהיסט 16. fp_offset מתחיל ב-48 כרגיל, כי אין פרמטרים ממשיים עם שם.

4. אחרי va_arg(ap, int) אחד הערך יהיה gp_offset = 24. הקריאה החזירה את מה שיושב ב-rdx (כלומר 42) וקידמה את המונה בסלוט אחד.

האימות. מקמפלים עם gcc -O0 -S ומסתכלים בקוד. אצל הקורא:

caller:
    leaq    .L.str(%rip), %rdi      ; "stats"
    movl    $3, %esi
    movl    $42, %edx
    movsd   .LCPI1_0(%rip), %xmm0   ; 2.5
    leaq    .L.str.1(%rip), %rcx    ; "done"
    movl    $7, %r8d
    movb    $1, %al                 ; one vector register used
    callq   report

ואצל הפונקציה עצמה:

report:
    movq    %r9,  -200(%rbp)        ; only rdx, rcx, r8, r9 are saved -
    movq    %r8,  -208(%rbp)        ; rdi and rsi hold the named parameters
    movq    %rcx, -216(%rbp)
    movq    %rdx, -224(%rbp)
    movq    %rdi, -8(%rbp)          ; tag
    movl    %esi, -12(%rbp)         ; count
    ...
    movl    $48, 4(%rax)            ; fp_offset = 48
    movl    $16, (%rax)             ; gp_offset = 16

שימו לב שהקומפיילר לא טרח לשמור את rdi ו-rsi לאזור השמירה בכלל. הוא יודע ש-gp_offset מתחיל ב-16, כלומר אף אחד לעולם לא יקרא את שני הסלוטים הראשונים.

תרגיל 2 - פתרון: להסתכל על va_list מבפנים

#include <stdio.h>
#include <stdarg.h>

typedef struct {
    unsigned int gp_offset;
    unsigned int fp_offset;
    void *overflow_arg_area;
    void *reg_save_area;
} my_va_list;

void show(int count, ...) {
    va_list ap;
    va_start(ap, count);

    // va_list is an array of one element, so 'ap' already behaves as a pointer
    my_va_list *p = (my_va_list *)ap;

    printf("initial:  gp_offset=%-4u fp_offset=%u\n", p->gp_offset, p->fp_offset);

    for (int i = 1; i <= count; i++) {
        int value = va_arg(ap, int);
        printf("after #%d: gp_offset=%-4u value=%d\n", i, p->gp_offset, value);
    }

    va_end(ap);
}

int main() {
    show(3, 10, 20, 30);
    return 0;
}

הפלט:

initial:  gp_offset=8    fp_offset=48
after #1: gp_offset=16   value=10
after #2: gp_offset=24   value=20
after #3: gp_offset=32   value=30

gp_offset מתחיל ב-8 כי count הוא הפרמטר היחיד עם שם והוא תפס את rdi. כל va_arg מקדם אותו בדיוק בסלוט אחד - 8 בתים.

נסו להעביר יותר מחמישה ארגומנטים ותראו משהו מעניין: gp_offset יגיע ל-48 ויישאר תקוע שם, כי הרגיסטרים נגמרו. מהנקודה הזו והלאה va_arg מפסיק להשתמש בו לגמרי ומתחיל לקדם את overflow_arg_area במקום - בדיוק ההסתעפות שראינו בהרצאה.

צריך לומר את זה במפורש: הקוד הזה הוא תרגיל לימודי בלבד. הוא מניח את המבנה הפנימי של x86-64, והוא יישבר על כל ארכיטקטורה אחרת ואפילו על אותה ארכיטקטורה עם ספרייה סטנדרטית אחרת. בקוד אמיתי משתמשים במאקרו, תמיד.

תרגיל 3 - פתרון: למה צריך va_copy

הניסיון הראשון, זה שלא עובד:

void print_normalized(int count, ...) {
    va_list ap;
    va_start(ap, count);

    int max = va_arg(ap, int);
    for (int i = 1; i < count; i++) {
        int v = va_arg(ap, int);
        if (v > max) max = v;
    }

    // the list is exhausted - there is nothing left to read
    for (int i = 0; i < count; i++) {
        int v = va_arg(ap, int);   // garbage!
        printf("%d -> %d%%\n", v, v * 100 / max);
    }

    va_end(ap);
}

הלולאה הראשונה הביאה את gp_offset עד הסוף. va_list לא חוזר אחורה מעצמו, ואין מאקרו בשם va_rewind. הלולאה השנייה פשוט ממשיכה לקרוא זיכרון שאינו הארגומנטים שלנו.

הפתרון עם va_copy:

#include <stdio.h>
#include <stdarg.h>

void print_normalized(int count, ...) {
    va_list ap, ap_copy;
    va_start(ap, count);

    // take a snapshot before we consume anything
    va_copy(ap_copy, ap);

    int max = va_arg(ap, int);
    for (int i = 1; i < count; i++) {
        int v = va_arg(ap, int);
        if (v > max) {
            max = v;
        }
    }
    va_end(ap);

    printf("max = %d\n", max);

    for (int i = 0; i < count; i++) {
        int v = va_arg(ap_copy, int);
        printf("%d -> %d%%\n", v, v * 100 / max);
    }
    va_end(ap_copy);
}

int main() {
    print_normalized(4, 25, 50, 100, 75);
    return 0;
}

הפלט:

max = 100
25 -> 25%
50 -> 50%
100 -> 100%
75 -> 75%

שלוש נקודות חשובות:

va_copy חייב להתבצע לפני שהתחלנו לצרוך את המקור. הוא מעתיק את המצב הנוכחי, לא את המצב ההתחלתי - אם נעתיק באמצע, נקבל עותק שמתחיל מהאמצע.

לכל va_copy צריך va_end משלו. במבנה שראינו בהרצאה זה נראה מיותר, אבל על ארכיטקטורות מסוימות va_start ו-va_copy מקצים משאבים, ו-va_end הוא מה שמשחרר אותם.

ולבסוף, אפשרות שלישית שלא דורשת va_copy בכלל: לקרוא ל-va_start פעם שנייה על אותו va_list אחרי va_end. זה חוקי לגמרי, אבל עובד רק אם אנחנו בתוך הפונקציה שקיבלה את הארגומנטים. אם קיבלנו va_list מפונקציה אחרת - כמו ב-vprintf - אין לנו גישה ל-va_start, ו-va_copy הוא הכלי היחיד.