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。
化繁為簡是最容易了解本質的方法, 所以讓我們從這個小程式開始:
37 my_printf("%x %d", buf, i);
這一行會依序把 i, buf, "%x %d" 放入 stack 中, c 語言的呼叫慣例是從最右邊將參數複製的 stack 中。
左邊是 stack 中的位址, 每次都佔用 4 byte (in 32bit environment), 右邊是變數, 我把複製到 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" 的位址。
0xffabf054 (buf) | 0xffabf06d |
從位址 0xffabf054 開頭 4byte 的內容是 0xffabf06d, 該指令的意思是把 arg 轉成 u32 位址 (轉成指標), 在抓取其內容, 抓多長的資料呢? 抓 4byte (因為 u32) 就好。
而 0xffabf06d 這位址存放著 '1', '2', '\0'
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。
0xffabf058 (i) | 10 |
所以 printf 才需要從 %d, %x 去辨識如何將參數的位址作何種方式的解讀, 否則把整數 10 解釋成位址, 那就錯了。
沒有留言:
張貼留言
使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。
我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。