printf、scanf 等函數支援不定數量參數,靠的是 C 語言可變參數 (Variadic Functions)機制。本文深入剖析堆疊傳參原理,手把手實現一個簡易的 PrintInt 函數,解析 %d 格式化處理流程。
C 語言參數從右向左壓入堆疊:
高地址 +----------------+ | 參數 3 | ← 最後壓入,最高地址 +----------------+ | 參數 2 | +----------------+ | 參數 1 | ← 最先壓入,最低地址 +----------------+ | 返回地址 | +----------------+ | 舊 EBP | +----------------+ 低地址
固定參數 (如格式字串) 位於已知位置,後續可變參數在堆疊中連續分佈。只要知道第一個可變參數地址,按格式字串解析即可依次取值。
<stdarg.h> 宏 (現代標準做法)#include <stdarg.h>
void print_ints(int count, ...) {
va_list args;
va_start(args, count); // 以最後一個固定參數為基準
for (int i = 0; i < count; i++)
printf("%d ", va_arg(args, int)); // 依次取 int 參數
va_end(args);
printf("\n");
}
這是正規、可移植的做法,編譯器處理架構差異 (x86/x64/ARM 參數傳遞不同)。
⚠️ 僅供原理教學,生產代碼嚴禁使用!
void PrintInt(char* buffer, int data, ...) {
static char space[1024];
char temp[32];
int* start = (int*)&buffer; // 危險!假設堆疊佈局
int count = 0;
if (!buffer) return;
memset(space, 0, 1024);
memset(temp, 0, 32);
// 掃描格式字串
while (buffer[count]) {
if (!strncmp(&buffer[count], "%d", 2)) {
start++; // 移至下一個堆疊位置 (假設 int 大小)
itoa(*start, temp, 10); // 整數轉字串
strcat(space, temp);
count += 2;
continue;
}
space[strlen(space)] = buffer[count++]; // 普通字符複製
}
memset(buffer, 0, strlen(buffer));
memmove(buffer, space, strlen(space));
}
void display() {
char buffer[32] = "%d %d %d %d\n";
PrintInt(buffer, 1, 2, 3, 4); // 格式字串 + 4 個 int
printf(buffer); // 輸出: 1 2 3 4
}
| 問題 | 後果 |
|---|---|
| 假設參數在堆疊連續 | x64 呼叫慣例前 4 參數用寄存器 (RCX, RDX, R8, R9),不在堆疊 |
假設 int 大小固定 | 不同架構 int/指標大小不同 |
(int*)&buffer 取址錯誤 | 取的是 buffer 區域變數地址,非參數堆疊位置 |
| 無型別安全 | %d 對應非 int 參數未定義行為 |
| 緩衝區溢出風險 | strcat 未檢查邊界 |