在 Unix 上用 GCC 和 Clang 檢查 C 或 C++ 程式碼

    前言

    GCC 和 Clang 是兩個在 Unix 或類 Unix 系統上主流的 C 和 C++ 編譯器。在編譯程式時,我們可以使用 GCC 或 Clang 開啟選擇性的警告訊息,藉此了解程式碼潛在的問題。此外,我們可以藉由設置參數,鎖定 C 標準的版本。藉由這些特性,可以改善 C 或 C++ 程式碼的品質。

    然而,每次都要手動逐一輸入指令來編譯程式,過於費時費力。即使使用 Make 或其他專案管理軟體,仍然要手動處理設定檔。在本文中,我們使用 shell 命令稿將編譯和執行程式碼的過程自動化,簡化了這項任務。

    注意事項

    本範例程式會編譯及執行 C 或 C++ 程式碼,故勿將本程式用在有安全疑慮的 (untrusted) 程式碼上。

    系統需求

    本範例程式的 shell 命令稿以 POSIX shell 寫成。此外,該 shell 命令稿會檢查系統上是否有安裝 GCC 和 Clang。

    我們在數種 Unix 或類 Unix 系統上測試此命令稿:

    • Ubuntu 18.04 LTS
    • CentOS 8
    • openSUSE Leap 15.1
    • TrueOS (FreeBSD 相容系統)
    • Solaris 11

    由於本程式使用 POSIX shell,故無法在 Windows 上運行。

    使用此命令稿

    我們將本文的範例程式包在微型專案 ccwarn 中。ccwarn 是立即可用的程式,而且該程式附有簡單的說明文件。

    由於該程式是 shell 命令稿,使用前要先給予該命令稿可執行 (executable) 的權限:

    $ chmod +x path/to/ccwarn
    

    在預設情形下,ccwarn 將程式碼視為應用程式,會直接編譯並執行該程式碼:

    $ ccwarn path/to/file.c
    

    除了單一程式碼外,ccwarn 也可處理多程式碼:

    $ ccwarn path/to/*.c
    

    但這些程式碼要能夠編譯成完整的命令列程式,否則會出錯。

    如果想要編譯靜態函式庫,在呼叫 ccwarn 時加上 static 子命令:

    $ ccwarn static path/to/*.c
    

    同理,如果想要編譯動態函式庫,在呼叫時 ccwarn 時加上 dynamic 子命令:

    $ ccwarn dynamic path/to/*.c
    

    有些程式會混合 C 程式碼和 C++ 程式碼,ccwarn 也可以處理這種型態的程式碼:

    $ ccwarn path/to/*.c path/to/*.cpp
    

    如果執行程式時需要參數,可以加上 -- 來區隔主程式的參數和目標程式的參數:

    $ ccwarn path/to/*.c -- --opt param_a param_b param_c
    

    輸入 help 子命令可取得更多關於 ccwarn 的說明:

    $ ccwarn help
    

    在下一節中,我們會講解 ccwarn 的內部實作。如果對實作細節沒興趣,也可以略過講解的部分,把 ccwarn 當成立即可用的程式就好。

    此命令稿的內部流程

    一開始先檢查程式碼的類型:

    if [ "$cmd" = "application" ] \
       || [ "$cmd" = "static" ] \
       || [ "$cmd" = "dynamic" ];
    then
        shift;
    fi
    
    if [ "$cmd" != "application" ] \
       && [ "$cmd" != "static" ] \
       && [ "$cmd" != "dynamic" ];
    then
        cmd="application";
    fi
    

    ccwarn 能處理的類型可能為應用程式 (application)、靜態函式庫 (static library)、動態函式庫 (dynamic library) 三種。預設為應用程式。

    接著,設置 C 編譯器指令:

    GCC=$GCC;
    if [ -z "$GCC" ];
    then
        GCC="gcc";
    fi
    

    在預設情形下,會以 Unix 上常用的慣例為主,例如,GCC 的預設指令即為 gcc,G++ 的預設指令為 g++

    ccwarn 會檢查系統上是否有安裝 GCC 和 Clang:

    if ! command -v $GCC 2>/dev/null 1>&2;
    then
        echo "Please install gcc";
        exit 1;
    fi
    

    當系統上沒有 GCC 和 Clang 時,就無法執行我們設計的任務。這時候會中止程式並吐出相關錯誤訊息。

    由於實際執行任務的程式碼很長,我們先將過程寫成虛擬碼:

    • iterate over each source file
      • check whether file extension is valid
      • create object name according to source name
      • compile the source into object with a C compiler if it is C
      • compile the source into object with a C++ compiler if it is C++
    • compile all objects into an executable
    • run the executable

    我們會把編譯過程分為兩階段,先將原始碼 (source file) 編成目的檔 (object file),再將目的檔編成執行檔 (executable)。由於原始碼可能會混合 C 和 C++,使用這種方式來編譯就可以順利應對。

    以下 shell 程式從 ccwarn 中節錄出來,並加上行號註解:

    if [ "$cmd" = "application" ];                               #  1
    then                                                         #  2
        IS_CPP=0;                                                #  3
        OBJS="";                                                 #  4
        IS_WRONG=0;                                              #  5
        for file in $@;                                          #  6
        do                                                       #  7
            if [ "$file" = "--" ]; then                          #  8
                break;                                           #  9
            fi                                                   # 10
    
            FILE_EXT=$(check_ext "$file");                       # 11
    
            if [ "$FILE_EXT" = "$unsupported_file_extension" ];  # 12
            then                                                 # 13
                IS_WRONG=1;                                      # 14
                continue;                                        # 15
            fi                                                   # 16
    
            filename=$(basename "$file");                        # 17
            objname="${filename%$FILE_EXT}.o";                   # 18
            OBJS="$objname $OBJS";                               # 19
    
            FILE_TYPE=$(check_file_type "$FILE_EXT");            # 20
    
            if [ "$FILE_TYPE" = "c" ];                           # 21
            then                                                 # 22
                $GCC -c -o $objname $file \
                    -Wall -Wextra -std=$CSTD $CFLAGS;            # 23
            elif [ "$FILE_TYPE" = "c++" ];                       # 24
            then                                                 # 25
                IS_CPP=1;                                        # 26
                $GXX -c -o $objname $file \
                    -Wall -Wextra -std=$CXXSTD $CXXFLAGS;        # 27
            fi                                                   # 28
        done                                                     # 29
    
        if [ $IS_WRONG -ne 0 ];                                  # 30
        then                                                     # 31
            exit 1;                                              # 32
        fi                                                       # 33
    
        if [ $IS_CPP -eq 1 ];                                    # 34
        then                                                     # 35
            LIBS_INTERNAL="-lstdc++";                            # 36
        fi                                                       # 37
        $GCC -o $dest $OBJS $LIBS_INTERNAL $LIBS \
            -Wall -Wextra -std=$CSTD $CFLAGS $LDFLAGS;           # 38
    
        args="";                                                 # 39
        is_arg=0;                                                # 40
        for arg in $@; do                                        # 41
            if [ "$arg" = "--" ];                                # 42
            then                                                 # 43
                is_arg=1;                                        # 44
                continue;                                        # 45
            fi                                                   # 46
    
            if [ $is_arg -eq 1 ];                                # 47
            then                                                 # 48
                args="$args $arg";                               # 49
            fi                                                   # 50
        done                                                     # 51
        LD_LIBRARY_PATH=$LD_LIBRARY_PATH ./$dest $args;          # 52
    elif [ "$cmd" = "dynamic" ];                                 # 53
    then                                                         # 54
        # Compile a dynamic library                              # 55
    elif [ "$cmd" = "static" ];                                  # 56
    then                                                         # 57
        # Compile a static library                               # 58
    fi                                                           # 59
    

    原本的程式碼有三段任務,我們為了簡化程式碼,只保留第一段任務的程式碼。第 53 行後的程式碼已經略去了,只保留 if 敘述的部分。

    第 6 行至第 29 行的程式碼在編譯原始碼成目的檔。在編譯目的檔時,會將目的檔移到工作目錄下,所以不需考慮子目錄的議題,但在偶然情形下,會碰到檔名衝突。在這個迴圈中,會根據檔案的副檔名呼叫 C 編譯器或 C++ 編譯器。

    第 34 行至第 38 行的程式碼在編譯目的檔成執行檔。我們一律使用 C 編譯器來編譯程式。若碰到 C++ 目的檔,就加上 -lstdc++libstdc++ 是 C++ 標準函式庫,當為我們有編譯到 C++ 目的檔時,就連結此函式庫。

    第 39 行至第 51 行的程式碼在解析目標程式的命令列參數。該迴圈會偵測 -- 出現的位置,並將之後的命令列參數切割後重接。

    第 52 行的程式碼會執行編譯好的程式。

    程式結束時,不論目標程式正確與否,皆會清掉編譯輸出。清除輸出的函式如下:

    clean ()
    {
        rm -f $dest "lib${dest}.a" "lib${dest}.so" -- *.o;
    }
    

    附註

    除了本範例程式外,我們另外做了一個類似的 shell 程式 ccrun。該命令稿可以在不使用自動編譯設定檔的前提下自動編譯及執行 C 或 C++ 程式碼,但不會檢查程式碼。

    結語

    除了編譯 C 或 C++ 程式碼外,GCC 和 Clang 可以進行靜態程式碼檢查,也可以在編譯時鎖定 C 標準或 C++ 標準的版本。我們應該善用 GCC 和 Clang 的這些特性來檢視 C 或 C++ 程式碼,儘可能減少警告的數量,以改善 C 或 C++ 程式碼的品質。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Yahoo
    【追蹤本站】
    Facebook Facebook Twitter Plurk