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;
}
הפלט:
שלוש נקודות חשובות:
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 הוא הכלי היחיד.