一些關於資料型態的故事

雖然我說要「說一些故事」,故事本來應該有悲有喜, 不過看來我總是要說一些悲劇故事。

忽略或者誤用了資料型態,將造成難以預料的後果。 犯錯的人,通常不是故意的,但是如果他知道那後果,就算不是故意的, 想必也很難自處吧? 以下我要舉出幾個我知道的悲劇故事, 它們的悲劇都是因為資料型態的錯誤或者使用不慎造成的。

千禧蟲

最有名的問題,恐怕就是所謂的「千禧蟲」了吧。 事情的肇因可以追溯到 1960 年代,那時候電腦剛剛誕生不久, 寫程式很麻煩、記憶體又少又貴,再加上那時候寫程式的人多半是美國人, 他們本來就非常習慣用西元 19xx 年的後兩位數來紀年。 當時可能沒什麼人認真想到,竟然那時候寫的某些程式, 居然可以一直用到西元 2000 年。 所以,一方面是節省記憶體、一方面是習慣,早期的 programmers,特別是商業軟體 (譬如金融機構所使用的軟體系統) 的 programmers, 定義一種只放得下兩位十進制數字的資料型態,用來紀錄西元 19xx 年的年份。 後來,電腦記憶體已經不再昂貴,可是似乎師徒相傳已經成為習慣, 即使資料型態夠裝得下更大的整數,還是有許多人只用兩位數字紀年。

因此,所謂「千禧蟲」就是有很多軟體, 特別是與金融有關的、資格比較老的軟體,很可能把西元 1999 年 12 月 31 日記作 99-12-31,而它的下一日,就會變成 00-01-01!

那又怎樣?許多電腦程式要計算從某日到另一日有幾天的時候, 都會先把日期換算成相對於某個「基準日」(epoch day) 的日數。 例如換算成相對於 1970 年 1 月 1 日至今的天數 (設定 1970-01-01 為「第零日」)。 舉例來說,我寫這句話的日子是 2003 年 5 月 23 日, 這一天相對於基準日的日數是 12195 (從 1970-01-01 算起,今天是第 12195 天)。 如果我知道 2000 年 1 月 1 日是相對第 10957 日, 那麼兩數相減就知道今天是進入二十一世紀的第 1238 天。

那麼,讀者想想看,如果你要寫一個「換算基準日數」的程式, 是不是少不了要把做「今年年份」減去「基準年」的計算? 如果是 2003 - 1970 那就沒錯,但是如果只取後兩位,豈不是成了 03 - 70? 拿這個計算結果,再繼續按照程式算下去,會得到什麼樣的答案,其實就很難想像了! 如果日數算錯了,那麼金融機構計算利息的程式會產生怎樣的結果, 不是也很難想像了嗎?

在 1990 年代末期,為了確保這種金融風暴不會發生, 使得世界上的金融機構陷入某種程度的恐慌,砸下去做實驗、抓蟲、測試的金錢和時間, 我沒有精確數字紀錄,但是總有一億美金吧 (光是台灣,據說就至少花掉兩千萬台幣)。 而 2000 年 1 月 1 日早晨的新聞報導,還是替我們蒐集到許多有趣的千禧蟲發作的故事, 當然都不嚴重。譬如說,有一棟高級公寓大樓的智慧型電梯由電腦控管, 但是沒留意它居然也有千禧蟲在裡面, 那天凌晨有些住戶在瘋完跨年派對之後,發現電梯拒絕把他載上樓了。

下一個千禧年還有 997 年 (您讀到這裡的時候,可能所剩更短一點), 似乎不值得現在活著的人操心。 不過,同樣的問題,只要不小心,還是會重新上演。 譬如我寫的現在是民國 92 年,八年之後就是民國 100 年 (如果我們都還在的話), 過去這二十年來,有沒有電腦程式只用兩位數字紀錄民國的年份呢? 如果有,會造成什麼後果呢?我不知道。

除了民國百年危機之外,下一個已知的問題是西元 2038 年。 目前幾乎所有的 UNIX 作業系統其實不是計算「相對基準日」的日數, 而是「秒數」。亦即從 1970-01-01 凌晨開始計秒,把迄今的秒數記在一個整數裡面。 這個整數的資料型態如果是 int (32-bit 整數), 則大約在 2038 年就會爆滿 (大約二十億秒)。 我不知道這份教材是否能夠被使用到西元 2038 年, 萬一能的話,在那之前,我們必須留意這個問題,否則 BCC16 的資料庫裡面, 有許多資料會開始不正確了。

百年人瑞上小學

這可能是現在我要講的故事裡面,唯一不是悲劇的吧。 這個小故事發生在丹麥,一位高齡 107 歲的老太太, 收到一封由電腦印製的規格信件,通知她到當地的小學去註冊入學。 難道這是丹麥最新的「終身學習」社會福利嗎? 不是的,只是因為 (就像千禧蟲一樣),當初寫程式的人沒考慮百歲人瑞, 他寫的程式只用兩位數來儲存年齡,因此一百歲的人就被「歸零」了, 而 107 歲被視為 7 歲,正是入學的年齡。

法國雅利安火箭

以下直接引述李國偉教授翻譯的《電腦也搞不定》第 33 頁 (括號裡是我寫的註解)

1996 年 6 月,法國雅利安五號火箭才升空不到一分鐘, 就自動銷毀了,直接與間接造成幾十億元的金錢損失, 以及使雅利安計畫停滯達數月之久。 對失敗的原因,調查委員會這樣描述:「在主引擎點火後 37 秒, 導向與高度的資訊完全喪失。」原因是:「內部參考系統軟體的規格與設計錯誤。」 最後發現錯誤的地方出在某一行程式,要把 64 位元的數字裝填到 16 位元的位置 (應該是把一個屬於 double 型態的數值, 存入一個 short 型態的變數裡面;譬如說 short n=exp(12); 就會發生這種情況), 使得電腦溢流 (overflow) 爆掉了。

我曾經把這故事講給一位當時數學系三年級的同學聽, 他說「這人肯定沒修過計算機概論十六講」。 照文章看起來,雅利安五號火箭的事故可能沒有人員死亡 (肯定有些人丟官或者失去研究經費)。 下一個故事就沒那麼幸運了。

沙漠風暴戰役

1990 年底至次年初,老布希發動美國對伊拉克的第一次戰爭, 他們稱之為「沙漠風暴」Deseart Storm。就像本世紀初由小布希發動的第二次美伊戰爭一樣, 大多數美軍的傷亡都發生在自己人的不慎 (稱為「友軍火力」friendly fires)。 沙漠風暴中美軍最大的一次傷亡事件, 於 1991 年 2 月 25 日發生在沙烏地阿拉伯之德蘭 (Dhahran) 軍營。 當時正是用餐時間,伊拉克的飛毛腿 (Scud) 飛彈擊中了營房,一次造成 28 人死亡。 那時我在美國賓州留學,當地報紙更詳細地說, 那批陣亡大兵多半來自賓州,許多人的家鄉還是我曾經造訪的鄉村小鎮, 造成我更深刻的印象。

飛毛腿飛彈是當年伊拉克最神勇的武器:機動、快速、準確。 一開始的時候,以色列和美國都吃了它的苦頭,居然沒有有效攔截它的反制武器。 後來,美國發現扔在倉庫角落的愛國者 (Patriot) 飛彈竟然恰好是飛毛腿的剋星, 就趕快徵調它上戰場。以科技眼光來看,愛國者是個老掉牙的地對空飛彈: 它設計於 1960 年代,主要服役於 1970 年代的西歐, 所有的武器控制系統都是在那時候設計和製造的,程式也是那時候寫的。 它部署在歐洲的主要任務是攔截蘇聯的中、高空飛機或巡弋飛彈, 當時假設攔截對象的飛行速度大約二馬赫 (時速約 2500 公里)。 而且,愛國者被設計成機動性強、不容易被發現的小型發射砲台。 它可以被車子載著跑,藏在樹林裡,發射之後就逃跑換個地點躲藏。 這就意味著,愛國者飛彈的電腦開機時間,通常不會很長,經常不到一小時。 在當時的這些任務前提之下,愛國者的控制電腦做得比較簡單: 它的計算儲存器只有 24 位元,也就是說,就無號整數而言, 它的 CPU 只能處理介於 0 與 16777215 之間的整數。

讀者必須明白,武器上面的電腦不是 Intel 或 Sparc 這種東西, 它們是非常特別的設備,因此程式設計師通常沒有 C 語言與 UNIX 作業系統這種奢侈的享受 (你還以為是 Java 嗎?), 他們要不是使用非常特殊的程式語言工具,就是直接用組合語言寫程式。

1970 年代的工程師為愛國者設計了輕簡的硬體, 而程式設計師也因應寫了「夠用」的軟體。 他們用 24 位元的整數來模擬小數,設定小數點下必有四位, 這就是一種「定點數」:小數點固定在第四、五位數之間。 譬如 31.4 就記做 314000,而 2.71828 記做 27183。 愛國者的控制電腦用這種方式計算,並且把某種時間的計算結果累加到一個變數內。 很顯然地,時間在 10-5 秒就要被四捨五入到 10-4秒, 因此將會造成誤差。 這小小的誤差會累積在一個紀錄時間的變數裡面, 執行得越久、累積的誤差越多。 具體地說,每一小時會累積 0.0034 秒的誤差。 而這個數據在愛國者飛彈的控制系統中非常重要, 因為要用它和觀測所得的目標物速度 (譬如說飛毛腿飛彈) 來計算導引雷達的方位變化, 使得雷達可以鎖定目標物,然後發射愛國者飛彈升空攔截。

過去,愛國者飛彈並不太在乎這些誤差。原因有二:

  1. 理論上愛國者飛彈是機動性的,它躲躲藏藏,每次開機不會太久
  2. 它的假想敵飛得不算太快,所以稍微的誤差不會導致導引雷達失效
但是,1990 年它老驥伏櫪、臨危受命,情況卻大不相同:
  1. 這次它被部署在固定營地中,長時間開機, 以備隨時攔截隨時可能射過來的飛毛腿飛彈
  2. 飛毛腿的飛行速度是五馬赫 (時速約 6150 公里),所以前述的小小累積誤差, 對於導引雷達的追蹤能力就有比較大的影響 (每小時誤差看來「只有」0.0034 秒,而時速五馬赫的東西已經飛了 7 公尺)
美軍並非不知道這個道理。根據 2 月 11 日由以色列軍方協助作成的研究, 發現愛國者飛彈的控制系統若連續開機 8 小時,則會造成導引雷達 20% 的偏差。 據此推算,若連續開機 20 小時,則偏差超過 50%,此時愛國者將會和飛毛腿擦身而過, 連邊也碰不上更別談攔截了。此時已經來不及升級硬體,而軟體雖然可以改寫, 但是一時還寫不出來。因此美軍下了一道很合理的「使用說明」命令:
愛國者飛彈的控制系統必須 每小時重新開機 一遍
這樣就可以使累積時間的變數歸零,重新開始,也就不至於累積太多的誤差。 每次重新開機需要 90 秒,這段時間沒有防禦功能, 因此美軍安排了審慎的重開機時間表,使得大家不會在同一段時間重新開機。

但是,就像電影裡面演的笨蛋大兵一樣,那一批負責德蘭基地愛國者飛彈的大兵, 居然讓它連續開機了 100 個小時,完全沒有遵守「使用手冊」的命令。 或許他們覺得那台電腦跑得好好的,不明白為什麼要重新開機; 或許他們每個人都以為前一班的人重開過了; 或許一切都是命中注定;我不知道。 2 月 25 日從那個基地發射出來的「攔截飛彈」完全朝著反方向射出去, 我不知道有沒有因此炸死其他軍民? 就這樣,有 28 名從美國賓州新報到的阿兵哥, 還沒上戰場就在吃飯的時候給炸死了。

而改寫的軟體,恰好在 2 月 26 日送達德蘭基地--就在這個事件的後一天。

事實上,在沙漠風暴戰爭期間,愛國者飛彈的控制軟體一共改寫了六次, 也重灌了六遍。每次重灌大約需要兩小時,在這段期間它沒有防禦能力。

這個事件並非程式設計師或者硬體的錯誤,而是使用者的錯誤。 不過,不論如何, 問題的關鍵也就在於「資料型態」的有限性,以及它在計算中所產生「無可避免」的誤差。

[ 發表感想或意見 ] ‧ [ 讀者推薦課外讀物 ]
單維彰 (2003/05/23) ---