嵌入式
【第一部分】背景簡介
前幾年鬧得沸沸揚(yáng)揚(yáng)的豐田剎不住事件最近又有新進(jìn)展。十月底俄克拉荷馬的一次庭審,2007年一輛2005年凱美瑞暴沖(Unintended Acceleration,UA)致一死一傷事件中豐田被判有責(zé)。引起廣泛關(guān)注的是庭審中主要證人Michael Barr的證詞讓陪審團(tuán)同意豐田的動力系統(tǒng)軟件存在巨大漏洞可能導(dǎo)致此類事件。這是豐田在同類事件中第一次被判有責(zé)。庭審過后豐田馬上同意支付300萬美元進(jìn)入調(diào)解程序。
出于好奇,我漫不經(jīng)心地下載了Barr的286頁證詞,卻一下子被吸引住了。幾天內(nèi)讀完,算是對這次事件進(jìn)行了一次深入了解。下面就從外行角度總結(jié)一下這份證詞并嘗試以更簡單的語言解釋里面提到的暴沖原因以及豐田犯下的錯誤。
Barr的證詞下載自他的個人博客Barr Code,但現(xiàn)在該文已經(jīng)被刪除。稍后找地方上傳。
Michael Barr是誰?他是一位擁有20年以上行業(yè)經(jīng)驗(yàn)的嵌入式系統(tǒng)工程師。在十八個月中,有12位嵌入式系統(tǒng)專家,包Barr,受原告訴訟團(tuán)所托,被關(guān)在馬里蘭州一間高度保安的房間內(nèi)對豐田動力控制系統(tǒng)軟件(主要是2005年的凱美瑞)源代碼進(jìn)行深度審查。這房間沒有英特網(wǎng),沒有手機(jī)信號,他們進(jìn)出不能攜帶任何紙張、記錄甚至皮帶。最后的調(diào)查結(jié)果被寫入一份800頁,13章的詳細(xì)報(bào)告。而鑒于保密協(xié)議,調(diào)查內(nèi)容一直沒有公布,直至俄克拉荷馬這次庭審才首度部分公開(報(bào)告本身似乎還沒公開)。
回到正題。豐田的軟件有沒有缺陷?根據(jù)Barr的調(diào)查,答案是有。這其實(shí)是廢話,任何軟件都會有缺陷,關(guān)鍵在于是什么樣的缺陷。豐田的軟件缺陷分為三類:
非常業(yè)余的結(jié)構(gòu)設(shè)計(jì)。
軟件設(shè)計(jì)的基本要求是模塊盡量簡單化,因?yàn)檫@樣可以一來更易于閱讀二來更易于維護(hù)。但豐田的工程師顯然沒有遵循這原則。Barr使用一種工具自動根據(jù)代碼的可能分支數(shù)量評估函數(shù)的復(fù)雜度,結(jié)果是豐田的軟件中至少有67條函數(shù)復(fù)雜度超過50,意味著運(yùn)行這個函數(shù)可能出現(xiàn)超過50種不同的執(zhí)行結(jié)果,屬于“非可測”級別。因?yàn)闉榱藴y試這50個不同的結(jié)果,必須準(zhǔn)備至少50條不同的測試用例以及相應(yīng)的文檔,在生產(chǎn)環(huán)境中一般是不現(xiàn)實(shí)的。作為比較,Barr表示他自己的公司嚴(yán)格執(zhí)行的其中一條規(guī)定就是任何代碼復(fù)雜度不能超過30,否則不合格。而在這67條函數(shù)中還有12條復(fù)雜度超過100,達(dá)到“非可維護(hù)”級別,意味著一旦發(fā)現(xiàn)缺陷(Bug)也無法修復(fù),因?yàn)閷?shí)在太復(fù)雜,修復(fù)缺陷的過程中會產(chǎn)生新的缺陷。其中最復(fù)雜的一條函數(shù)有超過1300行代碼,146個可能執(zhí)行路徑——正好用于根據(jù)各傳感器數(shù)值計(jì)算節(jié)氣門開關(guān)角度。
如果你不知道什么是節(jié)氣門,那么這里我稍微解釋一下。為了讓內(nèi)燃機(jī)運(yùn)行,有三大要素:燃油、空氣和點(diǎn)火時機(jī)??諝夂腿加偷幕旌衔镞M(jìn)入氣缸,被火花塞在正確的時間點(diǎn)燃推動活塞并最終推動曲軸和車輪前進(jìn)。在電噴技術(shù)發(fā)明以后直到2002年以前,三大要素的燃油和點(diǎn)火時間是由電子設(shè)備控制,節(jié)氣門機(jī)械連接加速踏板,由司機(jī)直接控制。節(jié)氣門大致是一個連接加速踏板的“空氣龍頭”——踩下去越多,“龍頭”打開得越大,允許越多的空氣進(jìn)入發(fā)動機(jī)輸出更大的動力。2002年以后,豐田引入的“電子油門”讓電子系統(tǒng)掌管了最后一個要素:空氣。加速踏板不再機(jī)械連接節(jié)氣門,而是連接一些傳感器,由行車電腦將這些傳感器數(shù)值計(jì)算成節(jié)氣門開啟角度再由馬達(dá)控制節(jié)氣門。我們稍后會再討論節(jié)氣門開合。
極復(fù)雜的代碼帶來的是極復(fù)雜的功能。下面說一下被稱為“廚房洗滌盆”的Task X。這里先解釋一下,豐田的軟件系統(tǒng)和很多別的軟件系統(tǒng)一樣,都是由一個統(tǒng)領(lǐng)程序(稱之為“操作系統(tǒng)”)和若干小程序(稱之為Task)組成。就好比電腦上跑的Windows是統(tǒng)領(lǐng)全局的操作系統(tǒng),網(wǎng)絡(luò)瀏覽器和記事本是跑在操作系統(tǒng)上的小程序。豐田的系統(tǒng)里每個Task都有自己的名字,但這些名字非常敏感,敏感到每次被提及的時候律師都要求法庭內(nèi)的沒有閱讀代碼權(quán)限的人全部清場。為了減少清場次數(shù),Barr將這個最重要的小程序稱為Task X。這個Task X有多重要呢?跟廚房里的洗滌盆一樣重要。它負(fù)責(zé)非常多的事情,包括計(jì)算節(jié)氣門開啟角度、速度監(jiān)測和保持、定速巡航監(jiān)測等等。Task X的不正常運(yùn)行被認(rèn)為是暴沖事件的元兇。稍后會再繼續(xù)討論Task X。
還有一些別的匪夷所思的發(fā)現(xiàn)。比如豐田的軟件包含了超過一萬一千個全局變量。如果你不知道什么是全局變量,那么只需要知道軟件設(shè)計(jì)的一般原則是要盡量少使用全局變量,因?yàn)橛锌赡軒頍o法預(yù)測的結(jié)果。這里的“少”的意思是“盡量接近零”,絕對不會是一萬一千個。
不符合軟件開發(fā)規(guī)范。
如同很多行業(yè)一樣,汽車行業(yè)也有自己的規(guī)范。更具體一點(diǎn),由于汽車的危險性質(zhì),汽車控制系統(tǒng)被劃分為“安全關(guān)鍵性系統(tǒng)(Safety Critical System)”——說白了就是安全性非常重要,弄不好會死人的。為了達(dá)到這一特殊要求,汽車相關(guān)軟件開發(fā)人員定期舉行會議討論并發(fā)布編程規(guī)范,稱為MISRA C。該規(guī)范的2004年版的感謝列表里還能看到豐田員工的名字,至少讓外界認(rèn)為豐田確實(shí)也在遵循這些規(guī)范。
真的嗎?根據(jù)源代碼來看,答案是否定的。對此之前的NASA報(bào)告也有所提及,豐田辯稱他們遵循的不是行業(yè)規(guī)范,而是豐田內(nèi)部編程規(guī)范。這一規(guī)范與行業(yè)規(guī)范的吻合程度達(dá)到50%。但是Barr認(rèn)為根據(jù)他的調(diào)查,兩個規(guī)范之間吻合度小于10%,甚至有若干規(guī)范條目相互沖突。后來發(fā)現(xiàn)豐田的代碼甚至沒有遵循豐田內(nèi)部規(guī)范,當(dāng)然比起別的問題這個已經(jīng)無關(guān)緊要了。
MISRA C擁有超過100條規(guī)范,NASA的調(diào)查只使用了到其中35條進(jìn)行校對,發(fā)現(xiàn)超過7000處違規(guī)代碼。Barr使用全部條目,對照結(jié)果是豐田的程序擁有超過80000處違規(guī)代碼。
這些數(shù)字說明了什么?根據(jù)統(tǒng)計(jì),違規(guī)數(shù)量可以用于預(yù)測代碼中暗藏的缺陷(Bug)數(shù)量。在之前提到的汽車相關(guān)軟件開發(fā)人員會議中,有人就這一主題發(fā)表過專題演講,提出每30處違規(guī)代碼可能包含一個重大缺陷和十個輕微缺陷。諷刺的是這人是豐田員工。
特別需要指出MISRA C其中一個規(guī)則的內(nèi)容是不得使用遞歸。
如果你不知道什么是遞歸,那么這里我稍微解釋一下。遞歸是一種計(jì)算方法。但一般計(jì)算方法要么是自己算,要么詢問別的計(jì)算模塊索要結(jié)果。而遞歸則是通過問一層層問自己的方法完成計(jì)算。好處是代碼簡單,壞處是計(jì)算層數(shù)不固定??赡軙?層就出結(jié)果了,也可能會是10000層,在設(shè)計(jì)程序的時候無從得知。
軟件計(jì)算需要消耗存儲器。越繁瑣、越長的計(jì)算自然需要占用越多的存儲器。遞歸的問題在于其嵌套層數(shù)無法預(yù)測,從而導(dǎo)致可能消耗的存儲器容量無法控制。稍后會再討論存儲器。
對關(guān)鍵變量缺乏保護(hù)。
什么是變量?變量就是存在一段存儲器的0和1的集合。這些變量既可以是一些函數(shù)的處理結(jié)果,也可以是另一些函數(shù)的處理原材料。比方說前面提到有一條程序?qū)iT計(jì)算節(jié)氣門開合角度,比如說是20度,這個20就是一個變量,存在存儲器的一個指定位置。另一個程序?qū)iT負(fù)責(zé)開合節(jié)氣門,它知道去那個指定位置讀取這個20,然后把節(jié)氣門開啟20度。
什么是保護(hù)?嵌入式系統(tǒng),或者任何系統(tǒng),都會在一定條件下發(fā)生硬件或者軟件錯誤。客觀上這是無法避免的。而且汽車作為一個時常在震動、發(fā)熱、位移的系統(tǒng),它的內(nèi)部環(huán)境不能說不惡劣,發(fā)生硬件錯誤的可能性甚至更高。什么樣的硬件錯誤呢?別忘了變量都是0和1的組合,這些0和1由存儲器上的高低電平代表。由于某些不可抗原因,一個電平從高變成低,或者反過來,那么這個變量就被更改了。這被稱為“位反轉(zhuǎn)(Bit Flip)”。為了對抗這樣的事情發(fā)生,需要對變量進(jìn)行保護(hù)。保護(hù)的方法一般是鏡像法。簡單來說就是在兩個不同的地方寫入同一個變量,讀取的時候兩邊都讀,比較是不是一致。如果不一致,那么可以得知這個變量已經(jīng)不可靠,需要進(jìn)行容錯處理。
豐田的程序總體上對其上萬個變量進(jìn)行了有效保護(hù),但問題出在操作系統(tǒng)上。前面提到豐田的軟件本質(zhì)上分為操作系統(tǒng)和Task。Task是由豐田自己開發(fā),但是操作系統(tǒng)則是由芯片供應(yīng)商提供,固化在芯片里的。豐田在這里的過失是沒有對供應(yīng)商提供的代碼進(jìn)行深度審核,拿到什么用什么。
另一個保護(hù)措施是錯誤校驗(yàn)碼(Error Detective and Correction Codes,EDAC)。這是一個硬件層面的數(shù)據(jù)保護(hù)措施。簡而言之就是給內(nèi)存中每一個字節(jié)(8比特)后面物理地增加幾比特校驗(yàn)碼。這樣萬一變量出錯了,可以通過校驗(yàn)碼得知,甚至可以通過校驗(yàn)碼修復(fù)一些輕微錯誤。這個措施十分簡單有效,但是在2005年款凱美瑞的系統(tǒng)中完全沒有使用,豐田卻告訴NASA他們用了。而在2008年款凱美瑞中使用了3比特長的EDAC。Barr認(rèn)為是為了節(jié)省成本,否則應(yīng)該使用5比特長。
還有值得一提的是,汽車相關(guān)的軟件行業(yè)有那么幾家操作系統(tǒng)供應(yīng)商,早已形成了一套成熟標(biāo)準(zhǔn)稱為OSEK。各供應(yīng)商開發(fā)的符合OSEK認(rèn)證的操作系統(tǒng)至少都能達(dá)到一定的質(zhì)量。但豐田選用的操作系統(tǒng)卻沒有通過認(rèn)證,讓人不解。
現(xiàn)在我們知道豐田在編寫軟件的時候至少有三種缺陷。那么重點(diǎn)問題:豐田的這些軟件缺陷是否會導(dǎo)致車輛暴沖?根據(jù)Barr的調(diào)查,答案是有可能。暴沖的起因需要結(jié)合上述三種缺陷來說明。
汽車正常運(yùn)行需要倚靠若干程序(這里叫Task)的同時運(yùn)作。Task有很多,CPU只有一塊,在任何時刻只能處理一個Task,怎么辦呢?這需要操作系統(tǒng)的統(tǒng)籌規(guī)劃,合理分配CPU的任務(wù),讓每個Task都能按時執(zhí)行。如果出現(xiàn)某種意外,讓某個Task突然不執(zhí)行了,那么就稱為這個Task“死亡”。Task死了,自然不能執(zhí)行它的任務(wù)。根據(jù)Barr的測試,在某些特定情況下,Task X的死亡可以導(dǎo)致節(jié)氣門敞開——因?yàn)門ask X的其中一個任務(wù)就是根據(jù)司機(jī)的操作計(jì)算節(jié)氣門開合角度,它死了也就沒法重新計(jì)算這個角度,即使司機(jī)把腳挪開加速踏板,節(jié)氣門也無法關(guān)閉。此為暴沖的直接原因。更糟糕的是,節(jié)氣門的開合角度這個數(shù)值,被Task X算出來以后保存在一個變量中。這個特定的變量正好沒有被保護(hù)(缺陷3)。意味著萬一Task X死亡并且停止計(jì)算,這個數(shù)值有可能因?yàn)椴豢煽乖虮桓淖?,而程序無從得知。
那么Task X為何會死亡呢?一般是因?yàn)閮?nèi)存出錯。這個出錯可能是一個小小的位反轉(zhuǎn),也可能是內(nèi)存里的數(shù)值被別的程序錯誤覆蓋。同一系統(tǒng)內(nèi)同時運(yùn)行了若干程序,這些程序需要共享一塊內(nèi)存,內(nèi)存內(nèi)部必然要被劃分成若干塊。比如第一塊給程序1,第二塊給程序2,等等。如果程序1因?yàn)槟承┰颍ū热鏐ug)寫到第二塊內(nèi)存上去,就會導(dǎo)致程序2讀取了錯誤的信息。這就是所謂的內(nèi)存出錯。豐田的系統(tǒng)里,正好有這么兩塊相鄰的內(nèi)存塊。第一塊被稱為“堆棧(Stack)”,這是所有Task存儲它們運(yùn)行狀態(tài)的地方,大小為4KB。與之相鄰的地方儲存了操作系統(tǒng)進(jìn)行任務(wù)分配的記錄。那么可以想象,如果很多Task給堆棧里寫入太多東西,超過4KB,那么就會錯誤地寫入與之相鄰的任務(wù)分配表。這種錯誤被稱為“堆棧溢出”。操作系統(tǒng)拿到了錯誤的任務(wù)分配表,就會錯誤地分配任務(wù),造成某些Task多執(zhí)行幾次,某些Task少執(zhí)行幾次,某些Task甚至就再也不執(zhí)行——死了!必須指出的是,程序死亡并不罕見,甚至可以認(rèn)為是正常現(xiàn)象。稍后解釋處理方法。
那么堆棧為什么會溢出呢?顯然是因?yàn)橐獙懭氲臄?shù)據(jù)超過了堆棧的容量。在設(shè)計(jì)程序的時候要計(jì)算最壞的情況并且允許冗余。即使作出了正確的設(shè)計(jì),這種錯誤也相對常見,所以NASA在他們的調(diào)查中重點(diǎn)排查堆棧溢出的可能性。于是NASA問豐田,豐田的回復(fù)是最壞的情況下4KB堆棧只寫入了41%的數(shù)據(jù),換句話說發(fā)生溢出的可能性非常低。NASA直接取信了這個數(shù)字并沒有再深入調(diào)查。但Barr他們發(fā)現(xiàn)豐田的回答有嚴(yán)重低估,實(shí)際上最壞的情況會達(dá)到94%,而且還不算遞歸。豐田在代碼中使用了遞歸(缺陷2)。因而實(shí)際數(shù)字有可能超過94%而且無法預(yù)計(jì)上限,因?yàn)檫f歸計(jì)算的嵌套層數(shù)是無法預(yù)測的。所以實(shí)際情況下堆棧溢出的可能性相當(dāng)可觀。一旦溢出,相鄰的任務(wù)分配表不可避免就會遭到破壞。此為暴沖的根本原因其中之一。之所以說“其中之一”,是因?yàn)槎褩R绯鰞H僅是損壞任務(wù)分配表的其中一個原因,別的還有許多可能性并沒有被Barr在法庭上深入解釋。而且任務(wù)分配表的損壞也僅僅是導(dǎo)致Task死亡的原因之一。
順便提一句,2005年的凱美瑞的這部分供應(yīng)商是電裝,沒有搭載堆棧實(shí)時監(jiān)測功能——溢出了也不知道。同年的卡羅拉卻搭載了,因?yàn)楣?yīng)商是通用。
到這里我小結(jié)一下,串鏈子。左邊是原因,右邊是后果。
堆棧溢出→(可能導(dǎo)致)→任務(wù)分配表被改寫→(可能導(dǎo)致)→Task X死亡→(可能導(dǎo)致)→節(jié)氣門敞開→(導(dǎo)致)→汽車暴沖
必須指出的是,這條鏈子從最左邊一直到Task X死亡,都還算是嵌入式系統(tǒng)的常見故障。雖然程序代碼寫得不好也許導(dǎo)致更容易出錯,客觀上豐田并沒有特別大的過錯。只要處理得當(dāng),這些故障都不會導(dǎo)致暴沖。
到此為止還只是前奏而已,接下來我們來看看豐田到底做錯了什么。