不要動(dòng)輒滾粗,先看堆棧是否溢出
中國人是慣于精打細算的,魯迅先生說(shuō):“時(shí)間是海綿里的水,擠一擠總會(huì )有的!”
本文引用地址:http://dyxdggzs.com/article/201910/406345.htm領(lǐng)導說(shuō),魯迅說(shuō)得對!
于是,領(lǐng)導們經(jīng)常帶著(zhù)期盼的神情,忽悠苦逼的軟件工程師:“再多想想辦法吧,嗯,MCU的主頻是低了點(diǎn),RAM資源是少了些,但是,考慮一下成本,MCU還是盡量不要換的吧?方法總比困難多,看著(zhù)RAM資源好像不大夠,但是換個(gè)實(shí)現方式,還可以再擠一擠的吧?魯迅先生曾說(shuō)......”
好吧,領(lǐng)導們肯定讀過(guò)華嚴經(jīng),深諳佛菩薩“螺螄殼里做道場(chǎng)”的本事:大即是小,小即是大,大小無(wú)二無(wú)別!嫌功能太多,RAM資源太少,多少算多呀?為啥子要生出那么多分別心撒?
可是,領(lǐng)導們可能不知道,魯迅先生寫(xiě)錯了字不叫錯別字,叫“通假字”,我們寫(xiě)錯了就是實(shí)實(shí)在在的“錯別字”,而且佛菩薩的境界也是“非汝邊事”。所以,用小馬拉大車(chē),在資源一般般的MCU中塞入盡可能多的代碼,實(shí)現那么多功能,還能讓這些模塊配合無(wú)間地親密運轉,實(shí)在不是我等的境界了!
這不,同事小王又找我來(lái)訴苦了。
1
“馬步君,救救我吧??煲活I(lǐng)導折磨瘋了,這就是個(gè)16位的單片機,RAM總共512個(gè)字節。留給堆棧256個(gè)字節,剩下的就只有256字節了,哪能實(shí)現那么多功能呢?可是領(lǐng)導說(shuō)干嘛留給堆棧256個(gè)字節,堆棧留少一點(diǎn)RAM不就夠用了嗎?”
說(shuō)著(zhù)說(shuō)著(zhù),小王愈加地憤憤不平了:“明明有個(gè)管腳兼容的芯片,RAM有1k字節,但是領(lǐng)導就是不讓換。說(shuō)讓堆棧留少一點(diǎn),哼,他知道個(gè)屁!滾粗,堆棧不夠的話(huà)系統會(huì )跑飛的呀!”
看著(zhù)小王蠟黃黃的臉蛋和紅通通的眼睛,我心下有些不忍:‘萬(wàn)般皆苦,做人最苦,難怪如來(lái)說(shuō)為可憐愍者呀!’可是,領(lǐng)導說(shuō)的也不無(wú)道理,對于堆棧該設置多少,很多人都是稀里糊涂,又有多少人能夠弄得明白呢?于是我竟而給領(lǐng)導辯護了起來(lái):“也許領(lǐng)導說(shuō)得對吧,因為你確實(shí)不知道該給堆棧留多少空間吧?”
我一邊小心翼翼地說(shuō)著(zhù),一邊看著(zhù)小王的臉慢慢地耷拉了下來(lái),甚而就要拉到地上了。于是我趕忙提起萬(wàn)般的精力找補一番,給他講起MCU中RAM資源和堆棧分配的矛盾性來(lái):
“RAM資源確實(shí)很重要,領(lǐng)導的意思應該是說(shuō)你對它的分配要照顧到應用、系統堆棧兩方面的需求,不可有所偏頗。
在MCU的地址空間中,RAM是連續分配一段線(xiàn)性地址空間,應用中用到的全局變量、中斷和系統調用用到的棧、動(dòng)態(tài)分配用到的堆都要分配在這段有限的線(xiàn)性空間內,當然你可以選擇不用‘堆’。不過(guò),如果有所富余,或者確實(shí)需要,你還得把存在程序存儲空間中的一段代碼復制到RAM空間內運行,以加快程序的運行速度,提高系統實(shí)時(shí)性。
所以,RAM資源確實(shí)是有限,不可能也不應該盲目得為堆棧分配太大的尺寸。不過(guò)話(huà)又說(shuō)回來(lái),如果堆棧設置地過(guò)小也不行,因為設置過(guò)小的話(huà),一旦程序設計得不合理就很容易出問(wèn)題。比如在函數調用中子函數中的局部變量太多、中斷優(yōu)先級設置得不合理導致高低中斷間的嵌套、中斷ISR程序過(guò)長(cháng)導致本中斷被嵌套,或者出現函數調用層次過(guò)深等程序設計不當之處都可能導致堆棧溢出,改變臨近堆棧的RAM空間中的內容,從而造成程序運行異常,發(fā)生故障甚至導致重大事故。
從這個(gè)角度來(lái)說(shuō),在一定程度上,堆棧設置得大一些,有利于彌補程序設計的缺陷。話(huà)再說(shuō)回來(lái),程序設計地很完美,就不需要設置那么大的堆棧。歸結到底,這就是個(gè)平衡木??!”
跟小王進(jìn)行了這段科普后,他著(zhù)實(shí)有些懵圈了。于是我把他晾在一邊,忙活起了自己的事兒。
我想,上面那番話(huà)夠他消化一段時(shí)間的了。
2
快到飯點(diǎn)了,辦公室里突然熱鬧了起來(lái),有人在大聲講電話(huà),有人被踩了尾巴似的叫上一聲,然后戛然而止,就像被一把剪刀剪斷了聲線(xiàn)一般,有人開(kāi)始四處走動(dòng)串聯(lián),但是我卻感到背后有一種異樣的寂靜!果然,一回頭,小王又找上門(mén)來(lái)了。
“馬步君,你剛才說(shuō)的是不錯,堆棧不能設置得過(guò)大,也不能設置得過(guò)小,可是這好像等于什么也沒(méi)有說(shuō)一樣嘛。歸根到底,我該怎么設置堆棧的大小呢?”緩過(guò)神來(lái)的小王,突然發(fā)現我只是專(zhuān)業(yè)性地描述了問(wèn)題,卻沒(méi)有給出問(wèn)題的答案。
“孺子可教也,”小王的發(fā)問(wèn)讓我不禁有些凜然,我一邊向他投去贊賞的目光,一邊心下思忖該怎么樣回答。思量片刻,我又開(kāi)啟了說(shuō)教模式:
“可以通過(guò)靜態(tài)分析的方式確定堆??臻g的尺寸。你需要根據源程序中每個(gè)函數的局部變量大小確定每個(gè)函數的堆棧使用量,然后根據編譯器生成的函數調用列表為每個(gè)函數建立調用樹(shù),檢查每棵調用樹(shù),確定從樹(shù)根到樹(shù)葉的調用路徑的堆棧使用量,從中選出最大堆棧使用量,同時(shí),還要仔細分析系統用到的所有中斷,確定中斷服務(wù)程序的堆棧使用量?!?/span>
看著(zhù)他再次陷入懵圈狀態(tài),我滿(mǎn)意地點(diǎn)了點(diǎn)頭,鼓起腮幫子繼續說(shuō)教,
“但是,除了咱們自己寫(xiě)的程序,你所調用的C標準庫函數以及大值整數的乘除、浮點(diǎn)運算等對應的運行庫函數也會(huì )消耗堆棧,它們的堆棧使用量具體是多少我也不是很清楚,但是應該可以查得到。講到這里你也看到了,這種靜態(tài)分析方式對開(kāi)發(fā)者的技術(shù)水平、對產(chǎn)品代碼的理解程度要求非常高,得到的數據并不完善,而且這種方式依賴(lài)于具體的應用和源程序實(shí)現方式,所以,好麻煩!”
被說(shuō)到懷疑人生的小王再次鎖緊了眉頭,抿著(zhù)嘴唇一言不發(fā),他在想什么我不清楚,但是我想:“按照我剛才的說(shuō)法,我不是也不知道該怎么設置堆棧大小的嘛?哎,做人難,做嵌入式軟件工程師更難??!”
3
我本以為這件事到此結束了,沒(méi)曾想吃完飯后,小王又找上門(mén)來(lái)了,“馬步君,你剛才說(shuō)了,通過(guò)靜態(tài)分析判斷堆棧使用量對程序員要求很高,而且不通用,那么,有沒(méi)有一種動(dòng)態(tài)的判斷方式呢?”
“當然有了,可以在鏈接文件中,對RAM的空間分配做手腳?!蔽以俅钨┵┒勂饋?lái),這邊廂我吐沫飛濺,那邊廂小王兩眼放光。各位看官且先不要覺(jué)得筆者的思維實(shí)在敏捷、腦路不得了的靈光,而對筆者投來(lái)欽敬的目光。實(shí)際上,就在吃飯的空當,我就在苦苦地思索,到底該怎樣,堆棧的空間分配才算適當。
如果不在鏈接文件中做任何設置,RAM就是堆棧區+全局變量區,這樣一來(lái),堆棧區以下便是全局變量區,堆棧的生長(cháng)方向為自上而下,即向著(zhù)RAM地址減小的方向增長(cháng),堆棧溢出時(shí)改變全局變量的值,可是很多情況下,你根本意識不到程序溢出,只有在特殊的觸發(fā)條件下程序運行某個(gè)功能時(shí),你才可能意識到不對勁。
所以,為了第一時(shí)間就檢查到堆棧溢出,要加入一個(gè)緊鄰堆棧區的新區,這個(gè)新區叫‘堆棧溢出緩沖區’。想一想哈,這時(shí)堆棧溢出時(shí)就會(huì )改變‘堆棧溢出緩沖區’的數據,只要我上電初始化時(shí)將‘堆棧溢出緩沖區’初始化為固定數據,然后定期查詢(xún)這個(gè)新區中的數據,就能判斷堆棧是否溢出,而且可以判斷這一段時(shí)間內的最大堆棧使用量?!?/p>
我緩緩著(zhù)解釋著(zhù)自己的思路,等著(zhù)小王慢慢跟上來(lái)。過(guò)了一會(huì )兒,小王又猝不及防地發(fā)問(wèn)了:“如果堆棧設置地比較大,不會(huì )發(fā)生溢出,那這個(gè)‘堆棧溢出緩沖區’也起不到什么作用,只會(huì )白白浪費RAM資源??!”
好吧,我承認,當時(shí)確實(shí)被他問(wèn)住了,但是,既然之前的思路已經(jīng)打開(kāi),再打個(gè)小補丁就不算什么難事了。我思量片刻,就給出了讓他滿(mǎn)意的答案:
“可以在MCU上電初始化時(shí),將堆棧區和堆棧溢出緩沖區的數據全部初始化為一個(gè)固定數據,比如0xa5,將最大堆棧使用量記為stack_max,然后用一個(gè)周期定時(shí)器定時(shí)讀取堆棧溢出緩沖區和堆棧區的數據,就可以判斷堆棧設置是否過(guò)大。
而且,第二次讀取這兩個(gè)區的數據時(shí),從stack_max個(gè)數據后開(kāi)始讀取即可,比如上周期統計到堆棧用到100個(gè)字節,stack_max=100,下個(gè)周期從第101個(gè)字節開(kāi)始讀起就可以了。
如果你開(kāi)始設置堆棧為384個(gè)字節,跑了一天后,發(fā)現stack_max=210,那就把堆棧設置為256就可以了。這樣就能解決你的問(wèn)題-科學(xué)合理地縮小堆棧分配了!”
4
過(guò)了幾天,小王終于發(fā)現,‘原來(lái)’自己的程序用到的堆棧從來(lái)都不會(huì )超過(guò)130個(gè)字節,于是他乖乖地改小了堆棧,把空出來(lái)的100來(lái)個(gè)字節都分給了全局變量,RAM一下子綽綽有余了,他很開(kāi)心地對我說(shuō):看來(lái)還是不要動(dòng)輒滾粗,要先看看堆棧是否真的溢出!
評論