公司项目遇到服务器core了 用gdb调试 出现了in _int_free ()

[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib64/libthread_db.so.1".
Core was generated by `gameserver'.
Program terminated with signal 6, Aborted.
#0  0x00007f7043f631d7 in raise () from /usr/lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install cyrus-sasl-lib-2.1.26-23.el7.x86_64 glibc-2.17-157.el7_3.5.x86_64 keyutils-libs-1.5.8-3.el7.x86_64 krb5-libs-1.15.1-37.el7_6.x86_64 libcom_err-1.42.9-13.el7.x86_64 libcurl-7.29.0-51.el7_6.3.x86_64 libgcc-4.8.5-36.el7_6.2.x86_64 libidn-1.28-4.el7.x86_64 libselinux-2.5-14.1.el7.x86_64 libssh2-1.4.3-10.el7_2.1.x86_64 libstdc++-4.8.5-36.el7_6.2.x86_64 nspr-4.19.0-1.el7_5.x86_64 nss-3.36.0-7.1.el7_6.x86_64 nss-softokn-freebl-3.36.0-5.el7_5.x86_64 nss-util-3.36.0-1.1.el7_6.x86_64 openldap-2.4.44-21.el7_6.x86_64 openssl-libs-1.0.2k-16.el7_6.1.x86_64 pcre-8.32-17.el7.x86_64 zlib-1.2.7-18.el7.x86_64
(gdb) bt
#0  0x00007f7043f631d7 in raise () from /usr/lib64/libc.so.6
#1  0x00007f7043f64a08 in abort () from /usr/lib64/libc.so.6
#2  0x00007f7043fa2f07 in __libc_message () from /usr/lib64/libc.so.6
#3  0x00007f7043faa503 in _int_free () from /usr/lib64/libc.so.6
#4  0x00000000021d4c60 in CMyString::~CMyString() ()

#5  0x00000000018adcda in tagC_SWAR_SMALL_FIRE_INFO::GetJsonStrData (this=0x4d0c694 <CSwarLogicModule::OpenSWar(unsigned short, char const*, unsigned short, unsigned int, unsigned short, void*)::stAck+27988>, stMyStr=..., bBase64=false)
    at swarmsg.cpp:405

有两种可能 一种是同一块内存被delete两次 另一种可能性是 内存越界了

后来经过排查 对每一段分配的内存检测 发现分配的内存不够用 导致后来操作内存越界了 越界了不会core 但是内存写溢出,把后一个chunk的size参数写坏了

有两种结果。一种是free函数能检查出这个错误,程序就会先输出一些错误信息然后abort;一种是free函数无法检查出这个错误,程序便往往会直接crash。
  根据堆栈推测,诱发bug的是前一种情况。
  顺便说一句,windows内存分配跟linux比较类似,也是将内存块大小存放在malloc返回的指针位置之前。DEBUG模式下,编译器还会在实际分配内存的两端放两个特殊值,这样在内存回收时就可以检测到内存写溢出的问题。
    
  其次,当free函数检查到size异常以后,会调用malloc_printerr输出一些错误信息,但它并不一定会写syslog。
  查看__libc_message的代码可以发现,出现错误以后,glibc会先尝试将错误信息写入到stderr或tty,如果写入失败,才会去写syslog(代码有点啰嗦就不贴了)。
  要模拟这个情况,只需将环境变量"LIBC_FATAL_STDERR_"设为1迫使出错时写stderr,然后将stderr关闭即可。通常daemon程序很容易处在这样的状态。
    
  再次,查看tzset_internal的代码,我们发现导致free操作的原因是静态变量static char* old_tz释放导致的。
  old_tz存放了上一次调用tzset_internal时使用的时区字符串。如果再次调用tzset_internal时,时区不变就复用;如果不同,则free掉旧的字符串,strdup新的字符串,而strdup里面malloc了新字符串所需的内存块。
  要模拟这个情况只需先设法给old_tz一个初值,然后再做内存释放触发free(old_tz)即可。要给old_tz设初值只需先调用相关的时间函数即可,例如localtime这个函数经常就被用到,old_tz会初始化为默认值"/etc/localtime"。当malloc_printerr一步步调用到tzset_internal时,glibc会从环境变量"TZ"读取新的时区字符串,通常大多数服务器是没设置这个环境变量的,所以新tz就是空,从而导致"free(old_tz); old_tz = NULL;"这样的操作。

我简单查看了一下glibc的历史版本代码,这个bug在2.4到2.8的版本上都存在。当然这个bug首先需要程序员犯了内存写溢出错误才会诱发,所以这并不是严重bug,大家只要知道了自然也可结合实际情况做防范。比如检查进程是否正常不能光看进程是否存在,还需用工具做收发包检测,或者查看定时日志是否一直有输出之类。还有要对代码进行仔细的测试,线上环境比较复杂,之前没有发现这个问题,是因为分配的内存刚好够用,但是加了一个新功能,极端情况下,stAck里面的数据刚好大于分配的内存,导致内存越界.

因为发现了这个错误,发现自己对底层分配内存不够了解,所以下面是malloc_chunk的学习笔记:

边界标记法

ptmalloc分配的空间统一用了malloc_chunk结构来管理,malloc_chunk的结构初看比较奇葩,看了注释,分析了一段时间的代码,发现这种边界标记的设计,在malloc_chunk虚拟地址都是彼此相邻的情况下,是十分高效的。

malloc_chunk结构:

 
  1. /*

  2.   This struct declaration is misleading (but accurate and necessary).

  3.   It declares a "view" into memory allowing access to necessary

  4.   fields at known offsets from a given base. See explanation below.

  5. */

  6. struct malloc_chunk {

  7.   INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */

  8.   INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  9.   struct malloc_chunk* fd;         /* double links -- used only if free. */

  10.   struct malloc_chunk* bk;

  11.   /* Only used for large blocks: pointer to next larger size.  */

  12.   struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */

  13.   struct malloc_chunk* bk_nextsize;

  14. };

 

首先我们要清楚,malloc_chunk都是虚拟地址相连的,这样我们需要知道一个chunk相邻chunk的地址。每个malloc_chunk都包括自身的size,又包括虚拟地址前面的那个malloc_chunk,这里不需要记录后面malloc_chunk的size,因为后面的首地址就是当前malloc_chunk的首地址+size,prev_size在前面的chunk如果是空闲的时候才是可用的。如果前面的chunk是正在被使用的,那么这个prev_size的空间则被前面的chunk所征用。如果当前chunk是使用中的,那么fd,bk,fd_nextsize,bk_nextsize,都是无效的,它们都是关于空闲链表的指针,那么这些指针的空间全部被认为是空闲空间。 ptmalloc中的注释画出了空闲malloc_chunk和已分配的malloc_chunk的结构。

空闲chunk的结构:

上图中是空闲chunk的结构示意图,从图中可以看出当前malloc_chunk的指针是chunk指针,如果想得到地址相邻前面的指针,只需要chunk-prev_size即可。得到地址相邻后面的指针,chunk+size。其中fd,bk指针是bin中的空闲双向链表。这种通过prev_size可以使malloc_chunk的合并过程非常迅速。

从代码中看下空闲chunk的合并过程,还是malloc_consolidate里面函数的片段:

//合并前面的
 
 
  1. if (!prev_inuse(p))

  2. {

  3.     prevsize = p->prev_size;

  4.     size += prevsize;

  5.     p = chunk_at_offset(p, -((long) prevsize));

  6.     unlink(p, bck, fwd);

  7. }

//合并后面的

 
 
  1. if (nextchunk != av->top) 

  2. {

  3.     nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

  4.     if (!nextinuse)

  5.     {

  6.         size += nextsize;

  7.         unlink(nextchunk, bck, fwd);

  8.     }

  9. }

从代码中可以看出来,合并前面的malloc_chunk过程中,可以看到不用获得前面malloc_chunk的指针,合并的过程大概就是当前的chunk指针挪动到前面的位置,同时更新size,把chunk指针从相应的bin双向链表中干掉;从合并过程中可以看到,我们只是用到了prev_size,并没有用前面的chunk的指针,如果有前面chunk的指针,那么合并过程肯定还是需要这个chunk的size的。所以这里只用到prev_size加快合并的速度。向prev方向合并需要更新size和chunk指针,向next方向合并,只要更新size就行。

空间复用

malloc_chunk来说size是必须的,标志了这个chunk的大小,来决定是否满足malloc的要求,那么对于空闲的malloc_chunk来说fd,bk,fd_nextsize,bk_nextsize是必须的,bin中的空闲双链表;对于非空闲的malloc_chunk来说,fd,bk,fd_nextsize,bk_nextsize是没有用的,所以这部分空间被作为了可用的空间。那么prev_size就比较复杂了,它的状态取决于虚拟地址相邻前面的chunk的状态,如果前面的chunk是使用状态,那么这个chunk的prev_size就没有意义了,也没有合并的必要了,所以就不需要知道前面chunk指针的位置了,所以这个变量的空间被前面的chunk征用了。

使用状态的chunk

malloc请求的size,要加上结构体的数据大小才和malloc_chunk的size有可比性。

 
 
  1. #define request2size(req) \

  2.     (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? MINSIZE : \

  3.      ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

从上面的宏可以看出实际请求的大小是req再加上size_t然后对齐,这里prev_size和size不是应该2*size_t么,但是还要计算上next chunk赠送的prev_size的size_t。

总结

从上面的分析可以看出malloc_chunk设计是巧妙的,prev_size字段可以通过它来找到地址相邻空闲的上一个chunk,使得合并空闲的chunk十分方便,同时如果当前chunk的前一个chunk是使用中的,prev_size的空间可以借给上一个chunk作为可用空间。

内存越界问题及malloc_chunk边界标记法和空间复用相关推荐

  1. linux 程序收到sigsegv信号_linux下定位多线程内存越界问题实践总结

    最近定位了在一个多线程服务器程序(OceanBase MergeServer)中,一个线程非法篡改另一个线程的内存而导致程序core掉的问题.定位这个问题历经曲折,尝试了各种内存调试的办法.往往感觉就 ...

  2. 定位多线程内存越界问题实践总结

    最近定位了在一个多线程服务器程序(OceanBase MergeServer)中,一个线程非法篡改另一个线程的内存而导致程序core掉的问题.定位这个问 题花了整整一周的时间,期间历经曲折,尝试了各种 ...

  3. 【内存】内存检测工具sanitizer[内存泄漏、内存越界] VS valgrind

    简介 Sanitizers是谷歌发起的开源工具集,包括了AddressSanitizer, MemorySanitizer, ThreadSanitizer, LeakSanitizer,Saniti ...

  4. 使用PageHeap.EXE或GFlags.EXE检查内存越界错误 (转)

    2011-05-27 20:19 290人阅读 评论(0) 收藏 举报 microsoftdebuggingstructureoutputimagefile 必先利其器之一:使用PageHeap.EX ...

  5. c语言中较常见的由内存分配引起的错误_内存越界_内存未初始化_内存太小_结构体隐含指针...

    1.指针没有指向一块合法的内存 定义了指针变量,但是没有为指针分配内存,即指针没有指向一块合法的内浅显的例子就不举了,这里举几个比较隐蔽的例子. 1.1结构体成员指针未初始化 1 2 3 4 5 6 ...

  6. 【问题积累】printf 引发的内存越界问题

    printf 引发的内存越界问题 问题描述 问题的分析 问题描述 在开发新版本的答题器的时候,总是容易应发内存越界的问题,越界之后设备显示的数据都是错乱的,且不不停有调试信息打印输出 问题的分析 测试 ...

  7. Detected memory leaks!内存泄漏,溢出,内存越界问题分析

    应用程序发生 Detected memory leaks!内存泄漏 一直程序员面对的是一个很痛苦的问题,要查出泄漏的地方有时候需要大半天甚至更长时间.这里讲讲我的一些查找内存泄漏以及避免内存泄漏的一些 ...

  8. 内存越界导致的原因:

    可能会导致内存越界的原因 1.悬挂指针(Dangling pointer): 当一个指针指向一个已被释放的内存块,如果在之后对这个指针进行操作,就会导致程序出现未定义的行为,例如崩溃或者错误输出. 2 ...

  9. c语言中较常见的由内存分配引起的错误_内存越界_内存未初始化_内存太小_结构体隐含指针

    本篇是基于云天之巅博主音视频开发中的一个bug,继而查阅了的一点资料:本篇转载自博客园某博主的随笔,并做极少量的修改,原文地址:https://www.cnblogs.com/haore147/p/3 ...

最新文章

  1. OKR能带来哪些价值
  2. Senparc.Weixin.MP SDK 微信公众平台开发教程(十一):高级接口说明
  3. 计算机应用基础中什么是桌面,福师《计算机应用基础》在线作业二 Windows中进行系统设置的工具集是 用户可以根据自己的爱好更改显示器 键盘 鼠标器 桌面等硬件的设置...
  4. 弧形玻璃清洁机器人_宅家大扫除清洁装备+清洁好物+全屋家电维护一文搞定
  5. SQL Server 字符串操作
  6. 科学出版社c语言实验答案,程序设计基础c语言第三章程序结构教材习题答案科学出版社...
  7. VuePress 手摸手教你搭建一个类Vue文档风格的技术文档/博客
  8. mysql测试表格的年龄的语句是_MySQL查询语句练习题
  9. PLT redirection through shared object injection into a running process
  10. python中a and b什么意思_Python中的a+=b和a=a+b之间的区别是什么?
  11. Netty实现丢弃服务协议(Netty4.X学习一)
  12. 【线程】——线程的启动和中断
  13. cocos2d-js 网络请求之GET/POST
  14. Stanford NLP 第六课: Long Short Term Memory
  15. 小程序与UC浏览器打通
  16. ‍Mybatis源码我搞透了,面试来问吧!写了134个源码类,1.03万行代码!
  17. 短除法(求最大公约数)
  18. 试算平衡表示例图_试算平衡表的编制步骤是什么?
  19. lol大区服务器维护,LOL官宣“扩容升级”服务器,排队时间将大大减少,电一玩家喜大普奔!...
  20. 前端应届生如何做一个职业规划

热门文章

  1. 在sql中case子句的两种形式
  2. 潇洒郎:Python获取文件大小-os.path.getsize(filepath)
  3. vim复制粘贴快捷键
  4. 微信小程序同一个登录按钮上触发获取微信用户信息和电话号码
  5. 【Java秒杀方案】11.功能开发-【商品秒杀及优化】防止超卖 接口优化(redis预减库存,内存标记减少redis访问,RabbitMQ异步下单) 安全优化(隐藏秒杀接口,验证码,接口防刷)
  6. 来说说《凡人修仙传》
  7. 地表最强队列-ZMQ无锁队列
  8. HackTheBox-BountyHunter靶场通关记录
  9. Web语义化的理解(H5语义化的作用)
  10. Python每日一学 01——输入输出