在嵌入式裝置上使用 C++ 進行開發:G++ 實做篇

這篇是承接我之前的文章 「在嵌入式裝置上使用 C++ 進行開發:概念篇」, 以實做的角度出發,提供使用 GNU G++ 為嵌入式裝置編寫 C++ 程式的方法。

把 C++ 編譯器般上嵌入式的建置環境中大約需要以下步驟:

  1. 關閉 C++ 編譯器的部份功能支援。
  2. 避免使用某些成本較大的 C++ 特性。
  3. 移除編譯器提供的 C++ 執行時期程式庫。
  4. 解決因移除基礎程式庫後所缺失的某些必須之元件。

關閉編譯器部份功能

加入下列編譯選項以關閉 G++ 部份功能支援:

  • -fno-exceptions: 停用例外處理機制。
  • -fno-rtti: 停用 RTTI 機制。
  • -nostdinc++: 不搜尋 C++ 標準標頭檔,等同禁用 C++ 標準程式庫。

避免使用部份功能

除了那些由編譯器支援的執行時期機制必須要關閉, 否則就算你不去使用它也不能避免它們對程式的影響之外, 有一些其他高成本的功能是編譯時期完成的,只要不去使用它們就沒事了, 如模版與多重繼承等等。

除了自己小心不要寫上包含這些功能的程式碼之外, 如果您使用的 G++ 在版本 6 以上的話,G++ 還提供一些編譯警告的選項, 讓它在發現你一不小心使用這些功能的時候給你一些警告。 請加入下列編譯選項以開啟這些警告訊息:

  • -Wtemplates: 對模版的使用發出警告。
  • -Wnamespaces: 對名稱空間的使用發出警告。
  • -Wmultiple-inheritance: 對多重繼承的使用發出警告。
  • -Wvirtual-inheritance: 對虛擬繼承的使用發出警告。

移除 C++ 程式庫

想要程式不連結 C++ 相關程式庫有一個很簡單的做法:把所使用的連結器從 G++ 改成 GCC 即可!

G++ 和 GCC 是一樣的,你甚至可以拿 GCC 來編譯 C++ 程式碼, G++ 比起 GCC 的不同就是會主動多連結一些 C++ 基礎程式庫, 所以你其實也可以顯式的加上 C++ 基礎程式庫的連結選項來使用 GCC 編譯與連結 C++ 程式, 例如:

gcc main.cpp -lstdc++

這個例子只是在說明,GCC 和 G++ 在經過一些選項配置之後完全可以互換使用。 因此最終,使用 GCC 來連結程式就可以剔除與 C++ 程式庫的連結。

解決缺失元件問題

當移除了 C++ 程式庫後,可能會讓我們遭遇不少「undefined reference」的問題, 這些問題大約可區別為兩種類型: 第一種狀況是可能你的程式碼、或你所使用的第三方程式庫呼叫了一些需要 C++ 程式庫支援的功能, 比方說 cout。 對於這種錯誤的處理方式就是拿掉或改寫這些程式碼,畢竟我們的本意就是要剪裁一些 C++ 的功能是吧? 如果是第三方程式庫的話,那可能就表示這個程式庫不適合嵌入式執行環境,請換掉它吧。

至於第二種狀況則是 C++ 程式庫內真的包含一些需要且必須的、非常基本的功能, 如果我們真的要去避免使用這些功能的話,可能會失去我們當初想要用上 C++ 的初衷。 這種情況下就需由我們自行實做相關的功能來補上, 也許我們把它包裝成一個靜態程式庫,然後給它命名為 libMyTinyStdCpp.a

以下列舉這些可能會遇到的錯誤,並說明需要由我們自行實現的功能:

  • undefined reference to “operator new(size_t) or
    undefined reference to “operator delete(void*)

    這個錯誤表示你的程式碼裡使用了 newdelete 來建立物件,而連結器找不到對應的函式 (C++ 程式庫被我們取消了,還記得嗎?)。 捨棄 new/delete 不用而改用 malloc/free 不能解決我們的問題, 因為它們不會去執行物件的建構子與解構子; 然而類別自動建構解構也是我們之所以想要用上 C++ 的一大原因, 若捨棄 new/delete 不用則我們勢必還要為物件的建構解構花費更大的力氣, 因此迴避這兩個運算子的做法並不實際。

    為此,我們必須要自己提供 newdelete 運算子的函式實做, 最簡單的方法就是製做一個轉呼叫 malloc/free 的函式:

    void* operator new(size_t n)
    {
        void *p = malloc(n);
    
        /*
         * 這裡記得檢查 p 為 NULL 的情況並做出處理,
         * 由於通常例外處理機制也被我們停用了,因此不適合拋出例外,
         * 建議可直接回報錯誤並終止程式。
         */
        if( !p ) ABORT_AND_REPORT_ERROR();
    
        return p;
    }
    
    void operator delete(void *p)
    {
        free(p);
    }
    

    上面這個範例直接簡單的為所有記憶體的分配釋放需求轉呼叫 C 的對應函式, 而若你基於任何目的需要自行管理記憶體,比方說想要控制記憶體碎片的發展, 則可以把這兩個函式替代為你自己的實做版本。

  • undefined reference to “__cxa_pure_virtual

    這個錯誤表示你的程式裡面有使用到純虛函式。 在執行時期,當遇到呼叫一個未存在實做的虛擬函式時, __cxa_pure_virtual 便會被呼叫。 由於我們想要導入 C++ 應該也是看上了他的多型特色, 因此迴避純虛函式的做法顯然也不實際。

    由於編譯器在編譯時期就會阻擋想要建構未實做完成的類別實體的程式碼通過編譯, 因此 __cxa_pure_virtual 這個檢查函式其實只有在非常不尋常的時候才會被執行, 那麼我們就可以很簡單的提供一個實做版本,直接列印錯誤訊息並終止程式, 好讓開發人員進行除錯:

    extern "C" void __cxa_pure_virtual()
    {
        ABORE_AND_REPORT_ERROR();
    }
    

基本就這樣,如果還有其它的缺失的話,等我發現再補上。