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))

1 則留言: