嵌入式平台使用 Electric Fence 侦测程序内存错误记录

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Miss_yuan/article/details/83384081
因为目前开发的项目中经常出现偶现的程序crash问题,所以最近一直在折腾C/C++程序内存错误的侦测方式。目前主要看了Address Sanitizer, Valgrind,还有一个比较有年头的东西 Electric Fence。
  • Address Sanitizer 真的比较好,只需要增加编译选项(-fsanitize=address)就可以用起来,能用的话很推荐使用。
    这个工具需要GCC版本在4.8.3以上才可以在出现问题时看到比较容易看懂的错误信息(出错位置程序堆栈)。
    而我这边也尝试过把在yocto中构建GCC5.2.0的交叉编译器并生产Linux的OS,同时供SDK来构建上层APP,但是APP程序跑到图形出现相关库的时候就出现问题了(libMali内部)。可能的原因是,我目前所开发的telechips平台上,有部分GPU相关的库是以prebuild形式被提供的,所使用的GCC版本为4.8.1,同时其链接的glibc库也是对应低版本的。(这个后面有空一定会继续折腾)

  • Valgrind 这个用起来也很简单,直接交叉编译出来,在开发板上跑就行了。好处是可以定位的问题类型很全面,处了常见的内存写溢出,还包含使用未初始化的内存问题。但主要的问题是运行太慢了,有UI界面的话要刷很久。即便我们的UI程序无法正常运转,我还是用这个工具抓到一个很久之前使用coredump定位到的crash问题的第一现场(在使用Valgrind之前,我是眼看着被写错的heap内存也是束手无策的)。

  • Electric Fence 这个东西应该是有些年头了。好在有源码,编译使用起来也方面,而且并不需要修改现存的代码。


经过一番折腾偶,目前可以用起来的就是 Electric Fence 这个东西。这里记录一下自己的使用这个工具的过程中遇到的问题及解决方式,最后是我使用该工具最终捕获到的一个代码中实际问题。
  • 关于这个东西的介绍可以看这里 Electric Fence 介绍
  • 源码我使用的是github上的这个版本 electric-fence 2.2.4 版本。实际使用中遇到了一些问题,通过自己定位简单修改了一下相关代码,可以使用起来了。这里还有个旧一些的版本 electric-fence 2.1.13 版本
  • 如果像我一样使用yocto发布linux版本的话,bb文件可以参考这里 bb文件

首先,构建出目标板的 electric-fence 库,静态或者动态库都可以。我这里是直接使用yocto构建的(方便以后做成动态库随linux版本发布),当然可以直接使用目标板的sdk去构建。

参考如上链接的bb文件,自己修改后使用的electric-fence.bb文件如下:

SUMMARY = "A malloc(3) debugger"
DESCRIPTION = "	Electric Fence is a debugger that uses virtual memory hardware	\
		to detect illegal memory accesses. It can detect two common 	\
		programming bugs: software that overruns or underruns the 	\
		boundaries of a malloc() memory allocation, and software 	\
		that touches a memory allocation that has been released by free()."

LICENSE = "GPLv2+"
LIC_FILES_CHKSUM = "file://COPYING;md5=18810669f13b87348459e611d31ab760"

SRC_URI += "git://github.com/CheggEng/electric-fence.git;branch=master"
SRCREV ="7199c87ec6df05cff6b0faa0b7114b02b53dffe1"
S = "${WORKDIR}/git"
PR = "r2"
PV = "2.2.4"

inherit autotools-brokensep
ALLOW_EMPTY_${PN} = "1"
ALLOW_EMPTY_${PN}-dev = "1"
PACKAGES = "${PN} ${PN}-dev ${PN}-staticdev"
FILES_${PN}-staticdev = "${libdir}/*"
INSANE_SKIP_${PN} += "installed-vs-shipped"

之后,把bb文件放置在合适的recipe路径下,然后bitbake electric-fence 即可。

编译中遇到一些了一些问题,通过修改Makefile文件解决(修改编译器程序,去除不需要的测试程序,修改了install规则等),修改后的Makefile文件如下:

INSTALL= install
CFLAGS= -g -std=gnu99
OBJECTS= efence.o page.o print.o

all:	libefence.a

install: libefence.a
	@echo "DESTDIR = ${DESTDIR}"
	@echo "libdir = ${libdir}"
	mkdir -p "$(DESTDIR)/$(libdir)"
	${INSTALL} -m 644 -p libefence.a $(DESTDIR)/$(libdir)/libefence.a

clean:
	- rm -f $(OBJECTS) libefence.a

libefence.a: $(OBJECTS)
	- rm -f libefence.a
	$(AR) crv libefence.a $(OBJECTS)

$(OBJECTS): efence.h

原始的Makefile编译生成的是静态库libefence.a,库比较小,直接链接到程序使用即可。
最后,将该库直接放置在sdk中合适的位置,编译应用程序时加入 -lefence -lpthread 即可。


然后开始侦测程序问题

运行程序,就等着抓应用程序的错误现场了,结果遇到了各种 Segmentation fault. 类似下面这种:
在这里插入图片描述
而且,每次的位置都不固定,共同点就是都在 libefence.a 的内存分配函数malloc或者free中。
这个问题和我参考的这边文章 内存调试工具Electric Fence 中最后提到的问题很类似,但该文章中并没有解决该问题。

不死心啊,还是对该工具寄予希望的~
分析问题,发现库函数中的lock()和unlock()线程锁似乎没有起到作用,大多数出问题时堆栈显示malloc和free都是同时进入执行。我觉得库中这段代码是有问题的(至少在我现在的环境下肯定达不到线程安全要求):

 /*
 * mutex to enable multithreaded operation
 */
static pthread_mutex_t mutex ;
static pid_t mutexpid=0;
static int locknr=0;

static void lock() {
    if (pthread_mutex_trylock(&mutex)) {
       if (mutexpid==getpid()) {  	// pid相同就能不用获取mutex么 ???
           locknr++;
           return;
       } else {
           pthread_mutex_lock(&mutex);
       }
    } 
    mutexpid=getpid();
    locknr=1;
}

static void unlock() {
    locknr--;
    if (!locknr) {
       mutexpid=0;
       pthread_mutex_unlock(&mutex);
    }
}

修改代码如下,并且在mutex初始化时,在属性中加入PTHREAD_MUTEX_RECURSIVE属性(因为库函数calloc需要在本线程内再次获取mutex,没有该属性会死锁):

static void lock()
{
	pthread_mutex_lock(&mutex);
}
static void unlock()
{
	pthread_mutex_unlock(&mutex);
}

extern C_LINKAGE void*
malloc(size_t size)
{
  void*  allocation;

  if(allocationList == 0)
  {
    /* 这里给 mutex 加入 PTHREAD_MUTEX_RECURSIVE 属性 */
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&mutex, &attr);
    initialize();   /* This sets EF_ALIGNMENT */
  }

  lock();
  allocation = memalign(EF_ALIGNMENT, size);
  unlock();

  return allocation;
}

编译后再次运行应用程序,已经可以正常运行起来了。并且运行速度上比 Valgrind 要快很多,UI界面刷新虽然有一点点慢,但是也到达了可以正常操作的程度。


下面来看一个我使用该工具发现的内存错误,错误类型为我们常说的 use after free

Electric Fence 工具检测到内存错误时,会直接导致 Segmentation fault。然后我们可以通过查看coredump中堆栈可以追溯问题发生的具体位置。

我的应用程序在我正常操作了一段时间后发生了 Segmentation fault,然后查看堆栈如下(这里我省略了一些我们的信号处理函数堆栈信息):

...
#5  <signal handler called>
#6  CommonAPI::DBus::DBusTimeout::setPendingCall (this=0xac1e5fd8, _pendingCall=0xac40bfd8) at .../common-api-c++-dbus/3.1.12-r0/git/src/CommonAPI/DBus/DBusMainLoopContext.cpp:544
#7  0xb5c122c0 in CommonAPI::DBus::DBusConnection::sendDBusMessageWithReplyAsync (this=0xb56cac50, dbusMessage=..., dbusMessageReplyAsyncHandler=..., _info=0xb6ec8a6c <_ZN9CommonAPI4DBusL15defaultCallInfoE>) at .../common-api-c++-dbus/3.1.12-r0/git/src/CommonAPI/DBus/DBusConnection.cpp:999
(gdb) frame 6
#6  CommonAPI::DBus::DBusTimeout::setPendingCall (this=0xac1e5fd8, _pendingCall=0xac40bfd8) at .../common-api-c++-dbus/3.1.12-r0/git/src/CommonAPI/DBus/DBusMainLoopContext.cpp:544
(gdb) p this
$6 = (CommonAPI::DBus::DBusTimeout * const) 0xac1e5fd8
(gdb) p sizeof(CommonAPI::DBus::DBusTimeout)
$7 = 40
(gdb) x/40xb 0xac1e5fd8
0xac1e5fd8:     0x10    0x04    0xcc    0xb5    0xb1    0x02    0x00    0x00
0xac1e5fe0:     0xff    0xff    0xff    0xff    0xff    0xff    0xff    0x7f
0xac1e5fe8:     0xe0    0xff    0x1d    0xac    0xdc    0xbf    0x4b    0xb5
0xac1e5ff0:     0xcc    0xbf    0x4b    0xb5    0x50    0xac    0x6c    0xb5
0xac1e5ff8:     0xd4    0xcf    0x7f    0xac    0xa0    0xbc    0xca    0xb4

出错堆栈对应代码为:

// #6 DBusMainLoopContext.cpp:544
543 void DBusTimeout::setPendingCall(DBusPendingCall* _pendingCall) {
544    pendingCall_ = _pendingCall;
545 }
// #7 DBusConnection.cpp:999
998 if(DBusTimeout::currentTimeout_)
999 	DBusTimeout::currentTimeout_->setPendingCall(libdbusPendingCall);

单从代码上看,就是简单的成员函数调用,给数据成员赋值的操作。

结合 Electric Fence 的原理是把释放后的内存用mprotect保护起来,变为不可读写。所以,怀疑的地方是该处的 DBusTimeout 对象所对应的内存已经被释放。这样去写 DBusTimeout 对象的 pendingCall_ 成员地址就会引起 Segmentation fault 错误。

为了证实这个猜测,可以利用该工具的一个配置项,设置环境变量 export EF_FREE_WIPES=1,这样工具会把free之后的内存里写入0xbd。

EF_FREE_WIPES
              By default, Electric Fence releases memory without changing the content of the released memory block.  IF EF_FREE_WIPES is non-zero, the sofware will fill the memory  block  with  0xbd  values  before  it  is
              released.  This makes it easier to trigger illegal use of released memory, and eaiser to understand why a memory access failed during gdb runs.

设置 EF_FREE_WIPES 完成之后,我再次尝试复现,果然还是可以抓到同样的问题,然后获取堆栈相关信息如下:

(gdb) bt
...
#2  <signal handler called>
#3  CommonAPI::DBus::DBusTimeout::setPendingCall (this=0xac389fd8, _pendingCall=0xabb11fd8) at .../common-api-c++-dbus/3.1.12-r0/git/src/CommonAPI/DBus/DBusMainLoopContext.cpp:544
#4  0xb5c292c0 in CommonAPI::DBus::DBusConnection::sendDBusMessageWithReplyAsync (this=0xb56e1c50, dbusMessage=..., dbusMessageReplyAsyncHandler=..., _info=0xb6edfa6c <_ZN9CommonAPI4DBusL15defaultCallInfoE>) at .../common-api-c++-dbus/3.1.12-r0/git/src/CommonAPI/DBus/DBusConnection.cpp:999
(gdb) p this
$1 = (CommonAPI::DBus::DBusTimeout * const) 0xac389fd8
(gdb) x/40xb 0xac389fd8
0xac389fd8:     0xbd    0xbd    0xbd    0xbd    0xbd    0xbd    0xbd    0xbd
0xac389fe0:     0xbd    0xbd    0xbd    0xbd    0xbd    0xbd    0xbd    0xbd
0xac389fe8:     0xbd    0xbd    0xbd    0xbd    0xbd    0xbd    0xbd    0xbd
0xac389ff0:     0xbd    0xbd    0xbd    0xbd    0xbd    0xbd    0xbd    0xbd
0xac389ff8:     0xbd    0xbd    0xbd    0xbd    0xbd    0xbd    0xbd    0xbd

再次输出这个 DBusTimeout 对象的内存数据,可以看到这块内存确实是已经被释放过的内存。(我自己后续也在DBusTimeout 的构造和析构中增加log,通过log输出,也证实了这个位置使用的 DBusTimeout 对象确实已经被析构)

结合之前自己也确实发现了一些程序crash在common-api-c+±dbus内部。所以,这里的错误也有可能就到导致crash的一些原因。毕竟释放后的内存可能被在利用,如果被写入错误数据,程序行为肯定不正常。
既然发现了具体出问题的地方,结合代码逻辑,修改起来就很简单了。

总结一下:

  • 首先,Electric Fence 的原理和缺点可以参考这个文章里的描述 内存调试工具Electric Fence

  • 关于内存占用方面,即使我们的程序使用malloc申请一个32bytes的空间,实际在内部会占用2个page(8192bytes),这个通过在gdb中打印全局的slot列表可以看出(如下)。内存占用大是肯定的,但是为了抓特定程序的随机crash问题,只给想侦测的那个程序用,大多情况下应该还是可行的。

(gdb) p allocationList
$525 = (Slot *) 0xac3db000
(gdb) p slotCount
$526 = 6324
(gdb) set $i = 0
(gdb) set $a = 0xac3db000
(gdb) while $i < 6324
 >p *(Slot *)$a
 >set $i = $i +1
 >set $a = $a +20
 >end
$527 = {userAddress = 0xb5691fe8, internalAddress = 0xb5691000, userSize = 24, internalSize = 8192, mode = ALLOCATED}
$528 = {userAddress = 0xb5701fdc, internalAddress = 0xb5701000, userSize = 36, internalSize = 8192, mode = ALLOCATED}
$529 = {userAddress = 0xb5703fdc, internalAddress = 0xb5703000, userSize = 36, internalSize = 8192, mode = ALLOCATED}
$530 = {userAddress = 0xb5705fdc, internalAddress = 0xb5705000, userSize = 36, internalSize = 8192, mode = ALLOCATED}
$531 = {userAddress = 0xb5707ff8, internalAddress = 0xb5707000, userSize = 8, internalSize = 8192, mode = ALLOCATED}
...
  • 线程同步和线性查找slot带来的效率问题,这个要看需要侦测的程序本身。当然还是前面说的能用起来 Address Sanitizer尽量用。
  • 为了能在侦测到错误时,可以通过堆栈查看到具体的段错误位置。我们的平台上必须要有生成coredump的机制,gdb程序以及程序及库文件的debuginfo信息。
  • Electric Fence 并不能检测程序使用了未初始化的内存问题,valgrind是可以的。
  • 自己思考了一个问题,其实在free掉的内存里写数据,可能会导致程序不正确的行为,但其实不会引起程序在之后的某个时刻发生crash。因为写错的内存在下次被分配后,一般都会被重新初始化的。怕的是把数据写到了正在用的内存里。这种问题工具检测的时候也检测不到,之后导致crash概率很高。但这种问题用工具来检测也是有效的,错误的那段程序写入的地址肯定是随机的,总不能每次都写入到正在用的内存里吧(那crash的概率很肯很高了),错误的那段程序只要有一次写到了free后的内存,就可以把错误的程序揪出来。

猜你喜欢

转载自blog.csdn.net/Miss_yuan/article/details/83384081