1 前言
在我的上一篇文章中,有講到掌握匯編知識(shí)的重要性,關(guān)鍵時(shí)刻可能還會(huì)拯救你于泥潭之中。
那么,本篇文章,我將再介紹一個(gè)使用匯編知識(shí)排查疑難問(wèn)題的方法,希望對(duì)大家有所幫助。
2 問(wèn)題描述
問(wèn)題是這樣的,前一段時(shí)間我們項(xiàng)目組在進(jìn)行一項(xiàng)自測(cè)試中,偶然發(fā)現(xiàn)我們的代碼好像掛了一樣:現(xiàn)象就是命令行輸入不了,但是沒(méi)有看到復(fù)位信息輸出。
當(dāng)時(shí),我們一個(gè)小伙伴說(shuō):“好像我們的系統(tǒng)掛了?”當(dāng)我了解到這個(gè)現(xiàn)象之后,根據(jù)我之前的排查經(jīng)驗(yàn),我當(dāng)即得出了一個(gè)結(jié)論:“可能是我們的代碼跑進(jìn)死循環(huán)了,好好檢查下”!
于是,我們開(kāi)始debug代碼,加了一些必要的調(diào)試信息,最終發(fā)現(xiàn)有一個(gè)計(jì)算校驗(yàn)的函數(shù),調(diào)進(jìn)去了但是沒(méi)有退出來(lái),而這個(gè)校驗(yàn)的函數(shù)非常之簡(jiǎn)單,它就長(zhǎng)這樣:
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;
}
我想當(dāng)你看到這段函數(shù)時(shí),肯定也是:“臥槽,這TM不就是算累加校驗(yàn)和嗎?怎么可能會(huì)死循環(huán)?”
沒(méi)錯(cuò),當(dāng)時(shí)我們的爭(zhēng)論的場(chǎng)景也的確如此!
3 簡(jiǎn)單分析
這個(gè)checksum函數(shù)真的是非常簡(jiǎn)單,入?yún)⒑?jiǎn)單、實(shí)現(xiàn)也簡(jiǎn)單、返回值也簡(jiǎn)單,根本不存在難點(diǎn)。
一步步來(lái)分析:
既然代碼沒(méi)有崩潰,證明data指針肯定非NULL的,不會(huì)有問(wèn)題;
倒是這個(gè)len有些可疑,len的類型是uint8_t無(wú)符號(hào)的,它的范圍是0-255;但是如果外面?zhèn)魅氲氖?1呢?
如果傳入-1,強(qiáng)制轉(zhuǎn)換為uint8_t,其值也是255,那么下面的for循環(huán),依然只會(huì)跑256次,它必須得退出呀?
有沒(méi)有可能for循環(huán)的過(guò)程中,棧的值被修改了,然后i的值和len的值都變了,進(jìn)而for的次數(shù)改變了?
于是我們開(kāi)始打印i和len的值,發(fā)現(xiàn)他們兩個(gè)的值,都是正常變化的,并不是剛剛想的那樣。
這就很奇怪了?。?!
如果說(shuō)這個(gè)for循環(huán)要“無(wú)限”循環(huán)下去,造成“死循環(huán)”,必須滿足的條件是len很大很大,但是len不是uint8_t類型嘛?最大也就255呀?
printf大法再來(lái)一遍:結(jié)果出乎我們的意料,請(qǐng)看:
log輸出:
[12-21 1938]checksum 128 len: 4294967295
[12-21 1938]0 4294967295
[12-21 1938]1 4294967295
[12-21 1938]2 4294967295
[12-21 1938]3 4294967295
[12-21 1938]4 4294967295
[12-21 1938]5 4294967295
[12-21 1938]6 4294967295
[12-21 1938]7 4294967295
[12-21 1938]8 4294967295
[12-21 1938]9 4294967295
[12-21 1938]10 4294967295
。。。省略很多
[12-21 1938]250 4294967295
[12-21 1938]251 4294967295
[12-21 1938]252 4294967295
[12-21 1938]253 4294967295
[12-21 1938]254 4294967295
[12-21 1938]255 4294967295
[12-21 1938]256 4294967295
[12-21 1938]257 4294967295
[12-21 1938]258 4294967295
[12-21 1938]259 4294967295
[12-21 1938]260 4294967295
。。。還在不停的打印
看到這里似乎有點(diǎn)眉目了?len的值為4294967295?
這個(gè)值不是0xFFFFFFFF嗎?
我們?cè)偈褂?d打印了一下len,發(fā)現(xiàn)值為-1。
回過(guò)頭來(lái)看下checksum的調(diào)用之處:
uint16_t res = checksum(&data[0], len - 1);
看似真相了,當(dāng)len為0的時(shí)候,傳入的值不就是-1嗎?
好像是這么回事,但是-1進(jìn)去,它是uint8_t的呀,頂多就是255啊?怎么變成了4294967295? 到底是誰(shuí)干的???
同時(shí)也發(fā)現(xiàn)關(guān)鍵問(wèn)題了,這里并不是真正意義的“死循環(huán)”,而是for循環(huán)執(zhí)行太久了,導(dǎo)致長(zhǎng)時(shí)間無(wú)法結(jié)束,因?yàn)槲覀兊闹黝l才160MHZ,CPU就是猛跑,從1加到0xFFFFFFFF,也需要好長(zhǎng)一段時(shí)間呢!
4 場(chǎng)景再現(xiàn)
為了充分說(shuō)明這個(gè)問(wèn)題,我盡可能地還原下當(dāng)時(shí)我們的代碼場(chǎng)景:
/*
一個(gè)結(jié)構(gòu)體定義數(shù)據(jù)
不要急于吐槽它的定義,這代碼是開(kāi)源的,冤有頭。。。
還有不要懷疑是字節(jié)對(duì)齊不對(duì)齊的問(wèn)題,曾經(jīng)我也懷疑過(guò),最后知道真相的時(shí)候,我被打臉了!
*/
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 調(diào)用checksum的C文件 */
/* 定義全局的數(shù)據(jù) */
static data_t g_data;
/* 設(shè)置全局的數(shù)據(jù) */
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函數(shù)的工具類 */
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;
}
在我的第一次認(rèn)知里,還是len=-1=255的情況,由于g_data.data只有127字節(jié),但它最后是可以訪問(wèn)到255下標(biāo)的,其實(shí)這本身就有數(shù)據(jù)非法訪問(wèn)的問(wèn)題;但是經(jīng)過(guò)仔細(xì)論證,得出的結(jié)論是,這并不會(huì)導(dǎo)致死循環(huán),或者說(shuō)并不會(huì)改變len的值;因?yàn)閏hecksum里面知識(shí)讀取data指針的值,并沒(méi)改變它的值,即便越界了,頂多訪問(wèn)了別人,并不會(huì)出啥異常(至少在我們的處理器平臺(tái)是這樣)。
這個(gè)問(wèn)題對(duì)我們來(lái)說(shuō),真的是百思不得其解,為了規(guī)避掉這個(gè)問(wèn)題,我們?cè)谡{(diào)用checksum的時(shí)候做了判斷,但len為0的時(shí)候直接不調(diào)用,也就繞過(guò)了這個(gè)問(wèn)題。
但是作為一個(gè)深挖底層邏輯的攻城獅來(lái)說(shuō),我們不應(yīng)該放過(guò)這樣的細(xì)節(jié),或許還有什么我們未發(fā)現(xiàn)的潛在風(fēng)險(xiǎn)呢?
這個(gè)問(wèn)題一直困擾著我,時(shí)不時(shí)有空的時(shí)候,我就會(huì)想想,到底還有什么情況還會(huì)導(dǎo)致這個(gè)現(xiàn)象?
5 柳暗花明
偶然有一天,我正瀏覽到一篇關(guān)于編譯器做代碼優(yōu)化的文章,它是在知乎上發(fā)出來(lái)的,我看到其中一個(gè)重要線索:
突然我腦子里,閃過(guò)一個(gè)疑問(wèn):“會(huì)不會(huì)那段for循環(huán)的checksum函數(shù),正是因?yàn)檎{(diào)用方?jīng)]有申明checksum函數(shù),也就是說(shuō)沒(méi)有include對(duì)應(yīng)的頭文件導(dǎo)致編譯器做了默認(rèn)處理呢?”?
我們都知道,在使用gcc編譯器編譯C代碼時(shí),如果一個(gè)函數(shù)未申明就調(diào)用,是會(huì)報(bào)一個(gè)警告的:“warning: implicit declaration of function ‘checksum’ [-Wimplicit-function-declaration]”!
同時(shí),尤其編譯器不知道被調(diào)用函數(shù)的原型,那么它只能依靠你的調(diào)用代碼結(jié)合一些默認(rèn)值做假設(shè):
比如我們的調(diào)用代碼是:
uint16_t res = checksum(&g_data.data[0], g_data.len - 0);
這里,我猜測(cè)編譯器的行為就是,你有一個(gè)叫checksum的函數(shù),但我找不到它的原型,那么我就按“返回值是uint16_t類型,第一個(gè)參數(shù)是int型,第二個(gè)參數(shù)也是int型”來(lái)吧!
為何,gcc默認(rèn)參數(shù)列表都是int類型?這是我的假想猜測(cè),下面我們?cè)僬撟C,究竟是不是這樣?
有了這個(gè)假設(shè)之后,我們回到ARM匯編在函數(shù)調(diào)用時(shí)的參數(shù),這時(shí)R0應(yīng)該等于&g_data.data[0],R1應(yīng)該等于-1。
由于R0/R1都是32位的寄存器,在存儲(chǔ)數(shù)據(jù)的時(shí)候,無(wú)所謂有符號(hào)和無(wú)符號(hào)一說(shuō),且本問(wèn)題R0沒(méi)有出現(xiàn)問(wèn)題,我們僅討論R1。
這個(gè)時(shí)候R1的寄存器值,應(yīng)該是“-1 = 0xFFFFFFFF”,這個(gè)假設(shè)很關(guān)鍵,如果分析地很順利,那么這個(gè)for循環(huán)不停地循環(huán)下去,才可以有理論進(jìn)行下去。
6 找到證據(jù)
既然上面我們發(fā)現(xiàn)了端倪,那么我們應(yīng)該進(jìn)一步找到相關(guān)的證據(jù),證明我們的想法;同時(shí),如果這個(gè)問(wèn)題根源在于include頭文件,那么當(dāng)我們添加了頭文件之后,這個(gè)問(wèn)題應(yīng)該不會(huì)再?gòu)?fù)現(xiàn)。我們來(lái)看下,究竟是不是這樣?
6.1 究竟是不是警告
由于我們的代碼實(shí)在太多警告了,就屬于那種 0 error N warnings 那種,屬于你需要找一個(gè)警告往往好費(fèi)好大勁!
經(jīng)過(guò)好一番檢索,果不其然,還真的報(bào)警告了,的確是“warning: implicit declaration of function ‘checksum’ [-Wimplicit-function-declaration]”!
6.2 盤根問(wèn)底
看編譯器的行為,我們肯定是要看其對(duì)應(yīng)的匯編文件,這里有兩個(gè)地方需要看,一個(gè)是checksum函數(shù)的匯編,還有一個(gè)調(diào)用checksum函數(shù)附近的匯編。
我們一起看看:
/* checksum 函數(shù)的匯編代碼 */
.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循環(huán)里面的關(guān)鍵判斷,即 i < len
beq .L28 // 退出for循環(huán)
.loc 1 131 0 discriminator 3 //下面就是for循環(huán)的循環(huán)執(zhí)行體
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循環(huán)執(zhí)行多少次,關(guān)鍵在于r5寄存器的值,也就是len的值。
注意在匯編代碼這里,是看不到r5是uint8_t還是uint32_t的,它僅僅是一個(gè)32位的寄存器。
.section .text.verify_checksum,"ax",%progbits
.align 1
.global verify_checksum
.code 16
.thumb_func
.type verify_checksum, %function
verify_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存儲(chǔ)結(jié)構(gòu)體g_data的地址
ldrb r1, [r4] //將g_data的第一個(gè)字節(jié),即g_data.len賦值為r1
adds r0, r0, #34 //r0的地址偏移34個(gè)字節(jié),即偏移到g_data.data的位置;
subs r1, r1, #1 //關(guān)鍵的一步:r1 = r1 - 1 由于我們復(fù)現(xiàn)問(wèn)題的時(shí)候,g_data.len是為0的,所以此時(shí)r1的值就是0xFFFFFFFF
bl checksum //調(diào)用checksum函數(shù),第1-2個(gè)入?yún)?,分別是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
了解匯編知識(shí)的,看到上面的匯編代碼,結(jié)合checksum函數(shù)的匯編代碼,就應(yīng)該明白,我前面的假設(shè)成立了,但len傳入到checksum函數(shù)時(shí),它的值真的是0xFFFFFFFF,而使用%u打印出來(lái),就是4294967295。
到此,罪魁禍?zhǔn)灼鋵?shí)已經(jīng)找到了,與其說(shuō)是編譯器的無(wú)故優(yōu)化,倒不如說(shuō)是程序猿寫(xiě)代碼不嚴(yán)謹(jǐn),沒(méi)有正確處理掉這個(gè)編譯警告。
6.3 解除風(fēng)險(xiǎn)
既然找到了問(wèn)題根源,那么我們嘗試下解除這個(gè)風(fēng)險(xiǎn)。
方法其實(shí)也很簡(jiǎn)單,直接需要在調(diào)用checksum函數(shù)的1.c中,include一下checksum函數(shù)所在的頭文件即可。
添加之后,我們看下發(fā)生的變化,很顯然,checksum函數(shù)的匯編代碼肯定是沒(méi)有任何不變的,應(yīng)該它壓根沒(méi)有改;
而調(diào)用checksum的匯編就發(fā)生了些許的變化,同時(shí)編譯輸出的地方,那個(gè)編譯警告也都消失了。
* 添加頭文件之后的匯編代碼 */
.section .text.verify_checksum,"ax",%progbits
.align 1
.global verify_checksum
.code 16
.thumb_func
.type verify_checksum, %function
verify_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 //關(guān)鍵改變?。。1 = r1 * (2的24次冪),也就是算術(shù)左移24位
lsrs r1, r1, #24 //關(guān)鍵改變!?。1 = r1 / (2的24次冪),也就是算術(shù)右移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
為了好對(duì)比,我直接使用對(duì)比工具貼圖上來(lái)看下:
查了下多出來(lái)的這兩條指令:lsls和lsrs,參考這里。
一個(gè)是算術(shù)左移24位,一個(gè)是算術(shù)右移24位,倒來(lái)倒去,無(wú)非就是把高24位給情況,這樣-1的值傳入checksum的時(shí)候,就只有0x000000FF了,而不是0xFFFFFFFF。
這樣就把uint8_t len拉回正常的邏輯了,自然也就不會(huì)出現(xiàn)之前的for循環(huán)一直退不出來(lái)了。
7 擴(kuò)展延伸
上面我提及的場(chǎng)景對(duì)應(yīng)的是ARM平臺(tái)的,由于我們的代碼是跨平臺(tái)的,支持RISC-V架構(gòu),X86架構(gòu)等等。
7.1 RISC-V架構(gòu)
所以我們來(lái)對(duì)比看下RISC-V架構(gòu)下的情況:
這么看,RISC-V的處理也是夠粗暴的,一個(gè)addi指令,把高24位去掉就完事了?。?!
7.2 80x86架構(gòu)
我push了一個(gè)簡(jiǎn)易的工程代碼到github,以便于重現(xiàn)此問(wèn)題,感興趣的可以看這里。
很遺憾的是,在80x86上竟然沒(méi)有復(fù)現(xiàn)此問(wèn)題。
代碼的核心差別就是是否include 2.h:
匯編代碼確實(shí)有差異:
但是跑出來(lái)的效果確實(shí)一樣的:
總結(jié)下沒(méi)有復(fù)現(xiàn)問(wèn)題的原因,可能是:
編譯選項(xiàng)沒(méi)有使用正確?
80x86編譯器更懂事?更能知道如何合理編譯代碼?
還有未知的編譯特性未了解到?
7.3 其他架構(gòu)
感興趣的可以在其他平臺(tái)上驗(yàn)證下,是否有類似的問(wèn)題,歡迎討論。
8 經(jīng)驗(yàn)總結(jié)
請(qǐng)?zhí)嵘愕拇a編譯嚴(yán)謹(jǐn)性,如果是gcc編譯器,-Wall -Werror -Os是最低要求;
談優(yōu)化代碼前,請(qǐng)close掉你的代碼編譯異常,先達(dá)到 0 error 0 warning 再說(shuō);
請(qǐng)重視warning: implicit declaration of function這個(gè)編譯警告;
如果使用gcc編譯器,不提示任何編譯警告和錯(cuò)誤,并不代表編譯器沒(méi)有告訴你,也許是你使用-w選項(xiàng)編譯了輸出,你僅僅是在自欺欺人而已;
老老實(shí)實(shí)在調(diào)用函數(shù)前申明你的函數(shù),或者包含其對(duì)應(yīng)的頭文件,有時(shí)候編譯器的默認(rèn)行文不見(jiàn)得就可靠;
代碼細(xì)節(jié)很重要,真的是細(xì)節(jié)決定成?。?/span>
不放過(guò)一絲可能性,作為一個(gè)攻城獅,這點(diǎn)專研精神需要時(shí)刻掛在心里;
大膽假設(shè),小心求證,亙古不變的方法論。
審核編輯:湯梓紅
-
匯編
+關(guān)注
關(guān)注
2文章
214瀏覽量
26547 -
代碼
+關(guān)注
關(guān)注
30文章
4899瀏覽量
70639
原文標(biāo)題:【匯編實(shí)戰(zhàn)開(kāi)發(fā)筆記】從匯編代碼中找出一段普通的for循環(huán)變成“死循環(huán)”的根本原因
文章出處:【微信號(hào):RTThread,微信公眾號(hào):RTThread物聯(lián)網(wǎng)操作系統(tǒng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
使用匯編知識(shí)排查疑難問(wèn)題的方法
如何使用內(nèi)聯(lián)函數(shù)法調(diào)用匯編函數(shù)?
為什么要用匯編語(yǔ)言寫(xiě)程序
飛控中的疑難問(wèn)題分享
如何有效地排查內(nèi)存泄露的疑難問(wèn)題
如何在C程序中使用匯編
視頻會(huì)議系統(tǒng)中常見(jiàn)的疑難問(wèn)題解答
電腦入門與提高疑難排解大全
模擬電子疑難問(wèn)題解惑系列(三):成為熟練的電子工程師
模擬電子疑難問(wèn)題解惑系列(四):濾波器、振蕩器
關(guān)于高速AD/DAC測(cè)量及設(shè)計(jì)中82個(gè)疑難問(wèn)題的解答

51單片機(jī)C語(yǔ)言調(diào)用匯編子程序的簡(jiǎn)便方法程序和工程文件免費(fèi)下載

單片機(jī)使用匯編開(kāi)發(fā)的簡(jiǎn)單介紹

在C中使用匯編程序的原因是?

評(píng)論