问题描述:最近在工作中遇到这样一个奇葩问题,程序里需使用一个.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刚好踩到的是一段不常用的地址,导致问题没有及时暴露出来。