进程退出的清理函数

有时候,我们期望在进程退出时做一些资源清理工作,比如释放共享内存、删除程序运行期间留下的一些临时文件等等。

一种方式是使用 __attribute__ 关键字将清理函数声明为 destructor 函数:

这样该清理函数就会在 main 函数结束后被执行。

另外一种方式是使用 atexit 函数注册清理函数:

如上,在声明为 constructor 的 func 函数和 main 函数里面,通过 atexit 分别注册了两个匿名的清理函数,执行结果如下:

首先是 constructor 函数被执行,然后是 main 函数,接着是 main 函数注册的清理函数,最后是 constructor 函数注册的清理函数。这说明通过 atexit 注册的清理函数,其执行顺序是跟注册严格反序的,也就是最后注册的最先执行。

另外,值得注意的是,c++ 限制通过 atexit 最多可以注册 32 个清理函数。

 

重载 gcc 和 glibc 的内建函数

工作中常常遇到这样一种场景,出于调试需要,需要重载编译器或者运行库内建的一些函数,如 malloc、free 之类。

一、简单重载

最简单直接的方式就是重新定义:

这样就会覆盖 glibc 内建的 malloc 函数。

二、符号别名

另外一种方式是使用 gcc 的 __attribute__ 关键字来为函数创建别名。

上述代码的含义是,为 my_malloc 函数创建了一个函数别名 malloc。这样一来,gcc 内建的 malloc 也就不再可用,所有调用到 malloc 的地方都将调用 my_malloc,也就达到了覆盖的目的。

三、LD_PRELOAD

LD_PRELOAD 是动态连接器支持的一个环境变量,可以指定共享库在程序运行时首先被装载,包括 C 语言运行库 glibc。

利用这个特性,如果在某个共享库里重载 glibc 内建的函数,然后使用 LD_PRELOAD 指定让其首先被装载,这样也能达到目的。

四、wrap 函数

以上几种方式的弊端在于,如果符号是对外可见的,那么所有模块的 malloc 函数都将被覆盖;如果符号不可见的话,只有模块内部才会使用重载的版本。如果我们期望某些模块使用重载的版本,而另一些模块使用编译器或者运行库内建的版本呢?

例如可执行程序 a.out 动态链接共享库 liba.so 和 libb.so,在 liba.so 里有对 gcc 编译器内建函数 malloc 的重载,我们期望 liba.so 和 a.out 都使用重载的 malloc,而 libb.so 使用 gcc 的版本。如果 liba.so 里面的 malloc 对外可见,那么 link 过程中,libb.so 和 a.out 都将使用重载的这一份;而如果不可见的话,a.out 将会使用 gcc 的这一份。

有人可能会想到,在每个需要重载的模块内都重载一遍 malloc,然后将其符号隐藏。这样确实可以达到目的,但是显得很笨拙,也会因重复的符号定义增大程序的体积。正确的方法是使用 ld 的 wrap 功能。

链接器会将 wrap 参数所指定的符号替换成重载的版本。这样可以对不同的共享库指定或者不指定 wrap 目标,来达成上述目的。

按照如上方式编译运行,结果如下:

liba.so 和 a.out 都使用了重载的 malloc, 而 lib.so 使用的是 gcc 内置的版本。

巧用 gcc 和 glibc 调试内存问题

工程中,我们常常会遇到内存访问越界导致的问题。一般而言,如果被踩到的内存没有被释放或者用作他途(比如恰好记录着 heap 的数据结构),是不会发生问题的。但是一旦发生 core dump,定位起来就非常麻烦。

先来看这样一段危险代码:

这里虽然发生了严重内存访问越界,但是运行时并没有什么异常:

但如果被踩到的内存后续被释放的话,就会发生问题。

我们将先前申请的内存释放掉,然后编译运行:

可以看到,在 free 的时候发生了问题。这里可以使用 gdb 找到发生内存访问越界的地方。但是如果遇到大型程序的话,类似方式往往很难定位到确切的位置。

这里提供两种方式,分别使用 C 运行库 glibc 和编译器 gcc 内置的功能,来帮忙调试类似的问题。

一、MALLOC_CHECK_

GNU 的 C 标准库(glibc)支持动态内存调试,需要通过环境变量 MALLOC_CHECK_ 来设定其行为,其取值有四个等级:

  • MALLOC_CHECK_=0 关闭所有检查
  • MALLOC_CHECK_=1 当有错误被探测到时,在标准错误输出(stderr)上打印错误信息
  • MALLOC_CHECK_=2 当有错误被探测到时,不显示错误信息,直接调用 abort 进行中断
  • MALLOC_CHECK_=3 当有错误被探测到是,同时执行 1 和 2

默认情况下,该环境变量是没有设定的,需要我们手工去设定:

再次运行上述程序,glibc 就会调用 abort 中断程序,并给出丰富的提示信息:

二、AdressSanitizer

AddressSanitizer 是 Google 出品的一款开源的内存检测工具,可以用来检测内存损坏错误,例如缓冲区溢出或访问悬空指针等一系列问题。它首先被 clang 支持,继而又被 gcc 支持,gcc 4.8 及以后的版本直接可用。

再次运行该程序,可以看到 AddressSanitizer 提示 heap-buffer-overflow 并给出了相关内存地址的申请和读写操作的 backtrace,可以快速定位问题。

当然,这么强大的工具,也是有缺陷的。AddressSanitizer 编译的程序的堆、栈占用比原生程序的大,因此对于某些嵌入式设备可能并不太适用。