2012年12月24日 星期一

c printf 的不定個數參數



1st edtition: 20120822
2nd edtition: 20121224

這篇文章要解釋 printf 如何傳定不定個數的參數。

test environment: linux debian 32bit (ubutu 64bit chroot debian 32bit)

以下測試結果是在 32 bit 環境下使用, 在 64 bit 環境似乎不太一樣, 我找不到正確方式處理不定個數參數 (而且有些奇怪的現象)。

在不同書上看了很多次, 總是記不起來, 果然只有自己思考過的東西比較容易記住。

每本書總是提到 va_list, 抽象化在編寫程式時這是好處, 但是抽象化隱藏了真正的本質, 非常不利於抽絲剝繭, 對於想要理解真正的本質來說造成了一些阻礙。本篇文章沒有 va_list, 都是 c 基本 type。

化繁為簡是最容易了解本質的方法, 所以讓我們從這個小程式開始:

my_printf.c
 1 // test for nonfixed argument
 2 // only for 32bit platform
 3 #include <stdio.h>
 4 
 5 typedef unsigned char u8;
 6 typedef unsigned int u32;
 7 
 8 int my_printf(const char *fmt, ...)
 9 {
10   int i;
11   char buf[256];
12 
13   u8 *arg = (u8 *)(&fmt + 1); // 1st argument address
14   printf("sizeof(fmt): %d # %p\n", sizeof(&fmt), &fmt);
15   printf("arg1 addr: %p\n", arg); // arg is address
16   printf("arg1 content: %#x\n", *((u32*)arg)); // get arg content is a pointer
17   printf("%s\n", (*((u32*)arg)) );
18 
19   arg = (u8 *)(&fmt + 2); //fmt is 0 argument,  2st argument address
20   printf("arg2 addr: %p\n", arg); // arg is address
21   printf("arg2 content: %#x\n", *((u32*)arg)); // get arg content is a pointer
22   printf("%d\n", (*((u32*)arg)) );
23 
24   return 0;
25 }
26 
27 int main(int argc, const char *argv[])
28 {
29   char buf[]="12";
30   int i=10;
31 
32   printf("buf addr: %p\n", buf);
33   printf("buf: %s\n", buf);
34   printf("i addr: %p\n", &i);
35   printf("i : %d\n", i);
36 
37   my_printf("%x %d", buf, i);
38   return 0;
39 }

執行結果
buf addr: 0xffabf06d
buf: 12
i addr: 0xffabf068
i : 10
sizeof(fmt): 4 # 0xffabf050
arg1 addr: 0xffabf054
arg1 content: 0xffabf06d
12
arg2 addr: 0xffabf058
arg2 content: 0xa
10

37   my_printf("%x %d", buf, i);

這一行會依序把 i, buf, "%x %d" 放入 stack 中, c 語言的呼叫慣例是從最右邊將參數複製的 stack 中。

左邊是 stack 中的位址, 每次都佔用 4 byte (in 32bit environment), 右邊是變數, 我把複製到 stack 的樣子畫一張表格以利說明:

stack
0xffabf058 i (10) 這個 10 是從 i copy 過來的
0xffabf054 buf (0xffabf06d, 這個位址開頭內容 "12")
0xffabf050 某個位址 (我不知道怎麼抓出這位址, 不過不重要) 該位址內容為 "%x %d"

"%x %d" 就是 my_printf 中的 fmt, 處理不定個數參數的原理就是以第一個參數 fmt 為基準, 一步一步找出其他參數。

fmt 的位址, 沒問題就是 &fmt, 那下一個參數的位址呢? fmt+4, 是的, 在 32 bit 環境一個 stack 位址要加或減 4byte。
 
13   u8 *arg = (u8 *)(&fmt + 1); // 1st argument address

那我怎麼 +1 呢?因為這個 +1 就等於 +4, 若真要用 +4, 要用下面的語法

u8 *arg = (u8 *)(&fmt) + 4; // 1st argument address

不過語法不重要, 重要的是得到這個參數的位址後, 再來該怎麼辦?
buf 的 type 是  char [], 表示一個位址, 指向 "12" 這個字串。
所以我得要得到這個位址才行, 這個位址是 0xffabf06d, 所以能得到這位址就成功一半。
 
*((u32*)arg)

這語法就是用來得到這個位址, 我需要從 0xffabf054 (buf) 這位址的內容抓取 4byte (0xffabf06d), 因為這就是指向 "12" 的位址。

memory layout
0xffabf054 (buf) 0xffabf06d

從位址 0xffabf054 開頭 4byte 的內容是 0xffabf06d, 該指令的意思是把 arg 轉成 u32 位址 (轉成指標), 在抓取其內容, 抓多長的資料呢? 抓 4byte (因為 u32) 就好。

而 0xffabf06d 這位址存放著 '1', '2', '\0'

memory layout
0xffabf06d '1'
0xffabf06e '2'
0xffabf06f '\0'

17   printf("%s\n", (u8*)(*((u32*)arg)) );

透過 printf 可印出 "12"。

指標概念有點難懂, 看看傳入整數 (變數 i) 的情形。

19   arg = (u8 *)(&fmt + 2); //fmt is 0 argument,  2st argument address

變數 i 位址, 再來從這位址取 4byte 內容, 這回的資料不是指標, 而是整數 10。

memory layout
0xffabf058 (i) 10

所以 printf 才需要從 %d, %x 去辨識如何將參數的位址作何種方式的解讀, 否則把整數 10 解釋成位址, 那就錯了。

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。