--- tags: software --- # 約爾談軟體 - 1.2 回到基本(Back to Basic)[^9] https://www.csie.ntu.edu.tw/~p92005/Joel/x.html ## 約爾是誰 約爾談軟體本書的作者-約爾[^1] 約爾是知名程式設計問答平台 Stack Overflow 和看板軟體 Trello 的共同創辦人,也曾參與 Microsoft 的 Excel 應用開發[^2][^3],還有製做過用來追蹤 Bug 的一個軟體 FogBugz[^4]。 在最新的 blog 中,約爾提到目前自己是知識共享平台HASH的共同創辦人[^5] ## 約爾談軟體-大綱 約爾趣談軟體,主要是談論軟體專案管理以及人才培訓與軟體創業經營的議題 最有名的是約爾測試(Joel Test),用來衡量軟體團隊的品質的一個指標 ## 主題 -- 1.2 回到基本(Back to Basic)[^9] 本次將會以章節 1.2 回到基本(Back to Basic) 作為討論 回歸到基礎面,從最基本的程式來看軟體 ![](https://hackmd.io/_uploads/BJjTl0EV2.png) 有時候架構上的錯誤來自於基礎的不夠理解 ![](https://hackmd.io/_uploads/rkJc40V4n.png) 你可能建了一座雄偉的宮殿,可是地基卻是一塌糊塗。 本來應該用好的水泥磚,結果卻用了碎石頭。所以宮殿看起來雖然華麗, 可是浴缸卻時常移位,而你根本不知道怎麼回事。 ### C 字串 ![](https://hackmd.io/_uploads/HyJF8A4N3.png) C 字串有以下特性: 1. 必須整個字串走一遍找到結尾的null字元,才能知道字串在哪裡結束(也就是說字串的長度)。 2. 字串裡不能有任何零。所以你不能用C字串來儲存JPEG圖片之類的二進位大物件 為什麼C字串要這樣運作呢? 因為發明UNIX和C語言時用的PDP-7微處理器有一種ASCIZ字串型別。 ASCIZ意思是「用Z(零)結尾的ASCII。 ### 以 strcat 作為範例,說明 ASCIZ 不是個好設計的部份 假設實作把 C 字串 src 加到 C 字串 dest 後面的一個程式如下 ```clike= void strcat( char* dest, char* src ) { while (*dest) dest++; while (*dest++ = *src++); } ``` 邏輯如下 1. 先找到 dest 字串尾的位置 2. 然後把 src 字串中所有字元加入 dest ![](https://hackmd.io/_uploads/HyZLCA4Nn.png) ### 思考一下 以下範例 有什麼問題 舉例來說。如果你有一大堆名字,全部都要附加到一個大字串後面: ```clike= char bigString[1000]; /* 我永遠不知道要配多少記憶體... */ bigString[0] = '\0'; strcat(bigString,"John, "); strcat(bigString,"Paul, "); strcat(bigString,"George, "); strcat(bigString,"Joel "); ``` ### 油漆工 YxTxn 演算法 YxTxn 得到一個在路上塗油漆的工作,他要漆在路中間的間斷分隔線。 第一天他拿了一罐油漆去漆好了300碼的路。 「做得真好!」他的老闆說「你手腳真快啊!」然後就給他一個銅板。 第二天 YxTxn 只漆了150碼。「這樣啊,沒有昨天好,不過也還是很快。150碼也很了不起。」也給他一個銅板。 第三天 YxTxn 只漆了30碼。「只有30碼而已!」老闆就哇哇大叫了。「這實在是無法接受!第一天你漆了十倍的長度耶! 究竟怎麼回事啊?」 「我也沒辦法啊,」YxTxn 說「我每隔一天就離油漆罐愈來愈遠啊! ![](https://hackmd.io/_uploads/rJPp_lrEn.png) ### 修正過後的 strcat -> mystrcat ```clike= char* mystrcat( char* dest, char* src ) { while (*dest) dest++; while (*dest++ = *src++); return --dest; } ``` 這個修正多做了一件事情 把連接完當下的字串尾傳回去 這樣就可以再下次連接字串時,不用從頭找起 使用範例如下: ```clike= char bigString[1000]; /* 我永遠不知道要配多少記憶體... */ char *p = bigString; bigString[0] = '\0'; p = mystrcat(p,"John, "); p = mystrcat(p,"Paul, "); p = mystrcat(p,"George, "); p = mystrcat(p,"Joel "); ``` ### Pascal字串 ![](https://hackmd.io/_uploads/HJuDy1SE2.png) 舊的麥金塔作業系統全都是用Pascal字串。很多其他平台的C程式師也為了速度而用Pascal字串。Excel內部就是用Pascal字串, 所以Excel裡很多地方的字串長度最多只能到255個位元,這也是Excel飛快無比的原因之一。 因為長度是存在一個字元:字元是由2個Byte ,一個 Byte是8個 bit。一個字元是16 bit 最多 2^16 - 1= 255。 ### 位元配置 之前省略了一個很重要的問題。記得這一行程式嗎? ```clike= char bigString[1000]; /* 我永遠不知道要配多少記憶體... */ ``` 正確的寫法:找出需要的位元組數然後配置正確的記憶體量。 #### 為何重要(緩衝區溢出漏洞)[^8] 如果不做的話,某個聰明的駭客讀我的程式時會注意到,我只配置了1000個位元組並期望這數字足夠。 他們會找出某些聰明的方法,讓我把一個長1100位元組的字串strcat到原本1000位元組的記憶體中。 然後就會覆蓋堆疊框並改變返回位址,於是當這個函數返回時就會執行到駭客寫的程式。當他們說某個程式有一個緩衝區溢出漏洞時就是指這種東西。 #### 計算記憶體配置 邏輯如下 ```clike= char* bigString; int i = 0; i = strlen("John, ") + strlen("Paul, ") + strlen("George, ") + strlen("Joel "); bigString = (char*) malloc (i + 1); ``` 我們必須把每個字串掃描一次才能算出所有字串的總長度,然後在連接字串時又要再掃描一遍。 既然用Pascal字串時 strlen動作會變快。或許我們可以寫一個能替我們配置記憶體的strcat。 這時就出現另一種蠕蟲(譯註:指下面所討論的可用鏈結):記憶體配置器 #### 記憶體配置器 malloc malloc的特性是有一長串由可用記憶體組成的鏈結串列,名為可用鏈結(free chain)。 ![](https://hackmd.io/_uploads/H1xQYu1S43.png) 當你呼叫malloc時會掃描鏈結串列, 找尋大小滿足要求的記憶體區塊。 然後把區塊切成兩塊,一塊是你所要求的大小,另一塊則是剩下的記憶體,把你要的那塊傳給你, 再把另一塊(如果有的話)放回鏈結串列中。 當你呼叫free時會把釋放的區塊加回可用鏈結。 最後可用鏈結會被切割成很多小塊,當你要求大塊記憶體時就會找不到大小適合的記憶體。於是malloc 就會逾時並開始掃描可用鏈結,嘗試把相鄰的小可用區塊合併成較大的區域 **malloc 代價昂貴** 聰明的程式師在配置記憶體時會用2的次方為大小(比如4位元組,8位元組,16位元組,18446744073709551616位元組等等), 讓malloc的潛在不隱定性降到最低。這樣可以讓可用鏈結裡小碎塊的數量降到最低, 總而言之,在這往下到最底層的位元組世界中,生命愈來愈混亂。 我們現在有像Perl、Java、VB、XSLT之類的偉大程式語言,讓你永遠都不用考慮這種事情,因為程式語言都想辦法替你處理好了。 不過偶而鉛管等基礎設施 還是會在客廳地板中央突出來,這時候我們就得考慮要用String類別還是StringBuilder類別, 或是想想某些類似的差異,因為編譯器還不夠聰明,無法理解我們嘗試完成的每件事,也無法幫助我們不會疏忽寫出油漆工 YxTxn 演算法 ### 思考處理資料位移的問題 #### 在關聯式資料庫存取資料 一個關聯式資料庫會如何實作SELECT author FROM books呢? 在關聯式資料庫中, 資料表的每一行(舉例來說是books資料表)的位元組數都完全相同,而且各個欄位由列開頭起算的偏移量都是固定的。 所以舉個例子來說,如果books資料表中每個記錄的長度都是100個Bytes,而author 欄是位於偏移量23的地方,那麼各個作者就會存在第23, 123, 223, 323等位元組的位址。 ![](https://hackmd.io/_uploads/BkBC6yr42.png) 在這個SQL查詢中要移到下一個記錄時要怎麼做呢 pointer += 100; 只要一個 CPU 指令即可 #### 在XML格式存取資料 現在讓我們來看看用XML格式儲存的books資料表。 ```xml= <?xml 廢話...> <books> <book> <title>UI Design for Programmers</title> <author>Joel Spolsky</author> </book> <book> <title>The Chop Suey Club</title> <author>Bruce Weber</author> </book> </books> ``` 來個小問題。要移到下一個記錄的程式要怎麼寫? 這時候一個好的程式師可能會說,好吧,讓我們分析XML轉成樹狀結構存在記憶體裡,這樣處理起來會相當快。 不過這時候CPU要處理SELECT author FROM books所做的工作量絕對會無聊到讓你哭出來。 每個編譯器作者都知道字彙和語法分析是編譯過程中最慢的部份。簡單的說,字彙分析和語法分析時牽涉到大量的字串處理 (前面已經發現這是很慢的)和大量的記憶體(也是已知很慢的),然後還要在記憶體中建立一個抽象文法樹。 而且還要假設你擁有足夠的記憶體可以一次載入所有資料。 在關聯式資料庫中,在記錄間移動的速度是固定的,而且實際上只要一個CPU指令。這是刻意設計出來的。 另外利用記憶體映對檔案,就只要把真正要用的資料由載入對應的磁碟區塊即可。在用XML的時候,如果有作前處理預先分析, 在記錄間移動的速度也是固定的,不過需要大量的啟動時間,如果不預先分析,那麼在記錄間移動的速度就要看之前的記錄有多長了, 而且不管怎麼都要幾百個CPU指令。 #### 結論 這表示當資料量很大又要求速度時不能用XML。如果資料不多或做的事並需要速度,XML是個不錯的格式 如果你想魚與熊掌兼得,就得想個方法在XML以外加些詮釋資料(metadata),儲存類似Pascal字串中位元組數目的資料。 這樣能提示資料在檔案中的位址,就不需要再去掃描分析了。不過當然不能用文字編輯器去編輯檔案了, 因為會把詮釋資料弄亂,所以其實這也不能算上真正的XML了。 ### 為何基礎很重要 探討像strcat和 malloc究竟如何運作這種枯燥的電腦科學一年級課程,能在你面對XML等技術時,協助你思考最新的高層次策略與架構上的決策。至於今天的作業,可以想想為什麼全美達(Transmeta)的晶片好像總是賣不好。 或者想想為什麼最早HTML規格的TABLE設計會爛到讓撥接上網的人不能快速的顯示大型表格。也可以想想COM既然這麼快, 為什麼跨越行程邊界時又快不起了。或者NT設計者為什麼會把顯示驅動程式放入核心空間而不留在使用者空間呢。 這所有的問題都需要你考慮位元組的層次,而它們也影響到我們在各種架構和策略上所做整體而高層次的決策。 ## 約爾測試(Joel Test)[^6] 約耳測試(Joel Test)是相較於SEMA[^7] 來說較為簡單的檢測軟體開發團隊好壞的指標。 內容如下: 1. 你有使用原始碼控制系統嗎? 2. 你能用一個步驟建出所有結果嗎? 3. 你有進行每日編譯嗎? 4. 你有沒有問題資料庫? 5. 你會先把問題都修好之後,才寫新的程式嗎? 6. 你有一份最新的時程表嗎? 7. 你有寫規格嗎? 8. 程式設計人員有沒有安靜的工作環境? 9. 你有沒有用市面上最好的工具? 10. 你有沒有測試人員? 11. 是否在面試時要求面試的對象試寫程式? 12. 是否進行過走廊使用性測試? ## 參考 [^1]:https://en.wikipedia.org/wiki/Joel_Spolsky [^2]: https://blog.techbridge.cc/2021/02/08/reading-notes-of-joel-on-software/ [^3]: https://www.joelonsoftware.com/about-me/ [^4]: https://ignitetech.com/softwarelibrary/fogbugz [^5]: https://hash.ai/ [^6]: https://www.joelonsoftware.com/2000/08/09/the-joel-test-12-steps-to-better-code/ [^7]: https://www.sei.cmu.edu/our-work/all-topics/index.cfm [^8]: https://www.csie.ntu.edu.tw/~p92005/Joel/fog0000000319.html [^9]: https://www.joelonsoftware.com/2001/12/11/back-to-basics/