你處理過(guò)哪些低級(jí)BUG?見(jiàn)笑,我耗時(shí)8小時(shí)排查低級(jí)BUG|popen內(nèi)存泄漏
最不值得一提的BUG
在我看來(lái)最不值得一提的BUG是那種可以重復(fù)復(fù)現(xiàn)的,他的穩(wěn)定復(fù)現(xiàn)通常排查起來(lái)沒(méi)啥技術(shù)含量, 早些年我處理一個(gè)不值得一提的BUG,BUG也很好復(fù)現(xiàn),難點(diǎn)是復(fù)現(xiàn)時(shí)間固定在4小時(shí)左右,BUG由于文件資源未釋放引起進(jìn)程訪(fǎng)問(wèn)文件數(shù)目受限而崩潰,早期A(yíng)ndroid系統(tǒng)用該BUG獲取到root權(quán)限, 本文向你分享,如何根據(jù)錯(cuò)誤提示和參考手冊(cè)找到故障點(diǎn),指導(dǎo)新碼農(nóng)如何正確閱讀Linux幫助手冊(cè)(man page), 最后總結(jié)我的排查過(guò)程給小白一點(diǎn)實(shí)用的建議。好下面開(kāi)始不如步入正題。需要調(diào)試的是一個(gè)監(jiān)控程序,代碼非常簡(jiǎn)單,2個(gè)線(xiàn)程執(zhí)行不同的任務(wù),每個(gè)任務(wù)都是間隔15秒執(zhí)行一次,程序固定在大約4小時(shí)后崩潰。代碼簡(jiǎn)單到用不著任何同步機(jī)制、沒(méi)有任何通信,極少的內(nèi)存訪(fǎng)問(wèn),按理來(lái)說(shuō)他就不應(yīng)該存在BUG,然而還是發(fā)生了。第1個(gè)4小時(shí):縮小排查范圍,是什么引起段錯(cuò)誤
在源碼若干位置加上打印執(zhí)行的函數(shù)、行號(hào), 打開(kāi)調(diào)試選項(xiàng)重新編譯應(yīng)用程序,開(kāi)啟coredump選項(xiàng),耐心等待4小時(shí)后故障復(fù)現(xiàn)。gdb打開(kāi)coredump 確認(rèn)段錯(cuò)誤(Segmentation fault),棧溯確認(rèn)崩潰現(xiàn)場(chǎng)調(diào)用棧。段錯(cuò)誤位于ti_ck_mutil函數(shù)第266行之后。TickStatusIO():105ti_ck_mutil():266Segmentation fault (core dumped) (gdb) bt#0 0x401b28e0 in vfwprintf () from /lib/libc.so.6#1 0x00009d10 in ti_ck_mutil (cmdstr=0xbebffa4c, len=1) at src/ti.c:268#2 0x00008e2c in TickStatusIO () at src/initgpio.c:106#3 0x00009238 in main (argc=1, argv=0xbebffbf4) at src/initgpio.c:304
審查ti_ck_mutil函數(shù)內(nèi)226行之后的代碼,結(jié)合棧底位置是vfwprintf函數(shù)入口,基本可以確定導(dǎo)致崩潰位置是fread函數(shù),fread可能會(huì)有什么錯(cuò)誤呢?
int ti_ck_mutil(char *cmdstr, int count){ FILE *stream; char strout[256]; int ret, failcount = 0; for (int i = 0; i < count; i++) { printf("%s()%d\n", __FUNCTION__, __LINE__);//226行 stream = popen(cmdstr, "r");//未檢查文件是否成功 ret = fread(strout, sizeof(char), sizeof(strout), stream); // 228行 strout[ret] = '\0'; pclose(stream); // ... } return failcount;}
fread輸入?yún)?shù)只有4個(gè),猜測(cè)可能存在的失敗原因有3點(diǎn):
1、被編譯器優(yōu)化后strout的緩存不是256但后面用的是算數(shù)表達(dá)式sizeof,就算被優(yōu)化也不會(huì)造成錯(cuò)誤。觀(guān)點(diǎn):暫時(shí)不去瞎想。2、fread寫(xiě)入最后一個(gè)字符時(shí)溢出。
strout后第256地址也被填寫(xiě)了,實(shí)際我讀寫(xiě)的文件不超過(guò)64byte,不應(yīng)該超過(guò)256。即使第256地址被fread寫(xiě)了,相當(dāng)于內(nèi)存訪(fǎng)問(wèn)越接。訪(fǎng)問(wèn)越接發(fā)生什么錯(cuò)誤都不奇怪,輕微越接會(huì)影響附近變量的值,比如ret和stream的值改變,大范圍越界破壞調(diào)用棧。觀(guān)點(diǎn):猜測(cè)fread可能訪(fǎng)問(wèn)越限,但絕對(duì)沒(méi)破壞調(diào)用棧。若破壞調(diào)用棧,那么棧不會(huì)是整整齊齊打印4個(gè)函數(shù),而是輸出若干問(wèn)號(hào)(“?? ()”),找不到函數(shù)名稱(chēng)標(biāo)簽。
#0 0x000028e0 in ?? () #1 0x000038e8 in ?? () #2 0x000048ec in ?? () #3 0x000068e0 in ?? () #4 0x00009d10 in ti_ck_mutil (cmdstr=0xbebffa4c, len=1) at src/ti.c:268#5 0x00008e2c in TickStatusIO () at src/initgpio.c:106#6 0x00009238 in main (argc=1, argv=0xbebffbf4) at src/initgpio.c:304
3、stream文件描述符無(wú)效觀(guān)點(diǎn):有可能,源碼未對(duì)popen返回結(jié)果做判斷。
第2個(gè)4小時(shí):是內(nèi)存越界?還是資源不足?
于是結(jié)合猜測(cè)2和3,對(duì)源碼做2處理修改:1、不向fread傳遞完整內(nèi)存長(zhǎng)度,保證最后一個(gè)字符不被fread填寫(xiě) 2、判斷popen返回值stream = popen(cmdstr, "r");ret = fread(strout, sizeof(char), sizeof(strout), stream); 修改后 stream = popen(cmdstr, "r");if (stream == 0) { perror("popen error:");}ret = fread(strout, sizeof(char), sizeof(strout) - 1, stream);繼續(xù)等待4小時(shí),程序依舊崩潰,輸出崩潰前提示執(zhí)行popen失敗,返回值0,錯(cuò)誤原因記錄在errno里,errno指示打開(kāi)太多文件,資源不足。
popen error:: Too many open files
機(jī)理分析:為什么文件打開(kāi)太多?
進(jìn)一步定位到故障點(diǎn)在popen函數(shù)上,問(wèn)題是:啥叫文件打開(kāi)太多?查看popen幫助介紹:man popen?;蛟S能給我解釋RETURN VALUEThe popen() function returns NULL if the fork(2) or pipe(2) calls fail, or if it cannot allocate memory.本質(zhì)上popen是個(gè)“殼",它返回0的原因有兩個(gè):1、它間接調(diào)用fork()創(chuàng)建子進(jìn)程執(zhí)行腳本,間接調(diào)用pipe()創(chuàng)建管道,子進(jìn)程輸出信息從管道傳遞到父進(jìn)程。2、沒(méi)有足夠的內(nèi)存分配。從第2點(diǎn):沒(méi)有足夠的內(nèi)存方向去排查,無(wú)非是內(nèi)存泄漏咯,通常是申請(qǐng)內(nèi)存有釋放干凈導(dǎo)致。c語(yǔ)言標(biāo)準(zhǔn)內(nèi)存分配函數(shù)有malloc、calloc、realloc、reallocarray,對(duì)應(yīng)的釋放函數(shù)只有free。我應(yīng)該在源碼上搜索,是否所有“分配函數(shù)和釋放函數(shù)都一一配對(duì)”,哦~別忘了,小白可能還不清楚,除了常用的malloc外,還有像mmap這樣的內(nèi)存分配函數(shù),它有專(zhuān)用的釋放函數(shù)munmap。從搜索結(jié)果上看,數(shù)目是能對(duì)得上的,暫且粗略的判定不存內(nèi)存泄漏。更仔細(xì)的排查方向應(yīng)該是:確定代碼執(zhí)行流真的執(zhí)行到釋放函數(shù),而不是單純地看數(shù)目是否匹配。
在繼續(xù)閱讀popen的errors段落描述。
ERRORSThe popen() function does not set errno if memory allocation fails. If the underlying fork(2) or pipe(2) fails, errno is set appropriately. If the type argument is invalid, and this condition is detected, errno is set to EINVAL.popen不會(huì)因?yàn)閮?nèi)存分配失敗而在errno記錄錯(cuò)誤碼,如果是fork()或pipe()函數(shù)執(zhí)行失敗則在errno設(shè)置相應(yīng)錯(cuò)誤碼。忙半天忙個(gè)寂寞,年輕人,別學(xué)會(huì)寫(xiě)一、二、三,就自以為無(wú)師自通懂得寫(xiě)四、寫(xiě)一萬(wàn)。讀完man全文再入手好不好!既然errno提示具體錯(cuò)誤信息,就不可能是內(nèi)存泄漏,執(zhí)行失敗原因一定是Too many open files的字面意思。回想以前初學(xué)Linux時(shí)有個(gè)知識(shí)點(diǎn):為了防止某用戶(hù)打開(kāi)過(guò)多的文件,系統(tǒng)對(duì)進(jìn)程件訪(fǎng)問(wèn)數(shù)目有限制,默認(rèn)是1024。2016年4月參加宋寶華的線(xiàn)下培訓(xùn),他說(shuō)Android剛出來(lái)時(shí)有個(gè)提權(quán)的方法(root權(quán)限):創(chuàng)建1024個(gè)無(wú)用子進(jìn)程資源且不釋放,第1025個(gè)進(jìn)程就能得到root權(quán)限。命令查看應(yīng)用程序運(yùn)行一段時(shí)間后,有多少文件描述符號(hào)(file descriptor)沒(méi)有釋放。果然,每間隔15秒文件描述符就多一個(gè)。256分鐘后達(dá)到1024個(gè)文件描述符,時(shí)間上和軟件4小時(shí)崩潰很接近。
watch -n 1 ls -l /proc/PID/fd
再用之前的篩選方法:排查open和close的函數(shù)是一一匹配。發(fā)現(xiàn)open關(guān)鍵詞篩選出6行,close作為關(guān)鍵詞篩出5行。opendir沒(méi)有對(duì)應(yīng)的close。
捂臉!?。?/span>“Linux下一切皆是文件”我還沒(méi)理解透徹,沒(méi)意識(shí)到打開(kāi)目錄(opendir)也是文件資源,應(yīng)用程序某線(xiàn)程每間隔15秒就訪(fǎng)問(wèn)一次目錄。man opendir確認(rèn)closedir是它的配對(duì)關(guān)閉函數(shù)。
SEE ALSOopen(2), closedir(3), dirfd(3), readdir(3), rewinddir(3), scandir(3), seekdir(3), telldir(3)添加上closedir后故障得以修復(fù)。
順帶提一下
貼圖用的搜索工具不是grep而是我自己寫(xiě)的腳本jgrep,它的用法和grep完全一樣,輸入前面的數(shù)字能打開(kāi)對(duì)于文件所在行,對(duì)于搜索源碼、系統(tǒng)配置文件檢索、跳轉(zhuǎn)特別適用。如果你對(duì)jgrep感興趣的話(huà),在我的公眾號(hào)“程序員寫(xiě)個(gè)解”發(fā)送 “20220411” 可獲取。