之前寫了一篇關於
c printf 的不定個數參數的文章, 主要是針對 x86 (32 bit)。現在 cpu 換成 cm3, 不知道是不是也一樣。不過我無意間發現 gcc 有提供相應的 function 來處理這個問題。
不過這是作業系統之前的程式, 有個困難的問題, 得先搞定要輸出到哪裡? 這邊選用的是 uart, 嵌入式系統最常用到的 output, 所以要先把 uart 搞定, 還好我們
很早以前就搞定。
gcc 提供了 builtin function:
- __builtin_va_start(ap,fmt);
- __builtin_va_arg(ap, char *);
- __builtin_va_end(ap);
我用了他們實作出一個 printf。L3 的 include 是必須的。
可以正常的運作, 不算太難。但是這樣的黑箱函式並不能滿足我, 我想知道這 3 個 builtin function 到底施了什麼魔法, 怎麼挖開這黑箱呢? trace source code?? 不, 不需要和 libc/libgcc source code 奮鬥, 反組譯就可以揭開這黑箱。
但別著急, 我們先來看看參數是怎麼傳遞到這個不定個數參數的函式裡頭。
很有趣,
int print(int i, int j)
int myprintf(const char *fmt, ...)
這兩個函式在傳參數時, 使用了不同的方法, print 直接用 r0, r1 去接 i, j 的值, myprintf 則出動 stack, 終於破除了我的疑惑。arm 在傳遞 4 個以內的參數是用 register, 多餘的會用 stack, 而 gcc 在對付不定個數參數乾脆用 stack 來傳遞 (不過不是很直覺的那種), 我實在是太蠢了, 竟然想不出來。
arm-none-eabi-objdump
L169, L225~227: L169 在將 r1, r2 傳入 myprintf 時, 還會 push 一下到 stack, 這樣就相當於用 stack 來傳參數。
L233 ~ 247 則只有 r0, r1, r2, r3 用 stack 傳入, 超過四個參數就用 stack 傳入。
L229~231: 單純用 r0, r1 傳參數。
再來的東西就沒什麼秘密了, myp.cpp L16 ~ 18 是我用 +4 的方式從第一個參數得到其他參數。透過第一個 fmt 參數來把其他的參數抓出來, &fmt 抓出 fmt 的位址, 每一次 +4 就是第二個、第三個參數, 簡單容易, 不需要使用 gcc builtin function。那怎麼知道參數有幾個, 難道要一直 +4 下去嗎? 施主, 這就是你的問題了, 要不然 printf 那個 "%d %s" 是用來幹嘛的?
L176~182 則是分析 __builtin_va_arg(ap, char *); 的組合語言, 我標上了注釋, 應該很簡單, 其實也只是 +4 的行為。
source code:
https://github.com/descent/stm32f4_prog/blob/master/cpp/myp.cpp
不過還是使用
- __builtin_va_start(ap,fmt);
- __builtin_va_arg(ap, char *);
- __builtin_va_end(ap);
容易多了, 也不用辛苦去搞懂 gcc 在不同平台的參數傳遞方式。有興趣的朋友可以試試看 x86/x64 的版本會如何。
藉由 c++ 的 namespace, 我不用取個 myprintf 的名字, DS::printf 看來痛快多了。本篇的範例是 c++ 程式, 別看到 printf 就以為是 c 寫的。
透過 qemu-system-arm -M lm3s6965evb -kernel myp.bin -S -gdb tcp::1234 來測試
ref:
Use printf to output stream on STM32F4 (by jserv)
以下影片是整個執行過程:
FYI,
回覆刪除http://stm32f4-discovery.com/ 的 URL 需要更新成 https://stm32f4-discover.net/
Thanks.
Hi James Yang,
刪除謝謝, 已經修正。
謝謝您詳盡的筆記.... 這幾天都被黏在您的網誌上..... 後生可畏啊....
刪除很高興你喜歡這些內容。我年紀不小了, 不一定是後生。XD
刪除