2009年5月11日 星期一

淺談C++例外處理 (前篇)

例外處理是一種程式語言的機制,他是用來偵測和處理一些執行時期的錯誤或異常狀況(Runtime error/exception) ,這也意味著如果程式語言想內建這個機制的話,就必須要有執行環境的支援,直覺上也代表著那段程式碼將會跑得比較慢。然而,在某大部份的狀況,執行時期的異常安全(exception safety)是非常重要的,付出那些代價相對來說是非常值得的。對一般的使用者而言,出現Segmentation fault或跳出一個記憶體無法存取的視窗都不太能被接受了,更不用說那些需要高度安全的程式,就好比汽車的剎車系統或核電廠的控制程式,出現執行時期的異常是需要付出慘痛的代價的。

例外處理機制"內建"在程式語言當中最早是從IBM的PL/I( I是羅馬數字的1 , 1964 Appeared, 1976 ANSI, 1979 ISO) 開始的,後來也有一些物件導向的語言開始跟進,在某些作業系統中也有API來讓某些語言(比如說慣C最喜歡的C)能有例外處理的支援,也有某些語言使用了一些語法甜頭來加強他的功能性(如C#的using block),甚至在新興的腳本語言中都可以看到他們的蹤跡。然而,程式語言內建的例外處理機制其實還蠻多人討厭的,慣C的人也覺得這個機制不太直覺。猶有甚者,認為例外處理機制根本就是無形的goto語法,不僅讓工程師們永遠不知道他們是從哪裡開始變換程式流程的,而且他們覺得使用例外處理機制並不會讓問題變得更簡單,更不用說會破壞程式的結構性了。

程式語言的例外處理機制之所以會引發很多的爭議是在於某些例外處理的情況是可以靠check return code來早期發現的,慣C的人(應該)也已經很習慣使用這種例外處理的機制了(不過有時候會夾帶很多macro),而程式語言所提供的例外處理機制要一般化且具有通透性,在黑箱作業的結果就是很多狀況是會破壞程式的控制流程,畢竟程式如何從例外發生點跑到catch block的細節已由編譯器代為處理了。加上為了能在很多特殊的狀況中都能使用,所以他們的實現會比較困難,而且效能也不是相當好。不過程式語言的例外處理是靠執行環境來支援的,這意味著只有他們可以成為最後一道防線,這是任何編譯時期的機制無法辦到的。

例外處理的語法在程式語言當中算是複雜的,也是比較少被使用的,乃至於少數工程師不是很有辦法完全駕馭,即使是有語法甜頭的支援,有時候也會有一些迷失。那讓我們回到主題來吧,C++的例外處理機制說複雜其實也蠻複雜的,其中一個複雜點在C++可以把物件放在自動變數內。自動變數的一個特性就是可以自動進行建構和解構。由於C++標準明確地定義了程式的control flow從throw(異常發生點) 到 exception handler 被執行前是必須解構區塊內所有已建構成功的自動變數。標準講的很簡單,但這其中卻隱藏了許多的陷阱,我們就來看個例子吧。

class testthrow {
public:
    testthrow() {
       test tx;
        cout<<"testthrow constructor"<<endl;
        throw;
    }
    ~testthrow(){cout<<"testthrow deconstructor"<<endl;}
};

int main() {
    try { testthrow tt; } catch(…) {}
    return 0;
}

這段程式碼大部分的人會覺得tx必須被解構(因為他已經被建構了),很可惜是不會的,即便main中使用try catch的區塊並攔截所有例外。原因出現在 rethrow 的語法當中,C++標準規定了throw保留字如沒有參數,那所表達的語意就是進行 rethrow 。然而,C++標準又規定了在沒有發生 exception 的狀況使用rethrow的話,程式是會被終結的。接下來我們來看第二個例子:

int main() {
    someclass c;
    throw ( std::runtime_error("error!!") );
}

在以上的程式碼中,假設c會建構成功的話,那c會被解構嗎?答案是不一定,基本上這段程式碼在C++的標準當中是沒有定義的。C++標準提到了自動變數要解構時機是在stack unwinding 的時候。然而,C++標準又提到,如果在程式裡沒有定義 exception handler 那無論有沒有做stack unwinding 程式都會被終結,依現行的C++編譯器 VC++ 和g++ 的 libstdc++v3 的實作,c是不會被解構的。

2009年5月1日 星期五

Signed不Signed就是問題所在

每個男人都想慣C,但是要在嵌入式系統這個領域當個慣C,其實也不是那麼容易的,君不見遠方蘋果上的銅板跟嵌入式系統一樣小,失之毫釐可是會差之千里的。Jserv前輩的我是軟體 -- 那些處理器教我的事,點出許多使用C語言進行跨平台開發時,很多狀況並不是cross compile下去就沒事了,號稱可以劈腿無數平台的慣C也是有很多平台的差異需要解決的。然而,姑且不論跨平台開發時,其實想要慣C也需要面對許多陷阱的,最簡單的陷阱像 0.1 * rate 會等於 1/10 * rate嗎? (答案是否定的),modifier的位置的差異性(最著名的是指標和const的位置),更不用說誇平台開發了。我們今天的主題就鎖定在型別轉換系統,看Signed不Signed是怎樣影響每個工程師的。

大家都知道想要慣C是必須要了解C的型態系統(tpye system)的,由於C是有形態系統的語言,所以兩個不同的型態要做運算的時候,有時候並不是說聲我們要相加囉(感謝Jserv前輩提供這個梗)就可以完成的,有必要時是必須作型態的轉換才能進行運算的。這部分的描述主要是在C99規格書(ISO/IEC 9899) 的6.3.1(Arithmetic operands)中。然而,其實規格書跟法律條文一樣都是咬文嚼字的頗複雜難懂的,我們今天就來看看其中的一些規則吧。講到變數的型別轉換,其實大家比較容易遇到的是小的型別要變大的型別,這部分在C語言中是隱性轉換,編譯器會自己把這個細節完成。基本上這個轉換會依造以下的規則來進行:

如果一個整數型態要轉換成另一個整數型態時(_Bool除外),如果新型態能表式原本的值,那值應該維持不變。

這個規則點出一個很重要的事實,就是如果一個整數型態要被放到一個比他大且能表示他的型態中,一般處理器的作法是會對變數進行Signed extend或 Zero extend,其中最常被誤會的轉換莫過於一個變數char c; 使用 (unsigned int) c;來強迫進行型態轉換,那會做Signed extend還是Zero extend?答案是都有可能,如果toolchain的char預設是使用signed type,這個運算式會做 Signed extend,如果char預設是使用unsigned type (如ARM toolchain)那會做 Zero extend。

另外一個重要的算數型別轉換規則在C99規格書(ISO/IEC 9899)的 6.3.1.8 Usual arithmetic conversions中,裡面講的是兩個不同型態的變數要做運算時,那應該轉換到哪個型態來進行運算,裡頭講的也蠻複雜的,用簡單的話來說就是以下的規則:

  1. integer conversion rank:  Rank由高到低依序是 long long > long > int > short > signed char 而 char =  signed char = unsigned char
  2. 兩個運算元都是Signed或Unsigned時,就轉成高Rank的型別
  3. 兩個運算元一個是unsigned (A)一個是signed(B)時
    • Rank(A) >= Rank(B) B轉成A的型態
    • Rank(A) < Rank(B)
      • B型態如果能表示A的值,A轉成B的型態
      • B型態如果不能表示A的值,B轉成A的型態

以上的規則最容易出狀況的點在於64位元處理器LP64模式的unsigned int和long ,由於32位元處理器的long是不能表示unsigned int的值的,所以會根據上述的最後一個規則把long轉型成 unsigned int來做運算,而LP64模式卻會使用倒數第二個規則,把unsigned int轉成long來進行運算,所以當unsigned int i=5566; long = -1時,兩個變數在32位元和64位元LP64中,兩個人的大小關係就會產生了奇妙的變化了。

 

最後我們來談談一個型別系統的除錯工具,由於C語言當中埋藏了許多隱性轉換,這部分是由編譯器來完成的,所以我們可以利用編譯器的除錯資訊來看看編譯器到底幫了我們做些甚麼轉換。GCC可以使用-dr這個參數來dump RTL(GCC的中間表式法),雖然那個檔案對很多人來說是天書,但也並非讓人無法捉摸,以下便是一個簡單的範例。透過RTL我們可以知道,在x86中char是signed type,而c轉成p時會有一個隱性轉換而且是使用sign extend來進行轉換。

C Code:

main()
{
                char c =0xea;
                unsigned int p = c;
}

RTL 片段

;; c = -22
(insn 8 6 0 (set (mem/c/i:QI (plus:SI (reg/f:SI 54 virtual-stack-vars)
                (const_int -1 [0xffffffffffffffff])) [0 c+0 S1 A8])
        (const_int -22 [0xffffffffffffffea])) -1 (nil)
    (nil))

;; p = (unsigned int) c
(insn 10 8 11 (set (reg:SI 60)
        (sign_extend:SI (mem/c/i:QI (plus:SI (reg/f:SI 54 virtual-stack-vars)
                    (const_int -1 [0xffffffffffffffff])) [0 c+0 S1 A8]))) -1 (nil)
    (nil))