匯編實戰開發筆記】從匯編代碼中找出一段普通的for循環變成“死循環”的根本原因

      網友投稿 1034 2022-05-29

      【匯編實戰開發筆記】從匯編代碼中找出一段普通的for循環變成“死循環”的根本原因

      1 前言

      在我的上一篇文章中,有講到掌握匯編知識的重要性,關鍵時刻可能還會拯救你于泥潭之中。

      那么,本篇文章,我將再介紹一個使用匯編知識排查疑難問題的方法,希望對大家有所幫助。

      2 問題描述

      問題是這樣的,前一段時間我們項目組在進行一項自測試中,偶然發現我們的代碼好像掛了一樣:現象就是命令行輸入不了,但是沒有看到復位信息輸出。

      當時,我們一個小伙伴說:“好像我們的系統掛了?”當我了解到這個現象之后,根據我之前的排查經驗,我當即得出了一個結論:“可能是我們的代碼跑進死循環了,好好檢查下”!

      于是,我們開始debug代碼,加了一些必要的調試信息,最終發現有一個計算校驗的函數,調進去了但是沒有退出來,而這個校驗的函數非常之簡單,它就長這樣:

      uint16_t checksum(uint8_t *data, uint8_t len) { uint8_t i; uint16_t sum = 0, res; for (i = 0; i < len; i++) { sum += data[i]; } res = sum ; return res; }

      我想當你看到這段函數時,肯定也是:“臥槽,這TM不就是算累加校驗和嗎?怎么可能會死循環?”

      沒錯,當時我們的爭論的場景也的確如此!

      3 簡單分析

      這個checksum函數真的是非常簡單,入參簡單、實現也簡單、返回值也簡單,根本不存在難點。

      一步步來分析:

      既然代碼沒有崩潰,證明data指針肯定非NULL的,不會有問題;

      倒是這個len有些可疑,len的類型是uint8_t無符號的,它的范圍是0-255;但是如果外面傳入的是-1呢?

      如果傳入-1,強制轉換為uint8_t,其值也是255,那么下面的for循環,依然只會跑256次,它必須得退出呀?

      有沒有可能for循環的過程中,棧的值被修改了,然后i的值和len的值都變了,進而for的次數改變了?

      于是我們開始打印i和len的值,發現他們兩個的值,都是正常變化的,并不是剛剛想的那樣。

      這就很奇怪了!!!

      如果說這個for循環要“無限”循環下去,造成“死循環”,必須滿足的條件是len很大很大,但是len不是uint8_t類型嘛?最大也就255呀?

      printf大法再來一遍:結果出乎我們的意料,請看:

      log輸出:

      [12-21 19:45:38]checksum 128 len: 4294967295 [12-21 19:45:38]0 4294967295 [12-21 19:45:38]1 4294967295 [12-21 19:45:38]2 4294967295 [12-21 19:45:38]3 4294967295 [12-21 19:45:38]4 4294967295 [12-21 19:45:38]5 4294967295 [12-21 19:45:38]6 4294967295 [12-21 19:45:38]7 4294967295 [12-21 19:45:38]8 4294967295 [12-21 19:45:38]9 4294967295 [12-21 19:45:38]10 4294967295 。。。省略很多 [12-21 19:45:38]250 4294967295 [12-21 19:45:38]251 4294967295 [12-21 19:45:38]252 4294967295 [12-21 19:45:38]253 4294967295 [12-21 19:45:38]254 4294967295 [12-21 19:45:38]255 4294967295 [12-21 19:45:38]256 4294967295 [12-21 19:45:38]257 4294967295 [12-21 19:45:38]258 4294967295 [12-21 19:45:38]259 4294967295 [12-21 19:45:38]260 4294967295 。。。還在不停的打印

      看到這里似乎有點眉目了?len的值為4294967295?

      這個值不是0xFFFFFFFF嗎?

      我們再使用**%d**打印了一下len,發現值為-1。

      回過頭來看下checksum的調用之處:

      uint16_t res = checksum(&data[0], len - 1);

      看似真相了,當len為0的時候,傳入的值不就是-1嗎?

      好像是這么回事,但是-1進去,它是uint8_t的呀,頂多就是255啊?怎么變成了4294967295? 到底是誰干的啊?

      同時也發現關鍵問題了,這里并不是真正意義的**“死循環”**,而是for循環執行太久了,導致長時間無法結束,因為我們的主頻才160MHZ,CPU就是猛跑,從1加到0xFFFFFFFF,也需要好長一段時間呢!

      4 場景再現

      為了充分說明這個問題,我盡可能地還原下當時我們的代碼場景:

      /* 一個結構體定義數據 不要急于吐槽它的定義,這代碼是開源的,冤有頭。。。 還有不要懷疑是字節對齊不對齊的問題,曾經我也懷疑過,最后知道真相的時候,我被打臉了! */ typedef struct _data_t { /* result, final result */ uint8_t len; uint8_t flag; uint8_t passwd_len; uint8_t *passwd; uint8_t ssid_len; uint8_t *ssid; uint8_t token_len; uint8_t *token; uint8_t bssid_type_len; uint8_t *bssid; uint8_t ssid_is_gbk; uint8_t ssid_auto_complete_disable; uint8_t data[127]; uint8_t checksum; } data_t;

      /* 1.c 調用checksum的C文件 */ /* 定義全局的數據 */ static data_t g_data; /* 設置全局的數據 */ void set_global_data(void) { g_data.len = 0; } void handle_global_data(void) { uint16_t res = checksum(&g_data.data[0], g_data.len - 0); //sometimes no return form checksum } void test_func_entry(void) { set_global_data(); handle_global_data(); }

      /* 2.c 定義checksum函數的工具類 */ uint16_t checksum(uint8_t *data, uint8_t len) { uint8_t i; uint16_t sum = 0, res; for (i = 0; i < len; i++) { sum += data[i]; } res = sum ; return res; }

      在我的第一次認知里,還是len=-1=255的情況,由于g_data.data只有127字節,但它最后是可以訪問到255下標的,其實這本身就有數據非法訪問的問題;但是經過仔細論證,得出的結論是,這并不會導致死循環,或者說并不會改變len的值;因為checksum里面知識讀取data指針的值,并沒改變它的值,即便越界了,頂多訪問了別人,并不會出啥異常(至少在我們的處理器平臺是這樣)。

      這個問題對我們來說,真的是百思不得其解,為了規避掉這個問題,我們在調用checksum的時候做了判斷,但len為0的時候直接不調用,也就繞過了這個問題。

      但是作為一個深挖底層邏輯的攻城獅來說,我們不應該放過這樣的細節,或許還有什么我們未發現的潛在風險呢?

      這個問題一直困擾著我,時不時有空的時候,我就會想想,到底還有什么情況還會導致這個現象?

      5 柳暗花明

      偶然有一天,我正瀏覽到一篇關于編譯器做代碼優化的文章,它是在知乎上發出來的,我看到其中一個重要線索:

      突然我腦子里,閃過一個疑問:“會不會那段for循環的checksum函數,正是因為調用方沒有申明checksum函數,也就是說沒有include對應的頭文件導致編譯器做了默認處理呢?”?

      我們都知道,在使用gcc編譯器編譯C代碼時,如果一個函數未申明就調用,是會報一個警告的:“warning: implicit declaration of function ‘checksum’ [-Wimplicit-function-declaration]”!

      同時,尤其編譯器不知道被調用函數的原型,那么它只能依靠你的調用代碼結合一些默認值做假設:

      比如我們的調用代碼是:

      uint16_t res = checksum(&g_data.data[0], g_data.len - 0);

      這里,我猜測編譯器的行為就是,你有一個叫checksum的函數,但我找不到它的原型,那么我就按**“返回值是uint16_t類型,第一個參數是int型,第二個參數也是int型”**來吧!

      為何,gcc默認參數列表都是int類型?這是我的假想猜測,下面我們再論證,究竟是不是這樣?

      有了這個假設之后,我們回到ARM匯編在函數調用時的參數,這時R0應該等于&g_data.data[0],R1應該等于-1。

      由于R0/R1都是32位的寄存器,在存儲數據的時候,無所謂有符號和無符號一說,且本問題R0沒有出現問題,我們僅討論R1。

      這個時候R1的寄存器值,應該是**“-1 = 0xFFFFFFFF”**,這個假設很關鍵,如果分析地很順利,那么這個for循環不停地循環下去,才可以有理論進行下去。

      6 找到證據

      既然上面我們發現了端倪,那么我們應該進一步找到相關的證據,證明我們的想法;同時,如果這個問題根源在于include頭文件,那么當我們添加了頭文件之后,這個問題應該不會再復現。我們來看下,究竟是不是這樣?

      6.1 究竟是不是警告

      由于我們的代碼實在太多警告了,就屬于那種 0 error N warnings 那種,屬于你需要找一個警告往往好費好大勁!

      經過好一番檢索,果不其然,還真的報警告了,的確是**“warning: implicit declaration of function ‘checksum’ [-Wimplicit-function-declaration]”**!

      6.2 盤根問底

      看編譯器的行為,我們肯定是要看其對應的匯編文件,這里有兩個地方需要看,一個是checksum函數的匯編,還有一個調用checksum函數附近的匯編。

      我們一起看看:

      /* checksum 函數的匯編代碼 */ .section .text.checksum,"ax",%progbits .align 1 .global checksum .code 16 .thumb_func .type checksum, %function checksum: .LFB4: .loc 1 125 0 .cfi_startproc @ args = 0, pretend = 0, frame = 0 @ frame_needed = 0, uses_anonymous_args = 0 .LVL27: push {r4, r5, r6, lr} .cfi_def_cfa_offset 16 .cfi_offset 4, -16 .cfi_offset 5, -12 .cfi_offset 6, -8 .cfi_offset 14, -4 .loc 1 125 0 movs r4, r0 movs r5, r1 // r1 -> r5 ,即 len的值存在r5中 .loc 1 129 0 movs r2, r1 ldr r0, .L29 .LVL28: bl printf //打印len的值 .LVL29: movs r3, r4 .loc 1 127 0 movs r0, #0 adds r5, r4, r5 .LVL30: .L26: .loc 1 130 0 discriminator 1 cmp r3, r5 //for循環里面的關鍵判斷,即 i < len beq .L28 // 退出for循環 .loc 1 131 0 discriminator 3 //下面就是for循環的循環執行體 ldrb r2, [r3] adds r3, r3, #1 .LVL31: adds r0, r0, r2 .LVL32: lsls r0, r0, #16 lsrs r0, r0, #16 .LVL33: b .L26 .LVL34: .L28: .loc 1 136 0 @ sp needed .LVL35: pop {r4, r5, r6, pc} .L30: .align 2 .L29: .word .LC12 .cfi_endproc .LFE4: .size checksum, .-checksum

      由它的匯編代碼可知,for循環執行多少次,關鍵在于r5寄存器的值,也就是len的值。

      注意在匯編代碼這里,是看不到r5是uint8_t還是uint32_t的,它僅僅是一個32位的寄存器。

      .section .text.verify_checksum,"ax",%progbits .align 1 .global verify_checksum .code 16 .thumb_func .type verify_checksum, %functionverify_checksum:.LFB5: .loc 1 81 0 .cfi_startproc @ args = 0, pretend = 0, frame = 0 @ frame_needed = 0, uses_anonymous_args = 0.LVL17: push {r4, lr} .cfi_def_cfa_offset 8 .cfi_offset 4, -8 .cfi_offset 14, -4 .loc 1 83 0 ldr r4, .L20 .loc 1 91 0 @ sp needed .loc 1 83 0 movs r0, r4 //r0存儲結構體g_data的地址 ldrb r1, [r4] //將g_data的第一個字節,即g_data.len賦值為r1 adds r0, r0, #34 //r0的地址偏移34個字節,即偏移到g_data.data的位置; subs r1, r1, #1 //關鍵的一步:r1 = r1 - 1 由于我們復現問題的時候,g_data.len是為0的,所以此時r1的值就是0xFFFFFFFF bl checksum //調用checksum函數,第1-2個入參,分別是r0和r1.LVL18: .loc 1 84 0 adds r4, r4, #160 .loc 1 89 0 ldrb r3, [r4] lsls r0, r0, #24.LVL19: lsrs r0, r0, #24 subs r0, r0, r3 .loc 1 91 0 pop {r4, pc}.L21: .align 2.L20: .word .LANCHOR4 .cfi_endproc.LFE5: .size verify_checksum, .-verify_checksum

      了解匯編知識的,看到上面的匯編代碼,結合checksum函數的匯編代碼,就應該明白,我前面的假設成立了,但len傳入到checksum函數時,它的值真的是0xFFFFFFFF,而使用%u打印出來,就是4294967295。

      到此,罪魁禍首其實已經找到了,與其說是編譯器的無故優化,倒不如說是程序猿寫代碼不嚴謹,沒有正確處理掉這個編譯警告。

      6.3 解除風險

      既然找到了問題根源,那么我們嘗試下解除這個風險。

      方法其實也很簡單,直接需要在調用checksum函數的1.c中,include一下checksum函數所在的頭文件即可。

      添加之后,我們看下發生的變化,很顯然,checksum函數的匯編代碼肯定是沒有任何不變的,應該它壓根沒有改;

      而調用checksum的匯編就發生了些許的變化,同時編譯輸出的地方,那個編譯警告也都消失了。

      /* 添加頭文件之后的匯編代碼 */ .section .text.verify_checksum,"ax",%progbits .align 1 .global verify_checksum .code 16 .thumb_func .type verify_checksum, %functionverify_checksum:.LFB5: .loc 1 81 0 .cfi_startproc @ args = 0, pretend = 0, frame = 0 @ frame_needed = 0, uses_anonymous_args = 0.LVL17: push {r4, lr} .cfi_def_cfa_offset 8 .cfi_offset 4, -8 .cfi_offset 14, -4 .loc 1 83 0 ldr r4, .L20 .loc 1 91 0 @ sp needed .loc 1 83 0 movs r0, r4 ldrb r1, [r4] adds r0, r0, #34 subs r1, r1, #1 //r1寄存器的一樣的操作 r1 = r1 - 1 lsls r1, r1, #24 //關鍵改變!!! r1 = r1 * (2的24次冪),也就是算術左移24位 lsrs r1, r1, #24 //關鍵改變!!! r1 = r1 / (2的24次冪),也就是算術右移24位 bl checksum.LVL18: .loc 1 84 0 adds r4, r4, #160 .loc 1 89 0 ldrb r3, [r4] lsls r0, r0, #24.LVL19: lsrs r0, r0, #24 subs r0, r0, r3 .loc 1 91 0 pop {r4, pc}.L21: .align 2.L20: .word .LANCHOR4 .cfi_endproc.LFE5: .size verify_checksum, .-verify_checksum

      為了好對比,我直接使用對比工具貼圖上來看下:

      查了下多出來的這兩條指令:lsls和lsrs,參考這里。

      一個是算術左移24位,一個是算術右移24位,倒來倒去,無非就是把高24位給情況,這樣-1的值傳入checksum的時候,就只有0x000000FF了,而不是0xFFFFFFFF。

      這樣就把uint8_t len拉回正常的邏輯了,自然也就不會出現之前的for循環一直退不出來了。

      7 擴展延伸

      上面我提及的場景對應的是ARM平臺的,由于我們的代碼是跨平臺的,支持RISC-V架構,X86架構等等。

      7.1 RISC-V架構

      所以我們來對比看下RISC-V架構下的情況:

      這么看,RISC-V的處理也是夠粗暴的,一個addi指令,把高24位去掉就完事了!!!

      7.2 80x86架構

      我push了一個簡易的工程代碼到github,以便于重現此問題,感興趣的可以看這里。

      很遺憾的是,在80x86上竟然沒有復現此問題。

      代碼的核心差別就是是否include 2.h:

      匯編代碼確實有差異:

      但是跑出來的效果確實一樣的:

      總結下沒有復現問題的原因,可能是:

      【匯編實戰開發筆記】從匯編代碼中找出一段普通的for循環變成“死循環”的根本原因

      編譯選項沒有使用正確?

      80x86編譯器更懂事?更能知道如何合理編譯代碼?

      還有未知的編譯特性未了解到?

      7.3 其他架構

      感興趣的可以在其他平臺上驗證下,是否有類似的問題,歡迎討論。

      8 經驗總結

      請提升你的代碼編譯嚴謹性,如果是gcc編譯器,-Wall -Werror -Os是最低要求;

      談優化代碼前,請close掉你的代碼編譯異常,先達到 0 error 0 warning 再說;

      請重視warning: implicit declaration of function這個編譯警告;

      如果使用gcc編譯器,不提示任何編譯警告和錯誤,并不代表編譯器沒有告訴你,也許是你使用-w選項編譯了輸出,你僅僅是在自欺欺人而已;

      老老實實在調用函數前申明你的函數,或者包含其對應的頭文件,有時候編譯器的默認行文不見得就可靠;

      代碼細節很重要,真的是細節決定成敗;

      不放過一絲可能性,作為一個攻城獅,這點專研精神需要時刻掛在心里;

      大膽假設,小心求證,亙古不變的方法論。

      9 更多分享

      歡迎關注我的github倉庫01workstation,日常分享一些開發筆記和項目實戰,歡迎指正問題。

      同時也非常歡迎關注我的CSDN主頁和專欄:

      【CSDN主頁:架構師李肯】

      【RT-Thread主頁:架構師李肯】

      【C/C++語言編程專欄】

      【GCC專欄】

      【信息安全專欄】

      【RT-Thread開發筆記】

      【freeRTOS開發筆記】

      【BLE藍牙開發筆記】

      【ARM開發筆記】

      【RISC-V開發筆記】

      有問題的話,可以跟我討論,知無不答,謝謝大家。

      匯編語言

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:Windows設備信息獲取:(攝像頭,聲卡為例)Qt,WindowsAPI對比說明(1)
      下一篇:計算機網絡面試題整理
      相關文章
      亚洲精品二区国产综合野狼| 亚洲av午夜精品无码专区| 国产亚洲人成在线播放| 亚洲三级视频在线| 亚洲AV日韩AV永久无码绿巨人| 亚洲AV无码专区亚洲AV桃| 亚洲精品无播放器在线播放 | 国产亚洲精品va在线| 亚洲a∨无码男人的天堂| 国产亚洲A∨片在线观看| 老牛精品亚洲成av人片| 国产精品亚洲专区一区| 亚洲AV无码国产剧情| 亚洲av永久无码精品秋霞电影秋| 日韩国产精品亚洲а∨天堂免| 日本系列1页亚洲系列| 日韩国产精品亚洲а∨天堂免| 国产成人亚洲毛片| 亚洲AV永久无码精品放毛片| 亚洲乱码无人区卡1卡2卡3| 亚洲日韩一区精品射精| 国产精品高清视亚洲一区二区| 亚洲精品国产av成拍色拍| 亚洲av永久无码精品秋霞电影秋| 亚洲综合无码一区二区痴汉| 亚洲中文字幕无码久久| 亚洲爆乳少妇无码激情| 国产精品亚洲av色欲三区| 亚洲精品久久久www| 国产亚洲精久久久久久无码77777| 亚洲精品自在在线观看| 色噜噜综合亚洲av中文无码| 久久亚洲伊人中字综合精品| 亚洲最新永久在线观看| 亚洲av无码不卡久久| 亚洲av综合日韩| 亚洲第一页综合图片自拍| 国产亚洲精品国看不卡| 国产AV无码专区亚洲AVJULIA | 国产中文在线亚洲精品官网| 亚洲精品乱码久久久久久蜜桃不卡 |