博客專欄

        EEPW首頁 > 博客 > 數(shù)組越界是一顆****炸彈

        數(shù)組越界是一顆****炸彈

        發(fā)布人:魚鷹談單片機(jī) 時(shí)間:2023-04-23 來源:工程師 發(fā)布文章

        數(shù)組越界問題大家在軟件開發(fā)過程中應(yīng)該都司空見慣了。如果你沒見過,大概率是一個(gè)新手,工作經(jīng)驗(yàn)不足,倒不是說你自己會(huì)生產(chǎn)這種 BUG,但有些同事卻可能是 BUG 搬運(yùn)工。

        在魚鷹五年的工作開發(fā)過程中,除了在北京剛畢業(yè)那會(huì)沒遇到這種隱藏問題(碰到的都是自己生產(chǎn)的 BUG,不過自產(chǎn)自銷,也還行),在深圳的這幾家公司都遇到了數(shù)組越界的問題。

        問題一

        第一個(gè)問題是關(guān)于串口驅(qū)動(dòng)導(dǎo)致的越界(最終結(jié)果是 hardfault),這個(gè)魚鷹在以前的筆記中也反復(fù)強(qiáng)調(diào)了,因?yàn)檫@個(gè)問題差點(diǎn)導(dǎo)致自己熬了一個(gè)通宵,也是醉了(老代碼的一個(gè) bug)。

        當(dāng)然這個(gè)問題的解決和當(dāng)時(shí)沒有在線調(diào)試環(huán)境(當(dāng)時(shí)的 PCB 板子通過串口燒錄代碼,沒有調(diào)試接口,大坑)有很大關(guān)系,否則解決起來會(huì)快不少。

        當(dāng)然當(dāng)時(shí)魚鷹也沒掌握這個(gè)方法《BUG 終結(jié)者,現(xiàn)場(chǎng)抓獲!|顛覆認(rèn)知》,否則出現(xiàn)問題時(shí),這種小問題分分鐘定位它。

        所以當(dāng)時(shí)解決這個(gè)問題,全靠玄學(xué):運(yùn)氣。

        否則這個(gè)問題不知道要蹂躪魚鷹多少天。

        問題二

        這個(gè)問題在前東家遇到。當(dāng)時(shí)的環(huán)境是 boot + app 形式。boot 代碼也是跑了多年的老代碼,從來沒有出現(xiàn)過問題。

        直到有一次版本升級(jí),發(fā)現(xiàn)程序不能跳轉(zhuǎn)到 app 正常運(yùn)行(具體細(xì)節(jié)不記得了)。

        當(dāng)時(shí)有同事懷疑是我當(dāng)時(shí)更新的 printf 打印函數(shù)有關(guān)系,因?yàn)楫?dāng)時(shí)的版本更新有這個(gè)改動(dòng)。但魚鷹對(duì)自己寫的代碼還是比較有自信的,并且我的 printf 改動(dòng)和 app 跳轉(zhuǎn)能有什么關(guān)系。

        但懷疑到你頭上了,同時(shí)魚鷹也經(jīng)常負(fù)責(zé)定位這類疑難雜癥,剛好空閑,那就去瞧瞧看了,證明一下這不是你的問題。

        因?yàn)閱栴} 100% 復(fù)現(xiàn),又掌握了那個(gè)現(xiàn)場(chǎng)抓獲的技巧,很快就定位到是 boot 的一段代碼申請(qǐng)的棧數(shù)組空間不足,導(dǎo)致被調(diào)用的函數(shù)使用這塊空間時(shí)越界了。

        類似下面這種:











        func2(uint8_t *buff){  i = 5;  buff[i] = 0;}fun1(){   uint8_t buff[4];   func2(buff);}

        當(dāng)然實(shí)際代碼肯定不可能這么簡(jiǎn)單,i 的值是變化的,不可能一眼看出。

        這個(gè)問題也是導(dǎo)致 hardfault(退出 func2 時(shí),破壞了返回地址)。

        看到?jīng)]有,有時(shí)候二分法(二分查找有問題的代碼提交)查找問題也不是那么可靠,因?yàn)閱栴}可能根本不在提交的的代碼中。

        而下面的問題三也證明了這一點(diǎn)(當(dāng)然不是說二分法沒用,只是不能全靠它作為你的結(jié)果判斷)。

        問題三

        這個(gè)問題是現(xiàn)東家遇到的問題。

        自己開發(fā)的一個(gè)新模塊,當(dāng)合并到主分支時(shí),發(fā)現(xiàn)開機(jī)必定 hardfault,這讓我百思不得其解。自己新加入的代碼,都沒用到數(shù)組,怎么會(huì)hardfault。

        我的第一反應(yīng)就是,不是我的鍋。

        但問題出現(xiàn)在我合并的過程,也只能由我定位了。還好經(jīng)驗(yàn)豐富,一天時(shí)間+加班幾個(gè)小時(shí),總算是定位到了。

        這個(gè)問題定位有幾個(gè)難點(diǎn):

        1、使用 C++

        2、使用 O2 優(yōu)化,而使用 O0 的方式問題不復(fù)現(xiàn)了(最蛋疼)

        3、使用了 map 庫函數(shù)

        因此在復(fù)現(xiàn)率很高的情況下,還是花了這么多時(shí)間。

        但好在順利解決了(這么高的復(fù)現(xiàn)率,定位root case只是時(shí)間問題,信心也是 100%)。

        簡(jiǎn)單來說,是以前的一段代碼在使用 sprintf 時(shí)(這里強(qiáng)烈建議用 snprintf),導(dǎo)致棧緩存空間越界,然后導(dǎo)致上一層函數(shù)的局部變量被篡改,而這個(gè)局部變量會(huì)導(dǎo)致 map 傳入的參數(shù)有問題,最終導(dǎo)致了 hardfault 。

        可以看到,雖然根因在一個(gè)函數(shù)中,但最終出現(xiàn)問題可能在另一個(gè)函數(shù)中。

        就像犯罪現(xiàn)場(chǎng),作案現(xiàn)場(chǎng)只有一個(gè)(root case),但可能案發(fā)現(xiàn)場(chǎng)并不是作案現(xiàn)場(chǎng)。

        因此解決 bug 過程其實(shí)就是警察破案,通過蛛絲馬跡找到第一作案現(xiàn)場(chǎng),如此才能正確破案。

        而這種代碼在工程里面有好幾處.....并且在合入我的代碼之前,運(yùn)行良好。所以,數(shù)組越界也不一定會(huì) hardfault,就看你破壞的是啥了。

        為什么?

        大家很奇怪,為毛數(shù)據(jù)越界大部分情況下會(huì) hardfault,有時(shí)卻不會(huì)產(chǎn)生問題。只有思考到更深層的原因,你才能在 BUG 環(huán)繞中有所成長。

        這個(gè)時(shí)候,就看你的基礎(chǔ)扎實(shí)不扎實(shí)了。

        這里來個(gè)簡(jiǎn)單示意函數(shù)(優(yōu)化O0)













        void func2(){  int i = 0;  int buff[4];    buff[4] = 0; }void func1(){  int j = 0; // 假設(shè)該局部變量使用 r4  func2();}

        棧空間如下(因?yàn)橹挥?4 個(gè)字,編譯器可能 buff[4] 直接使用寄存器了,但為了簡(jiǎn)單說明,這里假設(shè) buff 都使用了棧):

        圖片

        從上圖我們可以知道,進(jìn)入 func2 函數(shù)時(shí),先 push,離開時(shí) pop。

        局部變量 i 使用 r4 寄存器,但是棧空間 r4 保存的是 func1 使用的 j 的值

        因此,當(dāng)我們數(shù)組越界時(shí)(一般越界是往高地址,因?yàn)閿?shù)組索引一般是自加),很容易破壞上一個(gè)函數(shù)的棧空間,在這里破壞的是 j 的值。如果 j 很重要,那么很可能會(huì)導(dǎo)致 hardfault 或者其它問題(能引起 hardfault 反而是好事)。

        并且這里面還有重要的返回地址 lr,如果這個(gè)值被越界破壞,那么大概率都是hardfault,因?yàn)槟闫髨D跳轉(zhuǎn)到一個(gè)不存在的地址執(zhí)行。

        數(shù)組越界是一個(gè)很危險(xiǎn)的 BUG,能觀察到現(xiàn)象還好,萬一是默默破壞而不能很快被察覺,成為一個(gè)隱藏 BUG,那才是最危險(xiǎn)的。

        那為啥問題三增加別的代碼會(huì)觸發(fā)這個(gè) BUG ,修改優(yōu)化等級(jí)又會(huì)消失呢?

        這和編譯器有關(guān)系,有可能你的代碼導(dǎo)致有問題的代碼使用了不同的內(nèi)存布局,從而越界篡改的位置變成了重要的內(nèi)存,因此出現(xiàn)了現(xiàn)象,而優(yōu)化等級(jí)對(duì)棧內(nèi)存布局更是有很大影響。

        另外本篇筆記介紹的局部緩存數(shù)組的越界,實(shí)際上還有全局?jǐn)?shù)組的越界,那種問題相對(duì)簡(jiǎn)單許多,看 map 文件即可。

        因此,操作數(shù)組時(shí),一定要時(shí)時(shí)刻刻檢測(cè)數(shù)組的索引的大小,以防越界。

        創(chuàng)作不易,如果對(duì)你有幫助,歡迎一鍵三連支持魚鷹,這樣魚鷹就更有動(dòng)力更新啦。


        *博客內(nèi)容為網(wǎng)友個(gè)人發(fā)布,僅代表博主個(gè)人觀點(diǎn),如有侵權(quán)請(qǐng)聯(lián)系工作人員刪除。



        關(guān)鍵詞: 單片機(jī)

        相關(guān)推薦

        技術(shù)專區(qū)

        關(guān)閉
        主站蜘蛛池模板: 炉霍县| 云阳县| 河北区| 咸丰县| 霍邱县| 潞西市| 延庆县| 平和县| 汉源县| 常宁市| 汕尾市| 东源县| 登封市| 永登县| 揭东县| 长汀县| 垣曲县| 青铜峡市| 视频| 惠来县| 天台县| 府谷县| 陆川县| 峡江县| 伽师县| 内乡县| 涿州市| 开远市| 宜宾市| 耿马| 临武县| 崇信县| 太康县| 万山特区| 屏东市| 霍邱县| 商河县| 正阳县| 蕉岭县| 梅州市| 宜宾县|