close

//========== debug ==================

引用及備分自以下網址

http://www.jeffhung.net/blog/articles/jeffhung/1013/

 

在程式寫作的過程中,通常我們會需要輸出一些訊息,方便我們瞭解程式的運作情形,以便偵錯。因為這些訊息,對程式的真正使用者,也就是我們開發者的 客戶,並沒有意義。是故,通常我們會將程式分成測試版 (debug version 或 debug mode) 或釋出版 (release version 或 release mode),然後只在測試版裡輸出這些訊息,而在釋出版裡完全抑制這些訊息的輸出。

第零版:dprintf_v0.c

這種除錯用的訊息,最直覺簡單的寫法,如下:

#include <stdio.h>

int main()
{
    int i = 3;
#ifndef NDEBUG
    fprintf(stderr, "%s(%d): i == %d\n", __FILE__, __LINE__, i);
#endif
    return 0;
}

// OUTPUT:
// dprintf_v0.c(7): i == 3

在 preprocessing 時檢查 NDEBUG[1] 是否有被定義,如果沒有,表示是為測試版,就將除錯訊息輸出。另外,我們也一併印出,印出訊息的程式碼檔案與所在行號,這對追蹤程式,有著極大的幫助[2]

第一版:dprintf_v1.c

然而,#ifndef/#endif 到處散落在程式裡,實在很亂又難看。所以,乾脆利用 <stdarg.h>,寫成一個 dprintf_v1(),然後只有在測試版才真的印出東西出來:

#include <stdio.h>
#include <stdarg.h>

void dprintf_v1(const char* file, size_t line, const char* fmt, ...)
{
#ifndef NDEBUG
    va_list ap;
    fprintf(stderr, "%s(%d): ", file, line);
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
    fprintf(stderr, "\n");
    fflush(stderr);
#endif
}

int main()
{
    int i = 3;
    dprintf_v1(__FILE__, __LINE__, "i == %d", i);
    return 0;
}

// OUTPUT:
// dprintf_v1.c(20): i == 3

直接在 dprintf_v1() 裡面,利用 NDEBUG 決定是否要輸出訊息。如果是釋出版,dprintf_v1() 就相當於什麼都不做的 function。不過,由於 dprintf_v1() 不一定會與呼叫端,存在於同一個 translation unit[3],因此無法被最佳化,呼叫的 overhead 不可避免。

第二版:dprintf_v2.c

實作 dprintf_v1() 的函式庫,不一定與呼叫 dprintf_v1() 的程式模組,使用相同的編譯模式,同屬於測試版或釋出版。所以,我們最好還是讓呼叫端來決定是否要把訊息印出來,同時,讓呼叫端可以在 compile-time 或 run-time 來決定是否要印出:

#include <stdio.h>
#include <stdarg.h>

// Use parameter enable to choose whether to print the message at run-time.
void dprintf_v2_impl(const char* file, size_t line, int enable, const char* fmt, ...)
{
    va_list ap;
    if (enable) {
        fprintf(stderr, "%s(%d): ", file, line);
        va_start(ap, fmt);
        vfprintf(stderr, fmt, ap);
        va_end(ap);
        fprintf(stderr, "\n");
        fflush(stderr);
    }
}

// Choose whether to print the message at compile-time (preprocessing-time actually)
#ifndef NDEBUG
#   define dprintf_v2 dprintf_v2_impl
#else
#   define dprintf_v2
#endif

int main()
{
    int enable_debug = 1;
    int i = 3;
    dprintf_v2(__FILE__, __LINE__, enable_debug, "i == %d", i);
    return 0;
}

// OUTPUT:
// dprintf_v2.c(29): i == 3

在這個版本裡,我們利用 NDEBUG 來決定,dprintf_v2 是否會被代換成 dprintf_v2_impl

  • 如果會代換,就由第一個參數,於 dprintf_v2_impl() 裡,在 run-time 決定是否要印出訊息,此時,呼叫 dprintf_v2_impl() 的 overhead 已經發生。
  • 如果不代換,剩下的括弧雖然會保留,但括弧與括弧內的參數,相當於是一串由 comma operator 隔開的 expressions,整個括弧最後會被 evaluate 成最後一個 expression 的值與型別[4],也就是 i。相對於整個程式來說,因為沒有任何的 side-effect,這一個括弧,相當於沒有任何意義,理論上會被 optimizer 整個消除。

然而,理論歸理論,我們無法保證這個括弧,會確實地被 optimizer 整個消除。萬一裡面有 side-effect,又沒有被 optimizer 消除,那事情就麻煩了[5]。所以最好在 #define 時,把參數串也包含在內。另外,每次都要寫 __FILE____LINE__ 實在太麻煩了,最好能夠自動得出。

2007-09-26 新增:

若是將 dprintf_v2.c 編譯成釋出版,但 GCC 加上 -Wall 選項,就會跑出這樣的警告訊息:

SHELL> gcc -DNDEBUG -Wall dprintf_v2.c
dprintf_v2.c: In function `main':
dprintf_v2.c:29: warning: left-hand operand of comma expression has no effect
dprintf_v2.c:29: warning: left-hand operand of comma expression has no effect
dprintf_v2.c:29: warning: left-hand operand of comma expression has no effect
dprintf_v2.c:29: warning: left-hand operand of comma expression has no effect
dprintf_v2.c:29: warning: statement with no effect

可見,這個沒有存在意義的括弧與其內容,會被 GCC 偵測出來。

使用 function-like macro 的問題

如果使用有參數的 macro,正式名稱為 function-like macro[6] 的方法來寫的話,會碰到一個問題就是,function-like macro 的參數,是「一對一對應的」,然而因為 dprintf_v2() 的參數個數,為不定個數,所以我們根本不可能窮舉出所有的參數個數,更何況,preprocessor 並沒有所謂的 function overloading 可以用。

另外,像這樣子的直覺想法:

#define dprintf_v2(params) dprintf_v2_impl(params)

也是不可行的。因為在 main() 裡呼叫 dprintf_v2() 時,給的參數有五個之多,而 params 只能代表一個參數,故無法通過編譯。GCC 會產生這樣的錯誤訊息:macro "dprintf_v2" passed 5 arguments, but takes just 1

即使,我們在呼叫端,多使用一層括弧,來避開這個問題,也是沒有用的:

#define dprintf_v2(params) dprintf_v2_impl(params)

void foo()
{
    // 多一層括弧,讓整串參數變成一個 function-like macro 的參數。
    dprintf_v2(("i == %d", i));
}

因為即使 dprintf_v2() 呼叫成功了,在呼叫 dprintf_v2_impl() 時,也會碰到參數個數不符的錯誤。

使用 C99 的 __VA_ARGS__:dprintf_v3.c

在 C99 標準裡,新增了 __VA_ARGS__ 這個東西,可以讓 preprocessor 也支援不定長度參數。使用方法是,就好像 C 的不定個數參數的函式一樣,寫 function-like macro 時,在參數列的最後面寫 ...,然後就可以用 __VA_ARGS__ 代表 ... 所傳入的參數。

有了這個功能,我們就可以解決以上的所有問題:

#include <stdio.h>
#include <stdarg.h>

void dprintf_v3_impl(const char* file, size_t line, int enable, const char* fmt, ...)
{
    va_list ap;
    if (enable) {
        fprintf(stderr, "%s (%d): ", file, line);
        va_start(ap, fmt);
        vfprintf(stderr, fmt, ap);
        va_end(ap);
        fprintf(stderr, "\n");
        fflush(stderr);
    }
}

#ifndef NDEBUG
#   define dprintf_v3(enable, ...) \
           dprintf_v3_impl(__FILE__, __LINE__, enable, __VA_ARGS__)
#else
#   define dprintf_v3(enable, ...) // define to nothing in release mode
#endif

int main()
{
    int enable = 1;
    int i = 3;
    dprintf_v3(enable, "i == %d", i);
    return 0;
}

// OUTPUT:
// dprintf_v3.c (28): i == 3

在這個例子的 main() 裡,呼叫 dprintf_v3() 所用的參數,enable 會對應到 dprintf_v3_impl() 的第三個參數,然後 "i == %d", i 這兩個參數,會對應到 dprintf_v3_impl() 的第四、第五個參數,也就是 __VA_ARGS__ 所在的位置。由於要考慮到呼叫 dprintf_v3() 時,可能只有給 format string 參數,而沒有給要代換掉 format specifiers 的其他參數,因此,...__VA_ARGS__ 所對應到的參數,要包含到 dprintf_v3_impl()fmt。也就是說,不能夠這樣寫:

#define dprintf_v3(enable, fmt, ...) \
        dprintf_v3_impl(__FILE__, __LINE__, enable, fmt, __VA_ARGS__)

否則,若只是像下面這樣呼叫,就會產生錯誤,因為 ...__VA_ARGS__ 一定要對應到至少一個參數。

int main()
{
    int enable = 1;
    dprintf_v3(enable, "a simple string");
    return 0;
}

如此一來,利用 __VA_ARGS__,我們既可以藏 __FILE____LINE__#define 中,呼叫時不必加上這兩個參數,又可以在維持方便好用的呼叫語法的條件下,在釋出版裡利用 preprocessor,從根本上把「印訊息」的所有 overhead 確實消除。真可謂是完美的 dprintf()。唯一的缺點就是,preprocessor 必須要支援 ...__VA_ARGS__,否則依據上面一連串的分析,是無法達到這樣完美的境界的。

偵測是否支援 __VA_ARGS__

最後,讓我們加上偵測 __VA_ARGS__ 的程式碼片段,以避免老舊 compiler 產生編譯錯誤。

由於 __VA_ARGS__ 是 C99 新增的功能,因此我們可以利用 __STDC_VERSION__ 這個 predefined macro name,來判斷 compiler 所支援的標準 C 版本。C99 的 __STDC_VERSION__ 依規定會是 199901L,所以只要 __STDC_VERSION__ 小於 199901L,就讓 preprocesser 印出錯誤訊息,要求使用好一點、新一點的 compiler:

#include <stdio.h>
#include <stdarg.h>

void dprintf_impl(const char* file, size_t line, int enable, const char* fmt, ...)
{
    va_list ap;
    if (enable) {
        fprintf(stderr, "%s (%d): ", file, line);
        va_start(ap, fmt);
        vfprintf(stderr, fmt, ap);
        va_end(ap);
        fprintf(stderr, "\n");
        fflush(stderr);
    }
}

#ifndef NDEBUG
#   if (__STDC_VERSION__ < 199901L)
#       error "Please use a newer compiler that support __VA_ARGS__."
#   else
#       define DPRINTF(enable, ...) \
               dprintf_impl(__FILE__, __LINE__, enable, __VA_ARGS__)
#   endif
#else
#   define DPRINTF(enable, ...) // define to nothing in release mode
#endif

int main()
{
    int enable = 1;
    int i = 3;
    DPRINTF(enable, "i == %d", i);
    return 0;
}

// OUTPUT:
// dprintf.c (32): i == 3

由於其實是個 macro,所以還是依照一般的慣例,使用全大寫命名 DPRINTF()。額外的好處是,既避免了一堆 #ifndef/#endif,仍然可以因為大、小寫差異的關係,讓我們可以方便地看出,那一行程式是不會被包含在釋出版裡。

如果使用 GCC 編譯,請加上 -std=c99 參數,因為 GCC 預設只開啟 C89 與其多加的 extension 功能,否則我們將可以確實地看到 Please use a newer compiler that support __VA_ARGS__ 的錯誤訊息[7]。至於 VC6 這個爛東西,不支援 __VA_ARGS__[8],所以我們得另外想點辦法才行,請期待下一篇文章:《Implementing dprintf() without __VA_ARGS__[9]

 

//========== Assert =================

 

#define ROMF             ((ROM_fun *)0x00000100)
#define ROM_CALL(fun)    ROMF->fun

 

   // enable debug function
#if ASSERT_BY_ERROR_CODE
#define ASSERT(_bool_, _msg_)   \
{                                                                           \
    if (! (_bool_)){                                                        \
        uint32 print_assert = (uint32)(ROM_CALL(print_assert_error_code));  \
        __asm__ __volatile__("move  $r0,%0\n\t" ::"i"(THIS_ASSERT_BASE + __LINE__));       \
        __asm__ __volatile__("move  $r1, $sp\n\t");                         \
        __asm__ __volatile__("jr    %0\n\t"                                 \
                 ::"r"(print_assert) :"$r0","$r1");                         \
    }                                                                       \
}
#else
#define ASSERT(_bool_, _msg_)   \
{                                                                     \
    if (! (_bool_)){                                                  \
        uint32 print_assert = (uint32)(ROM_CALL(print_assert));       \
        __asm__ __volatile__("la    $r0,%0\n\t" ::"i"(__FILE__));     \
        __asm__ __volatile__("move  $r1,%0\n\t" ::"i"(__LINE__));     \
        __asm__ __volatile__("la    $r2,%0\n\t" ::"i"(__FUNCTION__)); \
        __asm__ __volatile__("la    $r3,%0\n\t" ::"i"(_msg_));        \
        __asm__ __volatile__("move  $r4,$sp\n\t");                    \
        __asm__ __volatile__("jr    %0\n\t"                           \
                 ::"r"(print_assert) :"$r0","$r1","$r2","$r3","$r4"); \
    }                                                                 \
}
#endif

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 阿肯 的頭像
    阿肯

    韌體開發筆記

    阿肯 發表在 痞客邦 留言(0) 人氣()