January 31, 2012

C 編譯器跟你想的不一樣

2008 年筆者在 COSCUP 發表了題目為「我是軟體 -- 那些處理器教我的事」的演講,探討因為處理器架構與系統軟體組態或假設不同,導致一連串令人意外的結果,著眼於涉及跨平台開發所潛藏有如未爆彈的種種議題。日前嘗試修改某個客戶的專案,沒想到又踩到另一個地雷,自詡是「慣 C」迷的筆者,萬萬沒想到還得交叉對照組合語言輸出,才克服問題,撰文分享如下。

考慮以下程式碼:(test.c)
#include <stdio.h>
#define DEBUG 1
#define DBG( ... ) \
    if (DEBUG) {  __VA_ARGS__; }
int main(int argc, char *argv[]) {
    char *num;
    switch (argc - 1) {
             case  0: num =  "zero";
        DBG( case  1: num =   "one"; )
        DBG( case  2: num =   "two"; )
        DBG( case  3: num = "three"; )
        DBG( default: num =  "many"; )
        while (--argc)
            printf("%s ", argv[argc]);
        printf("\nArgument count: %s\n", num);
        break;
    }
    return 0;
}
先不看 DBG() macro 的使用,似乎沒什麼特別之處,編譯並執行看看:
$ gcc -Wall -o test test.c
$ ./test 1 && ./test 1 2 && ./test 1 2 3 && ./test 1 2 3 4
1
Argument count: many
2 1
Argument count: many
3 2 1
Argument count: many
4 3 2 1
Argument count: many
完全符合預期,就是把 switch-case 走訪一遍,因為沒有 break 敘述,就到了最終的 "default" 條件,所以 char *nnum 指向字串 "many" 的記憶體位址。但倘若我們將第二行的 DEBUG 定義修改為以下:
#define DEBUG 0
再來重新編譯與執行看看:
$ gcc -Wall -o test test.c
$ ./test 1 && ./test 1 2 && ./test 1 2 3 && ./test 1 2 3 4
1
Argument count: one
2 1
Argument count: two
3 2 1
Argument count: three
4 3 2 1
Argument count: many
這是什麼狀況呢?被一串 if (0) { ... } 包圍的 switch-case 竟然會被處理,而且 break 巧妙地伴隨每個敘述出現。也就是說,原本的 "DBG( case 1: num = "one"; )" 相當於 break; case 1: num = "one; break; 的效力。像這樣的 C 語法結構,可歸納為 [Clifford's Device],簡單來說,就是符合以下的結構:
if (0) { label: ... }
引述網頁描述:
    "it is skipped in the normal flow of execution and is only reached via the goto label, reintegrating with the normal flow of execution and the end of the if (0) statement. It solves a situation where one would usually need to duplicate code or create a state variable holding the information if the additional code block should be called."
在普通的執行流程中,if (0) { ... } 包圍的陳述指令會被忽略,但倘若配合 goto 時 (本例是 switch-case,一個 C 語法的變形),就不是這麼一回事,翻譯成組合語言或機械碼時,其實是很清楚且常見的結構。這樣的結構對於縮減煩冗的程式碼有些特別用途。上述程式碼有趣之處,就在於像 DBG() 這類的 macro 在前期開發階段通常被設定為 "1",在後期則會逐步取消偵錯 / 驗證的程式碼時,將 macro 改為 if (0) { ... } 型態,而使開發者誤踩到「未爆彈」。
由 jserv 發表於 January 31, 2012 1:21 PM
迴響
發表迴響









記住我的資訊?