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 又被稱為是ASCIIZnull-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) 新歌的梗來回答說 :"原諒我就是這樣的字串"