2009年11月2日 星期一

Signed不Signed還是問題所在 之 C# Remix 版

每個男人都想當慣C,可是在2009年的今天也有不少男人不想當慣C,想當慣西夏(C#)也說不定,跟C和C++比起來,C#是個純物件導向的程式語言,在Everything is object的設計前提上,這些Signed 不Signed 的問題當然使用者不是很想去沾惹,總是希望Framework能把事情處理的很好,這樣王子和公主就能過這幸福快樂的日子了。在這個那麼美好的世界當中,會不會有那些令人惱怒的轉換規則呢?在前篇中提到的C 語言中Signed Type 跟 Unsigned Type 容易造成的誤會和運算轉換的規則,在現行的純物件導向語言當中,轉換的規則、Signed Type 跟 Unsigned Type 之間的交互作用,又全然是另外一個故事了,本篇以C#為例來探討一下所謂的新世紀Signed 問題。

當然這整件事情的緣起,是在某朋友的部落閣當中,看到了一個程式。這個程式這這樣寫的。

unsigned int i=1;
int j=-1;
if(i>j) printf("i>j");
else printf(“j>i");

慣C的人看到一定馬上就看出問題所在,大聲說出:「閃開,讓專業的來」,或是「放開那女孩」,然後新警察就會出來說,依造前篇的規則,這個轉換會根據C99 Spec中的6.3.1.8的Rule 3(a),得到 "j>i"這個慣C的人看起來很自我感覺良好,可是大家感覺卻不太好的結果。有沒有念過國小的人都知道1是比-1來得大,不過"數學是數學","數字要在程式中表現出來"就是會有這樣的結果,數值的表示本來就是有極限,所以才需要訂規則消除這種模糊的狀況,這個應該是程式工程師應該小心的,這是他們的責任。不過把這個看起來詭異的程式拿去C語言的後繼者,也是2000之後才出現的新世紀語言C#去做驗證,卻得到了看起來正常,卻沒有那麼正常的答案"i>j" 。

看起來正常是法理上是"i>j" 應該,"j>i"會是悲哀,可是如果仔細去想的話,這樣的結果又會令人覺得奇怪,畢竟在C#中什麼都是物件,那個運算子'=='其實會對應到某物件的成員函式,那現在 i 跟 j 的型態並不一樣,那到底是哪個比較函式會被叫起來呢?不過對程式語言稍有理解的人其實會了解,這樣的情境下你的編譯器勢必會幫你動一些手腳,如果你不懂你的編譯器幫你做了些什麼,那可能會是個災難。一個看起來合理的想法可是卻是錯誤的想法就是編譯器會幫你把unsigned int i (C# 中 uint i) 會被轉成 Signed Type,讓這個轉換看起來會得到這樣的結果,可是C#是個號稱自己是個強型態的語言,除了隱性轉換之外(所謂的隱性轉換就是設計者覺得合理的轉換(笑)),使用者是必須在程式碼顯性的說出:「我就是要這個轉換。」然而unsigned int i 轉 int 是會丟失資料的,這樣一個轉換的確不該是一個隱性轉換。會設計成這樣就是不希望使用者管這問題,如果是顯性轉換的話,其實使用者是在意了。

另外一個合理的實作是把每個基本形態的比較運算子成員函式做不同型態的比較,這樣的話unsigned int 就能正確的跟int 做相比了,不過這樣的排列組合會挺多的,所以並不是一個很好的解法。另外一個合理的轉換方式就是把unsigned int i 跟 int j 都轉成64bit int 再來進行比較,這樣就可以得到合理的結果了,事實上C#的Language Specification中是使用這種解法的,所以C#會得到"i>j"也非常正常。其實就如同其他的語言,不同型態的運算難免不了要做 promotion,只是這樣的作法就見仁見智了。最後,這部份的轉換是定義在C# Spec中的 7.2.6 中,其中的第二節Binary numeric promotions就是在這個範例程式碼當中,C#編譯器所套用的轉換規則。

2009年10月26日 星期一

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

前言: 經過了漫長的混戰當中,終於把GNU G++例外處理的機制實作在某32位元CPU上了,為了避免富姦化,個人考慮多發表一些另類的文章才是阿!!

前文淺談了C++例外處理的機制,根據前篇的內容,我們可以初略的了解到例外處理機制在語言中的定位和C++例外處理機制中容易產生的迷失。然而,有志者總是喜歡更"深入"的去了解更底層的行為,看似平凡的東西有時候會使人驚艷,而像C++ 例外處理機制這個從Spec 看來就如此複雜機制該如何來實作呢?相信大家心中都有一樣的疑惑。編譯器是個複雜的東西,然而在GNU Toolchain還沒問世之前,要講例外處理的實作通常只能從產生出來的代碼來探討,然而一個開放原始碼的編譯器卻能讓程式工作者能以"編譯器"的角度出發,深入研究C++的例外處理機制,這也是開放原始碼偉大的地方。本文就以開放原始碼的編譯器 GNU G++為出發點,來探討C++例外處理的實作。

註: 本文探討的原始碼以GCC 4.4.2 為主

GNU Toolchain支援的C++例外處理模型的實作有兩種,與其說例外處理的實作有兩種,其實正確的說法是Stack Unwinder的實作方式有兩種。第一種叫Set Jump Long Jump Exception Handling (簡稱SJLJ) 另一種叫Dawrf 2 Stack Unwinder Exception Handling。如果要用簡單的幾句話來介紹一下兩者皆的差別,其實就是Set Jump Long Jump Exception Handling 這種方式實作起來比較簡單,可攜性也較高,可是效能和產生的代碼大小都差強人意。 而 Dawrf 2 Stack Unwinder是利用Table Driven的方式來進行例外處理,效能和速度都比前者好,想當然是可攜性比較差,而且需要其他二元工具組一些額外的支援,實作上也複雜許多了。本篇的探討會以Set Jump Long Jump Exception Handling為主,而下一篇再探討Dawrf 2 Stack Unwinder。

Set Jump Long Jump Exception 的原理很簡單,顧名思義就是用到setjmp和longjmp這兩個函式,這兩個函式是C標準函式庫的一員,原本是用來處理non-local goto 的堆疊平衡。所以用他們來處理C++的例外也是蠻單純的。就學理上來說,就是進入到需要被偵測例外的區域就呼叫setjmp來保持執行狀態,在丟出例外時使用longjmp來復原原本的執行狀態。然而事實上這部份的處理確沒那麼簡單,而且比想像中的複雜很多,因為C++程式語言規定了離開例外處理區塊的時候必須要正確的解構auto object,這造成許多麻煩。一個簡單的方法就是把例外處理區塊製造出來需要解構的物件都塞到一個list內,如果例外發生就能依序解構他們。如此一來想當然效能是非常差的,最致命的是,不論例外有沒有發生,這些保存的動作都是必須的。會採取這種實作有許多理由,除了簡單之外,可攜性也是當初在實作的一個重大考量,因為他在那些把C++原始碼轉成C原始碼的早期C++編譯器中也能夠運行的很好,

回到實作的層面,C++程式語言中用來作例外處理主要的關鍵字有三個,分別是try, catch, 和throw。如果大家用C++程式語言的角度來看,他們的作用我就不贅述,相信C++程式語言的書籍中都有介紹。如果想從標準文件的角度對這三個關鍵字的作用和描述更深入的研究,他們在ISO C++文件的第15章。然而,如果大家用的是更低階的角度來看例外處理,就會發現其實組合語言是沒辦法對這個行為進行原生的支援的。也就是說編譯器其實會根據那些關鍵字,在他們的程式區塊(Code Block)中產生特定的代碼和函式呼叫。關於這個假設,我們可以用GNU G++產生組合語言的功能(-S)來驗證。經過觀察我們就可以輕鬆的發現GNU G++會分別在這三種區塊內加入函式呼叫的代碼。稍微對G++有些了解的讀者會知道其實這些函式名稱其實就是GNU G++的"約定",也就是在運行時期函式庫中必須要有這些函式,才有辦法正確的進行例外處理。以下為例外處理模型設定成sjlj-exception 的 G++ 編譯器對特定的例外處理關鍵字產生的函式。

try-block
    _Unwind_SjLj_Register
    _Unwind_SjLj_Unregister
    _Unwind_SjLj_Resume
handler: (catch block)
    __cxa_begin_catch
    __cxa_end_catch
throw-expesssion
    __cxa_allocate_exception
    __cxa_throw

註: 以上的結果必須要將編譯器都設定成 “-wtih-sjljexception=yes “ 才有辦法看到一樣的結果,其中要注意的是大部分主流處理器的編譯器和主流Linux發行版預設的編譯器,預設都是使用Dawrf 2 Exception Handling,所以無法看到相同的結果。

_Unwind_SjLj_Register 和_Unwind_SjLj_Unregister分別用來加入或移除一個到Function_Context到unwinding-chain當中。_Unwind_SjLj_Resume是用來恢復例外的傳遞(也就是說用來做cleanup),前兩個函式的原始碼在<gcc>/gcc/unwind-sjlj.c 中,後面一個在<gcc>/gcc/unwind.inc中。如果大家有去觀看看原始碼會發現其實_Unwind_SjLj_Resume有一個兄弟函式叫_Unwind_SjLj_Resume_or_Rethrow,他是用來處理FORCE_UNWIND例外和實作rethrow的功能__cxa_begin_catch 和__cxa_end_catch就沒那麼複雜了,他們主要用來處理C++例外處理語意中一些housekeeping的動作,他們在libstdc++中libspu++的 eh_catch.cc 內。值得一提的是,C++運行函式庫最容易被問題的問題是有沒有支援thread-safe,然而G++在例外處理的實作是有考慮到thread-safe的。

throw的處理就複雜很多了,因為throw的處理牽扯到了執行狀態的恢復、例外處理函式的尋找和程式碼控制權的轉移。以下為__cxa_throw的原始碼(在libstdc++/libspu++/eh_throw.cc當中)

extern "C" void
__cxxabiv1::__cxa_throw (void *obj, std::type_info *tinfo, void (*dest) (void *))
{
  // Definitely a primary.
  __cxa_refcounted_exception *header
    = __get_refcounted_exception_header_from_obj (obj);
  header->referenceCount = 1;
  header->exc.exceptionType = tinfo;

… 中間省略 … 

  _Unwind_SjLj_RaiseException (&header->
exc.unwindHeader);
 

  __cxa_begin_catch (&header->exc.unwindHeader);
  std::terminate ();
}

看過原始碼後我們會發現,其實__cxa_throw並不複雜,比較值得注意的就是藍色的部份,就是如果unwind失敗的時候呼叫std::terminate離開,這也說明了大部分的工作就落是_Unwind_SjLj_RaiseException當中,而且這個函式如果執行成功的話是不會返回到__cxa_throw的。

_Unwind_SjLj_RaiseException的部份就是分成兩個階段,第一個階段是尋找例外處理函式的部份,這部份的程式會翻閱frame的資訊來尋找是否有例外處理函式被註冊,如果這部份有找到例外處理函式就會進入第二階段,反之就會返回__cxa_throw,這會造成程式被結束。第二階段會進行恢復到有例外處理函式的那個frame的執行狀態,並且取出他的Function_Context並且longjmp回到例外處理函式去。如果某一個函式有多個例外處理函式,那編譯器會在那個函式產生一段充滿跳躍指令的程式碼叫landing pad,而_Unwind_SjLj_RaiseException的會先longjmp回到landing pad後再判斷要跳躍到哪個例外處理函式中。

2009年8月21日 星期五

我宅 故我在 ~ 餘玹杯心得 [COSCUP 2009]

今年是小弟一次參加這個台灣開源/自由軟體界的大活動,也因為今年有不少中意的議程,所以小弟很注意報名資訊,也很幸運的也有報到名,不然4個小時內把450個名額搶購一空的熱浪也太驚人了。由於在活動前一週台灣發生了一件大事,而小弟的老家正好在災區近郊,也蒙受了一點損失,然而比起其他沿海地區算是非常微小了。小的雖然不是政治人物,且能先天下之憂而憂,後天下之樂而樂,曾經那麼熟悉的地方遭受到如此嚴重的天災地變,令人噓唏。也因為心情一直難以平復,就這樣渾渾噩噩過了一個禮拜馬上就到了需要上台北的周末了。

由於小弟是早上才從新竹趕往台北,加上前晚沒有很早睡(迷之音: 誰叫你要研究HOI3研究到半夜),又加上路途客運塞車,炎熱的天氣,漫長的椰林道,所以到會場的時候已經午時了,也漏掉了一些議程。不過我第一天最主要的目標是聽人稱人帥真好的Jserv前輩的場次Linux Virtualization Goes Mobile,所以我就在外面廠商攤位抽獎聊天順便看美女,正好呼應Jserv前輩那場的主題:「我宅、 我色、我舒服」,不過話說回來,雖然Jserv那場充滿的搞笑的橋段,就Introdutuion 來說是一個的好的演說,不過我猜大部分還是對征服和舒服比較有印象XD。

第二天的演講對我來說就蠻有吸引力的,早上Clutter - The Animation UI Framework on Mobile Device和開放源碼軟體授權分析工具就還蠻精采的。Happy Build with OpenEmbedded這個主題就比較難發揮了,講者也試圖把他講的精采,開發嵌入式Linux軟體的就會知道OE的好XD。下午的講題就比較硬了,由於個人背景的關係,廖博士那場"Smaller and Faster Android: Optimization and Toolchain Perspective"讓我感覺非常"舒服",如果Coscup有Best Section Award,我想我會投給他一票。

總結來說,這個活動辦的很用心,也相當的成功,小小的美中不足點就是以三十分鐘來說,對每個Section似乎是不太夠,其實只要再加個10分鐘,我想大部分的講者可以講得更從容,QA時間的互動也會更加精采,不過我猜這次熱烈的程度,應該可以預期明年會辦的更好,說不定有機會進軍國際會議廳,成為台灣開源/自由軟體界的典範。

#因為大鳥的建議 改一下標題 for SEO

2009年7月26日 星期日

這就是共和

前陣子因為討論雲林國光石化園區的關係,我提出了一個議題「什麼叫共和?」基本上只要是公民就會對政治就會有所關心,所以對於這個已經實行了那麼久的政治制度其實也不陌生。雖然得到的答覆蠻多的,可是錯誤的答覆卻不少,所以就讓我們來討論一下這個影響著現代人類社會非常深遠的政治制度吧。由於小弟是在現代社會出生的,而且我年紀並不大(??),在懂事的時候台灣社會已經離開獨裁統治(其實獨裁的情狀在"誓死"捍衛共和的傳統共和國也會發生),並步入了比較正式的民主共和時代,在這邊我並不打算對台灣政治作深入探討,只是想說明在現在出生的人對共和這兩個字並沒有比較特殊的感情,當然也不會有什麼需要誓死捍衛共和的情操。
台灣的教育對理工科是比較偏重的,自然組學生上高中之後基本上是不看歷史的,所以對最出名的古老共和國"羅馬共和國"了解並不多,僅知道有元老院這東西,而且後來就轉成帝制,史稱大秦,更不用講會去了解裡面的政治制度了。"神鬼戰士"(對岸翻譯成角鬥士)這部電影,讓我對羅馬的帝制和共和產生興趣,"恢復共和"這件事情竟然是如此的重要,讓這個我只理解中國歷史,認為一有機會大家都會想逐鹿中原,問鼎天下的人是完全想不透。後來我因為某些原因開始去研究共和國後期的歷史後,看到了一些人在網路上發表的心得說:"羅馬沒有完全共和過,也沒有完全帝制過,而在凱薩稱帝後再也沒有回到共和"(當然,這三個描述都有錯),我的心中不禁的重新思考了什麼叫共和?
共和制的英文是"Republic" ,起源於拉丁文的"Res publica"(大概的意思就是公共的事務)。而中文把英文的Republic翻譯成共和的原因則是因為在"竹書紀年"當中,周厲王因國人暴動逃離鎬京到到宣王即位這段期間稱為共和(行天子事,號曰『共和元年』,這部份史記周本紀記載的是周公、召公兩相行政),所以後來日本西化的時候,就引用這個典故,把Republic稱為共和。 就我們所知,農業的起源在西亞,人類因為農業的剩餘糧食,就可以供養一些不需要從事生產的技能專家、統治者和軍隊,統治者接而利用資源進行更大的水力建設來供養更多人口,部落和國家由此產生。上古時期的國家以君主制為主,但已經有一些國家採取共和制(如許多希臘城邦),可是真正把共和制發揚光大的卻是羅馬共和國。
羅馬是個非常神奇的城邦共和國,任何一個能維持霸權的國家一定有許多勢均力敵的對手,羅馬就是在這樣的環境下從一個小小的城邦成為一個環繞地中海的大國(他們稱為我們的內海),在這我就不贅述羅馬的歷史,來探討一下羅馬的政治制度。羅馬人自從革命趕走國王之後,就開始了漫長的共和制度,基本上羅馬走的是寡頭共和這個路線,而且政治上是非常保守的。羅馬最高的行政官叫執政官(Consul),以現在的國家來說相當於總統,為了避免獨裁者的出現,執政官有"兩名"(其中一個要來自平民),並且只有一年的任期。不過一向遵循共和傳統的羅馬在敵人兵臨城下時為了避免緊急危難,可由執政官選出一個人來獨裁,稱為獨裁官(Dictator)。會有這樣的職位存在,可以說明其實羅馬人很明確的知道"獨裁"是比較有效率的。(值得一提的是著名的獨裁官凱薩,跟一般人眼中專政獨裁的形象不同,凱薩其實並沒有濫用這個職位,第一次當獨裁官僅十一天而且也沒有福氣利用到終身獨裁官這個職位)
大家都知道共和國的理想是很遠大的,目的就是要建立一個屬於人民,不屬於個人的國家。但兩千年前的古老共和國告訴我們,縱使今天有了全民參政權,走的是民主共和,但是仔細琢磨後會發現其實今天的政治生態跟兩千多年前其實是相去不遠的,在羅馬共和時期的政客都會喊愛羅馬、愛人民,但大家也都知道他們骨子裡愛的就是錢,愛的是自己。所謂的共和制政府其實只代表著政經高層的意志,並不是大多數平民的意志,任何政客都可以利用施以小惠的方式來換取權利,平民也殊不知這些小惠也是從人民身上搜刮得來的,人民為了過生活,就只好繼續用選票選出吸自己血的政客,其實這就是共和。這一點在媒體如此發達,民智如此開化的今天也無法避免的,也就是說,即使的是2009年的今天,活在共和制底下的人民如果沒有一定的政經關係其實是監督不了政府的。什麼是共和?說穿了就是政經高層的利益。

2009年6月7日 星期日

原諒我就是這樣的字串

每個嵌入式系統工程師都想要慣C,可是我們都知道想慣C而不出包也是有難度的,原因是出在於C語言本身存在了許多陷阱,使得就算慣C把所以隱密的資料都砍掉了,貿然去修電腦還是有危險性的。字串,在電腦科學中就是一連串的符號,由於字串跟人類的生活很接近,程式又是用來解決人的問題,所以工程師們在寫程式的時候,也常常面對字串。每種語言對字串的實作方式都不太一樣,即使在同一個語言,每個函式庫也會試圖提供了不一樣實作的字串處理函式庫,這些函式庫大部分都很基本,所以是讓一些初學程式的人練功的好地方。而想慣C的工程師要面對的,就是一個叫c string (或稱為c-style string) 這樣的字串。

然而,對慣C的人來說 "C-String" 不只是無帶內褲,而是一個美麗與哀愁。對C語言來說,字串的表達方式 ─ 也就是所謂的 c string (C++族群稱之為 c-style string ) ,其實就只是一個以'\0' (null character) 當結尾 (null terminated) 的字元(或是寬字元)陣列,所以 c string 又被稱為是ASCIIZ 或 null-terminated string。然而,由於C語言的陣列(當然不包含C99的變動大小陣列)大小必須是固定的,這個特性加上0結尾就造成了一定的麻煩。對C的初學者來說,少算了0結尾或忽略忘記了0結尾就會變成了一個還蠻常見的錯誤,也誤了不少青春,更不用說固定大小的緩衝區容易造成安全漏洞。

為了使用上方便,大部分的語言會提供字串定字(string literals, C99, 6.4.5) ,讓工程師可以在程式碼內表示一些字串。由於C語言是個系統程式語言,所以大家對這些東西會身在何處是有高度興趣的。而 string literals 到底會身在何處,是跟編譯器的實作有關的。C語言規格中定義了他們會被規定分配在"static storage"當中(C99 spec, 6.4.5),並且說明了如果程式嘗試修改string literals的內容,將會造成未定義行為。以GCC的 ELF Target 來說,是將string literals分配在read-only data section中(當然包含了0結尾)。由於C語言提供了一些方便的語法甜頭來初始化陣列,這使得 char* p=”hello world”和char p[] = “hello world” 寫法差不多,可是私底下卻是大不同。

以指標的寫法 char *p 來說,代表的是p將會是指向static storage的一個指標。這樣的寫法是會有一些小問題的,因為嘗試修改string literals的內容,將會造成未定義行為,而這樣的寫法編譯器並不會對存取p的元素提出警告。比較值得注意的是陣列的寫法,依規定 string literals 是必須放在 "static storage"中的,而 char p[] 的語意則表示要把資料分配在Stack內,所以這會造成編譯器隱性的產生執行期把string literals從static storage複製到stack中的代碼,雖然字串本身不是放在Stack內,但char p[]卻是分配在Stack內,這也造成return p是未定義行為。

光是表示字串有時候是不太夠的,所以必須要在字串上做一些運算,而C語言的規格書中定義了標準函式庫必須提供一些字串處理的函式 ( 這部分定義在C99 spec, 7.21 String handling <string.h>中),來幫助工程師避免製造重複的輪子。接下來問題來了,這些字串處理函式的原型大部分是使用char *或void * 的型別來當參數,那這些參數到底能不能接受null pointer呢?如果不能,那函式要負責檢查嗎?null pointer算是一個字串嗎 ?對一個null pointer使用這些函式(如strlen)會是怎樣的結果?規格書中有定義這些東西嗎?

答案是 string handling function *不能接受* null pointer當參數,因為絕大部分的狀況null pointer並不是一個字串(可以思考一下為什麼),所以對null pointer使用strlen絕大部分是會造成未定義行為的,而大部分的實作也不會在函式庫內做null pointer檢查(有些函式庫的實作會使用編譯器的延伸語法來提出警告),所以代表工程師是要把null pointer檢查的責任一肩扛下的。大部分的人會把這部分的現象推給編譯器實作的差異,不過到GCC的論壇問這個問題,得到可能會是"由於GCC沒有提供標準函式庫所以這跟GCC無關(笑)"的答案。然而,規格書就像法律條文,有時候是需要解釋的,個人認為其實C99規格書隱晦的提到了這個問題,所以以上的現象都是合理的。原因出在於C99 Spec 7.21.1的第一點中提到了:

Various methods are used for determining the lengths of the arrays, but in all cases a char * or void * argument points to the initial (lowest addressed) character of the
array. If an array is accessed beyond the end of an object, the behavior is undefined.

null pointer顯然在*絕大部分*的狀況都不符合這個規定,既然不符合規定,函式庫的實作顯然也不需要浪費心力去做檢查。更不用說想要在一些物件導向的字串函式庫中使用 string物件的null pointer 來做字串運算,我想這一定是瘋了(題外話,以大部分物件導向語言使用字串的情形,null時要做的事情其實是跟empty時是差不多的,所以.NET的字串函式庫後來提供了IsNullorEmpty的類別方法,讓工程師不用每次檢查Empty之前還要check 是不是null )。當工程師想慣C而被這些字串的問題所擾,仰天長嘯問 c string 為什麼 要用這個方法來實作時,可能的情形就是 c string 使用了 戴佩妮(Penny) 新歌的梗來回答說 :"原諒我就是這樣的字串"

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