位元詩人 [C 語言] 程式設計教學:在 Unix 上用 GCC 和 Clang 檢查 C 或 C++ 程式碼

C 語言
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

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 script 從 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 script ccrun。該命令稿可以在不使用自動編譯設定檔的前提下自動編譯及執行 C 或 C++ 程式碼,但不會檢查程式碼。

結語

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

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。