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