問題描述:最近在工作中遇到這樣一個奇葩問題,程式裡需使用一個.so庫,同份源碼用我電腦編譯的庫放到程式使用出現各種異常問題,其他同僚編譯出來的沒問題。剛開始以為是編譯方式有問題,思來想去發現并不是。經分析發現是庫源代碼裡一全局數組記憶體位址大面積越界到其他全局數組了。
問題現象:現象為觸發某個業務條件,将導緻程式邏輯運作不正常,異常log如下圖,可看出“g_s32MaxFd”變量的值(檔案句柄)被置0,正常情況應該是大于0,是以此時導緻整個業務運作異常。
初步分析肯定是其他地方對變量“g_s32MaxFd”有指派才會導緻值為0。那麼到底是代碼正常邏輯語句操作還是代碼記憶體越界引起“g_s32MaxFd”值為0呢?這個倒好定位,隻需要搜尋下“g_s32MaxFd”變量在代碼哪些地方有使用就知道了,得出結論是代碼記憶體越界這種情況導緻。
一:開始定位記憶體越界處
【1】定位記憶體越界處,因程式并沒有因為記憶體越界而引發segment fault退出,是以準備使用Linux中mprotect()函數來設定指定記憶體區域的保護屬性為隻讀,故意使程式引發segment fault退出進而産生core dumped檔案來定位問題點。
分析下面問題前最好先熟悉下mprotect()函數
思路:使用mprotect()函數對被踩變量“g_s32MaxFd”記憶體位址設為隻讀屬性,由于mprotect()函數的局限性(保護屬性區域的起始位址必須為作業系統一個頁大小的整數倍),結合實際情況多樣性,分析情況如下表述:
1、當“g_s32MaxFd”數組起始位址剛好是頁大小整數倍時,此時隻需要将數組起始位址設定為mprotect()函數保護屬性為隻讀的起始位址即可,但需要注意一點,當被保護位址區域被程式正常資料結構進行通路時,也會引發segment fault退出(簡而言之就是當數組“g_s32MaxFd”記憶體位址被設定為隻讀後,如果是程式正常使用時也會引發段錯誤退出),這種情況就無法辨識是程式正常使用還是記憶體越界處使用,會影響分析真正的問題點。
解決方法:可利用GNU編譯器對.bss位址配置設定特性(具體特性自行查閱其他資料),在“g_s32MaxFd”數組位址處定義一個為頁大小整數倍大小的“g_debug_place”數組,這就相當于新增的“g_debug_place”數組占用之前“g_s32MaxFd”數組的位址。如下圖所示在“Var5”和“g_s32MaxFd”之間定義一個動态數組“g_debug_place”,大小最好是頁大小整數倍(如果小于一個頁大小會導緻鎖定的區域越界到“g_s32MaxFd”位址,問題得不到解決),這樣既可以保證新增的“g_debug_place”數組變量隻在記憶體越界的地方才會被通路而且數組大小也滿足mprotect()函數參數長度的取值要求(頁大小整數倍)。
2、 當“g_s32MaxFd”數組起始位址不是頁大小整數倍時,要結合上面第1種方法後還需要計算出大于且最靠近“g_debug_place”數組起始位址的頁大小整數倍位址。可套用公式:
設定保護屬性起始位址=被踩記憶體變量起始位址+(頁大小-(被踩記憶體變量起始位址%頁大小)) 注意:(被踩記憶體變量起始位址%頁大小)等于0時不适用以上公式,也就是被踩記憶體變量起始位址是頁大小整數倍情況下
假設“g_debug_place”數組起始位址為0x7fd8985bf8c0代入公式可得設定保護屬性起始位址為0x7fd8985c0000 ,理論上隻需要将位址0x7fd8985c0000設定為mprotect()函數保護屬性為隻讀的起始位址即可,但需要注意的是此時的0x7fd8985c0000位址并不是“g_debug_place”數組起始位址,由上面公式可知這個位址是為了滿足mprotect()函數的局限性而計算出來的位址。
解決方法:可通過在.bss段(之是以強調.bss段是因為我實際出現問題的變量就是未初始化的全局數組變量)首個變量位址前增加動态數組來改變記憶體配置設定解決。舉個例子,就好比是排隊,本來小明是排第六個,突然在隊伍最前面插一個小紅進來,小明就排在第七了,而小明前面之前那五個人的順序還是不變。而這個第七就是我們程式裡要的那個0x7fd8985c0000位址。
下圖藍色區域為新增動态數組(插隊小紅),大小為0x740位元組。增加後可使“g_debug_place”數組起始位址為0x7fd8985c0000(小明第七的位置),這時将0x7fd8985c0000位址作為mprotect()函數保護屬性為隻讀的起始位址就可以了,接下來就可以複現問題等着程式記憶體越界産生段錯誤退出吧。
注意:如果增加動态數組後并沒有直覺發現記憶體越界時,這可能是由于記憶體越界的位元組數太小(可能隻踩到一個位元組或幾個位元組),導緻調整過後的記憶體位址剛好踩到一個未使用的位址,這時需要微調動态數組大小來保證位址間隔及配置設定順序不變,具體問題具體分析。我是沒有出現這種情況,隻是覺得通過這種方法分析可能會存在此風險,如果有小夥伴遇到可以留言探讨。
bss段變量位址結構分布簡要展示如下圖(展示的是測試代碼,非實際工程代碼):
相關視訊推薦
90分鐘了解Linux記憶體架構,numa的優勢,slab的實作,vmalloc的原理
記憶體洩漏的3個解決方案與原理實作,知道一個可以輕松應用開發工作
linux記憶體管理問題-如何理出自己的思路出來,開發與面試雙豐收
學習位址:C/C++Linux鏈嶅姟鍣ㄥ紑鍙�/鍚庡彴鏋舵瀯甯堛€愰浂澹版暀鑲層€�-瀛︿範瑙嗛鏁欑▼-鑵捐璇懼爞
需要C/C++ Linux伺服器架構師學習資料加qun812855908擷取(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享
【2】gdb分析core檔案,編譯可執行程式時編譯選項需加-g參數,不要strip優化,否則可能會導緻調試資訊不是很完整。
檢查core dumped是否打開
/home # ulimit -c
0
/home # ulimit -c unlimited
/home # ulimit -c
unlimited
如果找不到ulimit指令,可以用busybox sh -c 'ulimit -a’指令測試ulimit是否存在,(ulimit是busybox的内置指令,往往我們想使用tab鍵快捷調用ulimit時可能不會彈出)有如下log輸出證明指令存在,後續直接執行ulimit -c unlimited,不要再執行busybox sh -c ‘ulimit -c unlimited’,這樣是打不開core的,我就這麼傻的操作過,當時還以為核心沒有打開這個功能。
/home # busybox sh -c 'ulimit -a'
-f: file size (blocks) unlimited
-t: cpu time (seconds) unlimited
-d: data seg size (kb) unlimited
-s: stack size (kb) 8192
-c: core file size (blocks) unlimited
-m: resident set size (kb) unlimited
-l: locked memory (kb) 64
-p: processes 1982
-n: file descriptors 1024
-v: address space (kb) unlimited
-w: locks unlimited
-e: scheduling priority 0
-r: real-time priority 0
分析core檔案過程,如下圖所示。當輸出log資訊不完整時,需要檢查下源碼和相關庫檔案路徑是否設定好,可根據圖檔中标注處進行設定。(展示的是測試代碼,非實際工程代碼)
實際代碼gdb分析log如下
/home/outapp/app # …/…/gdb xxx_capture core
GNU gdb (GDB) 7.6
Copyright © 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type “show copying”
and “show warranty” for details.
This GDB was configured as “arm-hisiv300-linux”.
For bug reporting instructions, please see:
http://www.gnu.org/software/gdb/bugs/…
Reading symbols from /home/outapp/app/xxx_capture…(no debugging symbols found)…done.
[New LWP 803]
[New LWP 789]
[New LWP 798]
[New LWP 807]
[New LWP 799]
[New LWP 791]
[New LWP 832]
[New LWP 797]
[New LWP 795]
[New LWP 802]
[New LWP 809]
[New LWP 790]
[New LWP 805]
[New LWP 804]
[New LWP 808]
[New LWP 796]
[New LWP 806]
[New LWP 810]
[New LWP 831]
[New LWP 833]
[Thread debugging using libthread_db enabled]
Using host libthread_db library “/lib/libthread_db.so.1”.
Core was generated by `xxx_capture capture 660’.
Program terminated with signal 11, Segmentation fault.
#0 0xb5e63b54 in memset () from /lib/libc.so.0
(gdb) bt
#0 0xb5e63b54 in memset () from /lib/libc.so.0
#1 0xb6e63064 in xxx3520D_Sample_OsdRegShowUpdata (ps8Contenx=0xb1dc2a70 " 000KM/H ", pstRegAttr=0x32f9e9c)
at SdkLogic/xxx3520dSample/xxx3520dOsd.c:436
#2 0xb6e63930 in xxx3520D_Sample_OsdShowGpsSpeed (pstRegAttr=0x32f9e9c, u8Speed=0 ‘\000’) at SdkLogic/xxx3520dSample/xxx3520dOsd.c:621
#3 0xb6e4dc14 in xxxSdkAl_OsdShowGpsSpeed (pstRegAttr=0x32f9e9c, u8Speed=0 ‘\000’) at SdkAppInt/xxxAHDSdkAL.c:474
#4 0xb6cb7b50 in OsdServiec::Osd_Reg_Show() () from /hi3520/lib/libxxxxxx_hi3520_AHDOsd.so
#5 0xb6cb726c in xxx_Osd_Display(void*) () from /hi3520/lib/libxxxxxx_hi3520_AHDOsd.so
#6 0xb6fc0f6c in start_thread () from /lib/libpthread.so.0
#7 0xb5e82134 in clone () from /lib/libc.so.0
#8 0xb5e82134 in clone () from /lib/libc.so.0
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
(gdb)
小結:以上定位記憶體越界隻是一個大體思路,實際情況多樣性,具體問題還需要具體分析,個人認為如果隻需要定位程式異常退出的話,用backtrace相關函數來代替gdb分析問題要輕量化很多。上述之是以使用gdb去分析問題是由于使用的交叉編譯是uclibc環境(uclibc環境下backtrace函數是沒實作的),就隻能使用sdk提供的gdb工具了
二:為什麼我電腦編譯出來的庫就暴露這個問題呢?
通過上面的方法已經定位到是哪行代碼有bug,是以想再分析下我編譯出來的庫為啥就暴露這個問題了呢?分析得知是在生成.so庫時由于連結.o的順序不同導緻庫裡面全局變量數組的位址分布也有所不同。下面分析下log檔案裡具體不同點,截圖貼上:
qiuhui@ubuntu:/mnt/hgfs/qh/work/app/SVN/?????$ arm-hisiv300-linux-objdump -t ???/lib?????.so > log
【圖一為我電腦編譯的】
【圖二為同僚電腦編譯的】
由上圖可以觀察到兩個全局數組變量“gs_s8Contenx”與“g_s32MaxFd”它們的位址有前後順序差異,圖一:“gs_s8Contenx位址0xfd9e4”小于“g_s32MaxFd位址0xfed34”,圖二:“gs_s8Contenx位址0xfdfd4”大于“g_s32MaxFd位址0xfdbd4”。正是由于這兩個位址的前後順序才導緻我編的庫暴露了問題,因為我編的gs_s8Contenx位址小于g_s32MaxFd,代碼裡剛好使用gs_s8Contenx數組時以超過數組元素最大值做指派操作,進而引發大面積記憶體越界,導緻越界位址直接就踩到g_s32MaxFd變量位址了(踩到很多全局變量了),是以g_s32MaxFd數組的值被莫名修改,進而産生各種異常。當然同僚編譯的同樣也會使gs_s8Contenx越界,但由于gs_s8Contenx位址大于g_s32MaxFd,是以gs_s8Contenx剛好踩到的是一段不常用的位址,導緻問題沒有及時暴露出來。