問題描述:最近在工作中遇到這樣一個奇葩問題,程序里需使用一個.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段變量地址結構分布簡要展示如下圖(展示的是測試代碼,非實際工程代碼):
【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剛好踩到的是一段不常用的地址,導致問題沒有及時暴露出來。
-
Linux
+關注
關注
87文章
11327瀏覽量
209966 -
內存
+關注
關注
8文章
3041瀏覽量
74177 -
程序
+關注
關注
117文章
3793瀏覽量
81221 -
源碼
+關注
關注
8文章
649瀏覽量
29312
發布評論請先 登錄
相關推薦
評論