2012年12月24日 星期一

c printf 的不定個數參數

1st edtition: 20120822
2nd edtition: 20121224

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 帳號。