翼度科技»论坛 云主机 LINUX 查看内容

[glibc] 带着问题看源码 —— exit 如何调用 atexit 处理器

8

主题

8

帖子

24

积分

新手上路

Rank: 1

积分
24
前言

之前在写 apue 系列的时候,曾经对系统接口的很多行为产生过好奇,当时就想研究下对应的源码,但是苦于 linux 源码过于庞杂,千头万绪不知从何开启,就一直拖了下来。
最近在查一个问题时无意间接触到了 code browser 这个在线源码查看器,它同时解决了源码包下载和环境搭建的问题,版本也帮你选好了,直接原地起飞进入源码查看:

下面是查找 glibc exit 的过程:

语法高亮、风格切换、跳转 (定义/引用) 等功能做的还是很全面的,看代码绰绰有余,简直是我等 coder 之福音。
这里感谢 Bing 同学的介绍,感兴趣读者可以在文末参考它写的关于 glibc exit 的另一篇文章,也很不错的。
glibc exit

之前写过一篇介绍 linux 进程环境的文章(《[apue] 进程环境那些事儿》),其中提到了 glibc exit 会主动调用 atexit 注册的处理器,且有以下特性:

  • LIFO,先进后出的顺序
  • 注册几次调用几次
  • atexit 处理器中再次调用 exit 能完成剩余处理器的调用
  • atexit 处理器中再次注册的 atexit 处理器能被调用
下面带着这些问题,来看 glibc exit 的源码,以及它是如何实现上面这些特性的。
atexit 处理器结构

开门见山:
  1. void
  2. exit (int status)
  3. {
  4.     __run_exit_handlers (status, &__exit_funcs, true, true);
  5. }
  6. static struct exit_function_list initial;
  7. struct exit_function_list *__exit_funcs = &initial;
  8. uint64_t __new_exitfn_called;
复制代码
exit 只调用了一个 __run_exit_handlers 接口,它需要的 atexit 处理器列表存储在 __exit_funcs 参数中,是从这里传入的。
未曾开言先转腚,来看下 __exit_funcs 的结构:
  1. enum
  2. {
  3.     ef_free,        /* `ef_free' MUST be zero!  */
  4.     ef_us,
  5.     ef_on,
  6.     ef_at,
  7.     ef_cxa
  8. };
  9. struct exit_function
  10. {
  11.     /* `flavour' should be of type of the `enum' above but since we need
  12.        this element in an atomic operation we have to use `long int'.  */
  13.     long int flavor;
  14.     union
  15.     {
  16.         void (*at) (void);
  17.         struct
  18.         {
  19.             void (*fn) (int status, void *arg);
  20.             void *arg;
  21.         } on;
  22.         struct
  23.         {
  24.             void (*fn) (void *arg, int status);
  25.             void *arg;
  26.             void *dso_handle;
  27.         } cxa;
  28.     } func;
  29. };
  30. struct exit_function_list
  31. {
  32.     struct exit_function_list *next;
  33.     size_t idx;
  34.     struct exit_function fns[32];
  35. };
复制代码
exit_function_list 作为容器有点类似 stl 中的 deque,是由 exit_function 块组成的链表,兼顾了可扩展性与遍历效率两个方面:

其中 idx 记录了实际的元素个数,块之间通过 next 指针链接。
注意第一个块是在栈上分配的 initial 对象,之后的块才是在堆上分配的。
fns 数组存储的 exit_function 记录可以包含三种不同类型的函数原型:

  • void (*at) (void) : atexit 注册的函数
  • void (*on) (int status, void* arg) :__on_exit 注册的函数,与 atexit 的不同之处仅在于回调时多了一个 status 参数
  • void (*cxa) (void *arg, int status) :__internal_atexit 注册的函数,它又被以下接口调用:

    •  __cxa_atexit,在程序退出或 so 卸载时调用,主要是为编译器开放的内部接口
    •  __cxa_at_quick_exit,它又被 __new_quick_exit 所调用,后者和 exit 几乎一致

其中 quick_exit 调用 __run_exit_handlers 的后两个参数为 false,少清理了一些内容,以达到"快速退出"的目的。
  1. void
  2. __new_quick_exit (int status)
  3. {
  4.   /* The new quick_exit, following C++11 18.5.12, does not run object
  5.      destructors.   While C11 says nothing about object destructors,
  6.      since it has none, the intent is to run the registered
  7.      at_quick_exit handlers and then run _Exit immediately without
  8.      disturbing the state of the process and threads.  */
  9.   __run_exit_handlers (status, &__quick_exit_funcs, false, false);
  10. }
复制代码
另外 atexit 也是通过调用 __cxa_atexit 实现的:
  1. int
  2. atexit (void (*func) (void))
  3. {
  4.     return __cxa_atexit ((void (*) (void *)) func, NULL, __dso_handle);
  5. }
复制代码
arg 参数为 NULL;so 模块句柄默认为当前模块。 所以实际上并没有类型为 ef_at 的处理器,基本全是 ef_cxa,另外

  • 将 ef_free 置为整个 enum 第一个元素也是有用意的,通过 calloc 分配的内存,自动将内容清零,而对应的 flavor 恰好就是 ef_free
  • ef_us (use) 表示槽位被占用,但是具体的类型有待后面设置 (ef_at/ef_on/ef_cxa),具有一些临时性,但不可或缺
处理器的注册

直接上源码:
  1. int
  2. __internal_atexit (void (*func) (void *), void *arg, void *d,
  3.         struct exit_function_list **listp)
  4. {
  5.     struct exit_function *new;
  6.     /* As a QoI issue we detect NULL early with an assertion instead
  7.        of a SIGSEGV at program exit when the handler is run (bug 20544).  */
  8.     assert (func != NULL);
  9.     __libc_lock_lock (__exit_funcs_lock);
  10.     new = __new_exitfn (listp);
  11.     if (new == NULL)
  12.     {
  13.         __libc_lock_unlock (__exit_funcs_lock);
  14.         return -1;
  15.     }
  16.     new->func.cxa.fn = (void (*) (void *, int)) func;
  17.     new->func.cxa.arg = arg;
  18.     new->func.cxa.dso_handle = d;
  19.     new->flavor = ef_cxa;
  20.     __libc_lock_unlock (__exit_funcs_lock);
  21.     return 0;
  22. }
复制代码
参数赋值到变量 new 的成员后,没看到插入列表的动作,怀疑是在 __new_exitfn 时直接分配的:
  1. /* Must be called with __exit_funcs_lock held.  */
  2. struct exit_function *
  3. __new_exitfn (struct exit_function_list **listp)
  4. {
  5.     struct exit_function_list *p = NULL;
  6.     struct exit_function_list *l;
  7.     struct exit_function *r = NULL;
  8.     size_t i = 0;
  9.     if (__exit_funcs_done)
  10.         /* Exit code is finished processing all registered exit functions,
  11.            therefore we fail this registration.  */
  12.         return NULL;
  13.     for (l = *listp; l != NULL; p = l, l = l->next)
  14.     {
  15.         for (i = l->idx; i > 0; --i)
  16.             if (l->fns[i - 1].flavor != ef_free)
  17.                 break;
  18.         if (i > 0)
  19.             break;
  20.         /* This block is completely unused.  */
  21.         l->idx = 0;
  22.     }
  23.     if (l == NULL || i == sizeof (l->fns) / sizeof (l->fns[0]))
  24.     {
  25.         /* The last entry in a block is used.  Use the first entry in
  26.            the previous block if it exists.  Otherwise create a new one.  */
  27.         if (p == NULL)
  28.         {
  29.             assert (l != NULL);
  30.             p = (struct exit_function_list *) calloc (1, sizeof (struct exit_function_list));
  31.             if (p != NULL)
  32.             {
  33.                 p->next = *listp;
  34.                 *listp = p;
  35.             }
  36.         }
  37.         if (p != NULL)
  38.         {
  39.             r = &p->fns[0];
  40.             p->idx = 1;
  41.         }
  42.     }
  43.     else
  44.     {
  45.         /* There is more room in the block.  */
  46.         r = &l->fns[i];
  47.         l->idx = i + 1;
  48.     }
  49.     /* Mark entry as used, but we don't know the flavor now.  */
  50.     if (r != NULL)
  51.     {
  52.         r->flavor = ef_us;
  53.         ++__new_exitfn_called;
  54.     }
  55.     return r;
  56. }
复制代码
确实如此,另外这个内部接口是没有锁的,所以调用它的接口必需持有锁 (__exit_funcs_lock)。
代码不太好看,直接上图,当第一次分配时,仅有 initial 一个块,内部 32 个槽位,第一次命中最后的 else 条件,直接分配处理器 (场景 1):

前 32 个都不用额外分配内存 (场景 2):

第 33 个开始分配新的 exit_function_list,并移动 __exit_funcs 指针指向新分配的块作为列表的头 (场景 3):

结合上面的场景来理解下代码:

  • 插入记录时,第一个 for 循环基本不进入,因为当前块一般有有效的记录 (for 循环的作用是寻找第一个不空闲的块,这只在 atexit 处理器被调用且在其中注册新的处理器时才有用,所以暂时放一放)
  • l 一般指向当前分配的块,中间这个 if 大段落,如果记录不满,则直接分配新的元素 (else),并递增 idx,此时对应场景 1 & 2
  • 如果 l 为空或记录已满,则分配新的块。此时对应场景 3,__exit_funcs 作为链表头会指向新分配的块,将 idx 设置为 1,并将第一个记录返回
  • 最后设置新分配记录的 flavor 为 ef_us 表示占用
因为 atexit 没提供对应的撤销方法,所以这个 deque 在程序运行期间只会单向增长。
另外有几个小的点也需要注意,后面会用到:

  • 初始时判断了 __exit_funcs_done 标志位,如果已经设立,就不允许分配新的记录了
  • 设置 flavor 的同时也递增了变量 __new_exitfn_called 的值,它记录了总的处理器注册总量,因为在清理函数被调用时可能会注册新的处理器 (此时总量将超过 deque 的尺寸)
处理器的调用

直接上代码:
  1. /* Call all functions registered with `atexit' and `on_exit',
  2.    in the reverse of the order in which they were registered
  3.    perform stdio cleanup, and terminate program execution with STATUS.  */
  4. void
  5. __run_exit_handlers (int status, struct exit_function_list **listp,
  6.         bool run_list_atexit, bool run_dtors)
  7. {
  8.     /* First, call the TLS destructors.  */
  9.     if (run_dtors)
  10.         __call_tls_dtors ();
  11.     __libc_lock_lock (__exit_funcs_lock);
  12.     /* We do it this way to handle recursive calls to exit () made by
  13.        the functions registered with `atexit' and `on_exit'. We call
  14.        everyone on the list and use the status value in the last
  15.        exit (). */
  16.     while (true)
  17.     {
  18.         struct exit_function_list *cur;
  19. restart:
  20.         cur = *listp;
  21.         if (cur == NULL)
  22.         {
  23.             /* Exit processing complete.  We will not allow any more
  24.                atexit/on_exit registrations.  */
  25.             __exit_funcs_done = true;
  26.             break;
  27.         }
  28.         while (cur->idx > 0)
  29.         {
  30.             struct exit_function *const f = &cur->fns[--cur->idx];
  31.             const uint64_t new_exitfn_called = __new_exitfn_called;
  32.             switch (f->flavor)
  33.             {
  34.                 void (*cxafct) (void *arg, int status);
  35.                 void *arg;
  36.                 case ef_free:
  37.                 case ef_us:
  38.                     break;
  39.                 case ef_on:
  40.                     ...
  41.                 case ef_at:
  42.                     ...
  43.                 case ef_cxa:
  44.                     /* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
  45.                        we must mark this function as ef_free.  */
  46.                     f->flavor = ef_free;
  47.                     cxafct = f->func.cxa.fn;
  48.                     arg = f->func.cxa.arg;
  49.                     /* Unlock the list while we call a foreign function.  */
  50.                     __libc_lock_unlock (__exit_funcs_lock);
  51.                     cxafct (arg, status);
  52.                     __libc_lock_lock (__exit_funcs_lock);
  53.                     break;
  54.             }
  55.             if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called))
  56.                 /* The last exit function, or another thread, has registered
  57.                    more exit functions.  Start the loop over.  */
  58.                 goto restart;
  59.         }
  60.         *listp = cur->next;
  61.         if (*listp != NULL)
  62.             /* Don't free the last element in the chain, this is the statically
  63.                allocate element.  */
  64.             free (cur);
  65.     }
  66.     __libc_lock_unlock (__exit_funcs_lock);
  67.     if (run_list_atexit)
  68.         RUN_HOOK (__libc_atexit, ());
  69.     _exit (status);
  70. }
复制代码
 先整理下主脉络:

  • __call_tls_dctors 处理线程局部存储的释放,这里不涉及主题,略过
  • 主循环加锁遍历处理器 deque
  • 处理 libc 的 atexit 列表,略过
  • 调用 _exit 退出进程
 重点就落在中间的两个 while 循环上,外层用于遍历块,内层遍历块上的记录。为突出重点,switch 内只保留了 ef_cxa 的内容,其它的类似。

  • 回顾之前列表建立的过程,cur 指向的是最新分配的处理器,所以调用顺序 FILO 的问题得到了解答,特别是在遍历块内部时,也是倒序遍历的
  • 在回调前解锁,回调后加锁,这样避免用户在回调中再次调用 atexit 注册处理器时发生死锁
  • 每次回调之前记录当前处理器的总量 (new_exitfn_called),回调结束后将它与当前值对比,从而可以得知是否设置了新的 atexit 处理器

    • 如果相同,表示没有注册新处理器,对当前结构没影响,继续遍历当前块和整个 deque
    • 如果不相同,说明插入了新记录,当前指针已经失效,需要重新遍历,这里直接 goto restart 重新开始遍历

  • 注意在回调前,先将处理器信息复制到栈上,同时将 flavor 设置为 ef_free,避免重启遍历时,重复遍历此记录造成死循环
  • 整个块遍历结束后,移动 __exit_funcs 到下个块,同时释放当前块,如果下个块不为空的话 (当移动到 initial 时,next 为空,不释放 initial 指向的内存,因为它不是在堆上分配的)
  • 当 cur 遍历到最后一个块 (initial) 的 next (NULL) 后,表明整个 deque 遍历完毕,设置 __exit_funcs_done 标志,这可以阻止 atexit 再次注册处理器
特性分析

 有了上面的铺垫,再来分析其它的特性就清楚了:

  • 注册几次回调几次,这是因为插入了多个记录,虽然它们的 func 字段都指向同一个地址
  • 处理器中调用 exit 能完成剩余处理器的调用,原因分为两个方面:

    • 处理器回调前已经解锁,因此再次调用 exit 时可以正常进入这里
    • 处理器回调前已经把标志设为了 ef_free,所以再次遍历时,不会再处理当前记录,而是接着之前遍历位置继续遍历
    • 最终呈现的效果是剩余的处理器被接着调用了,但是这里一定要清楚,调用 exit 的回调其实没有返回,_exit 会保证它终结在最深层的处理器那里

最后一个特性:处理器中再次注册的 atexit 处理器能被调用,这个稍微复杂一点,需要结合之前注册部分的逻辑来看,再复习一下 __new_exitfn:
  1. /* Must be called with __exit_funcs_lock held.  */
  2. struct exit_function *
  3. __new_exitfn (struct exit_function_list **listp)
  4. {
  5.     struct exit_function_list *p = NULL;
  6.     struct exit_function_list *l;
  7.     struct exit_function *r = NULL;
  8.     size_t i = 0;
  9.     if (__exit_funcs_done)
  10.         /* Exit code is finished processing all registered exit functions,
  11.            therefore we fail this registration.  */
  12.         return NULL;
  13.     for (l = *listp; l != NULL; p = l, l = l->next)
  14.     {
  15.         for (i = l->idx; i > 0; --i)
  16.             if (l->fns[i - 1].flavor != ef_free)
  17.                 break;
  18.         if (i > 0)
  19.             break;
  20.         /* This block is completely unused.  */
  21.         l->idx = 0;
  22.     }
  23.     if (l == NULL || i == sizeof (l->fns) / sizeof (l->fns[0]))
  24.     {
  25.         /* The last entry in a block is used.  Use the first entry in
  26.            the previous block if it exists.  Otherwise create a new one.  */
  27.         if (p == NULL)
  28.         {
  29.             assert (l != NULL);
  30.             p = (struct exit_function_list *) calloc (1, sizeof (struct exit_function_list));
  31.             if (p != NULL)
  32.             {
  33.                 p->next = *listp;
  34.                 *listp = p;
  35.             }
  36.         }
  37.         if (p != NULL)
  38.         {
  39.             r = &p->fns[0];
  40.             p->idx = 1;
  41.         }
  42.     }
  43.     else
  44.     {
  45.         /* There is more room in the block.  */
  46.         r = &l->fns[i];
  47.         l->idx = i + 1;
  48.     }
  49.     /* Mark entry as used, but we don't know the flavor now.  */
  50.     if (r != NULL)
  51.     {
  52.         r->flavor = ef_us;
  53.         ++__new_exitfn_called;
  54.     }
  55.     return r;
  56. }
复制代码
假设当前调用的处理器是 handler_p,新注册的处理器是 handle_c,从上到下看:

  • 因未遍历完所有记录,__exit_funcs_done 未设置,所以仍可以注册新的处理器
  • 第一个 for 循环扫描当前块,将刚才回调 handler_p 而设立的 ef_free 记录回退掉

    • 如果当前不是第一个记录,则表明并非整个块空闲,直接使用刚设置为 ef_free 的记录,来存储 handler_c  的信息,图 1 展示了这种场景下的状态
    • 如果当前是第一个记录,则整个块已空闲,将 idx 设置为 0,并继续向下个块遍历

      • 如果下个块为 NULL,表示当前已经是最后一个块,状态见图 2
      • 否则继续检查下个块,此时一般不空闲 (一般是满的),见图 3



图 1

图 2

图 3
以上 3 个场景中,每次仅回退一个记录,这是由于我们假设 handler_p 是第一个被调用的处理器,如果它不是第一个被调用的,是否就能出现回退多个记录的场景?
考虑下面这个用例:假设有 handler_3 / handler_2 / handler_1 三个处理器依次被调用,前两个处理器都没有注册新的处理器,handler_1 注册了两个新的 handler,分别为 handler_i / handler_ii。
首先假设 3 个 handler 都在一个块中,注册完两个新 handler 后状态如下图:

图 4
在注册 handler_i 时回退了三次、handler_ii 时回退了两次,因此是可以回退多个记录的,毕竟 __run_exit_handlers 仅仅将遍历过记录的 flavor 设置为 ef_free 而没有修改任何 idx。
下面来看看是否存在跨块回退多个记录的场景,假设 handler_1 与 handler_2 跨块,则调用 handler_1 注册 handler_i 后的状态已变为下图:

图 5
这是因为处理完 handler_2 前一个块已经被释放不可访问了,好在目前 l 指向的块已满且 p == NULL,回退到了当初扩展块时的状态 (注册处理器的场景 3),从而重新分配块和记录,最终效果如图 6:

因为是新分配的块,就不存在覆盖的问题了。
总结一下:

  • 可以回退多个记录,但是只限制在一个块内
  • p == NULL 时一般是需要分配新的块了
在这个基础上继续执行 __run_exit_handlers,来看新注册的处理器是如何被调用的:

  • 首先回顾 __new_exitfn,当它注册新处理器后,会递增 __new_exitfn_called 的值
  • 回到 __run_exit_handlers,因检测到 __new_exitfn_called 发生了变化,会 goto restart 重新执行整个 while 循环
  • 重新遍历时,会首先处理新加入的处理器,且也是按 FILO 的顺序处理
至此最后一个特性分析完毕。
结语

从这里也可以看到一个标准的 atexit 需要考虑的问题:

  • 程序运行期间单向增长
  • 程序退出时反向减少
  • 有可能在执行回调时注册新的处理器从而导致再次增长,所以并不是单向减少
代码优化

glibc 主要花费了大量的精力处理第三个场景,不过经过本文一番分析,似乎不需要做的如此复杂。
  1. ...
  2.     for (l = *listp; l != NULL; p = l, l = l->next)
  3.     {
  4.         for (i = l->idx; i > 0; --i)
  5.             if (l->fns[i - 1].flavor != ef_free)
  6.                 break;
  7.         if (i > 0)
  8.             break;
  9.         /* This block is completely unused.  */
  10.         l->idx = 0;
  11.     }
  12.     if (l == NULL || i == sizeof (l->fns) / sizeof (l->fns[0]))
  13.     {
  14.         /* The last entry in a block is used.  Use the first entry in
  15.            the previous block if it exists.  Otherwise create a new one.  */
  16.         if (p == NULL)
  17.         {
  18.             assert (l != NULL);
  19.             p = (struct exit_function_list *) calloc (1, sizeof (struct exit_function_list));
  20.             if (p != NULL)
  21.             {
  22.                 p->next = *listp;
  23.                 *listp = p;
  24.             }
  25.         }
  26.         if (p != NULL)
  27.         {
  28.             r = &p->fns[0];
  29.             p->idx = 1;
  30.         }
  31.     }
  32.     else
  33.     {
  34.         /* There is more room in the block.  */
  35.         r = &l->fns[i];
  36.         l->idx = i + 1;
  37.     }
  38. ...
复制代码
例如回退记录实际不存在跨块的可能,那么回退时就可以只考虑当前块了,__new_exitfn 中第一个两层的 for 循环就可以简化为单层:
  1. ...
  2.     l = *listp;
  3.     for (i = l->idx; i > 0; --i)
  4.         if (l->fns[i - 1].flavor != ef_free)
  5.             break;
  6.     if (i == 0)
  7.         /* This block is completely unused.  */
  8.         l->idx = 0;
  9.     if (i == sizeof (l->fns) / sizeof (l->fns[0]))
  10.     {
  11.         /* The last entry in a block is used.  Use the first entry in
  12.            the previous block if it exists.  Otherwise create a new one.  */
  13.         assert (p == NULL);
  14.         assert (l != NULL);        
  15.         p = (struct exit_function_list *) calloc (1, sizeof (struct exit_function_list));
  16.         if (p != NULL)
  17.         {
  18.             p->next = *listp;
  19.             *listp = p;
  20.         }
  21.     }
  22.     else
  23.     {
  24.         /* There is more room in the block.  */
  25.         r = &l->fns[i];
  26.         l->idx = i + 1;
  27.     }
  28. ...
复制代码
经过简化后,l 永远不为 NULL,p 永远为 NULL,第二个 if 段中对 l 和 p 是否为 NULL 的判断就可以去掉了。看起来是不是简洁了一些?
当然了,上面的代码是没有经过验证的,保不齐哪里还有逻辑漏洞,欢迎大家来找茬~
dump exit_function_list

本来是打算把 __exit_funcs 中的内容打印出来看看,然而 glibc 设置了完备的符号隐藏,无法获取这个变量的地址:
  1. extern struct exit_function_list *__exit_funcs attribute_hidden;
  2. extern struct exit_function_list *__quick_exit_funcs attribute_hidden;
  3. extern uint64_t __new_exitfn_called attribute_hidden;
  4. /* True once all registered atexit/at_quick_exit/onexit handlers have been
  5.    called */
  6. extern bool __exit_funcs_done attribute_hidden;
复制代码
其中 attribute_hidden 就是设置符号的 visibility 属性:
  1. # define attribute_hidden __attribute__ ((visibility ("hidden")))
复制代码
例如在示例代码中插入下面的声明:
  1. enum
  2. {
  3.   ef_free,
  4.   ef_us,
  5.   ef_on,
  6.   ef_at,
  7.   ef_cxa
  8. };
  9. struct exit_function
  10. {
  11.     long int flavor;
  12.     union
  13.     {
  14.         void (*at) (void);
  15.         struct
  16.         {
  17.             void (*fn) (int status, void *arg);
  18.             void *arg;
  19.         } on;
  20.         struct
  21.         {
  22.             void (*fn) (void *arg, int status);
  23.             void *arg;
  24.             void *dso_handle;
  25.         } cxa;
  26.     } func;
  27. };
  28. struct exit_function_list
  29. {
  30.     struct exit_function_list *next;
  31.     size_t idx;
  32.     struct exit_function fns[32];
  33. };
  34. extern struct exit_function_list *__exit_funcs;
复制代码
并在 main 中打印 __exit_funcs 的地址:
  1. printf ("__exit_funcs: %p\n", __exit_funcs);
复制代码
编译时会报错:
  1. $ make
  2. gcc -Wall -g dumpexit.o apue.o -o dumpexit
  3. dumpexit.o: In function `dump_exit':
  4. /home/users/yunhai01/code/apue/07.chapter/dumpexit.c:70: undefined reference to `__exit_funcs'
  5. dumpexit.o: In function `main':
  6. /home/users/yunhai01/code/apue/07.chapter/dumpexit.c:103: undefined reference to `__exit_funcs'
  7. collect2: error: ld returned 1 exit status
  8. make: *** [dumpexit] Error 1
复制代码
正打算放弃,无意间看到这样一段宏:
  1. #if defined SHARED || defined LIBC_NONSHARED \
  2.   || (BUILD_PIE_DEFAULT && IS_IN (libc))
  3. # define attribute_hidden __attribute__ ((visibility ("hidden")))
  4. #else
  5. # define attribute_hidden
  6. #endif
复制代码
看起来符号隐藏只针对共享库,改为静态链接试试:
  1. dumpexit: dumpexit.o apue.o
  2.         gcc -Wall -g $^ -o $@ -static
  3. dumpexit.o: dumpexit.c ../apue.h
  4.         gcc -Wall -g -c $< -o $@ -std=c99
复制代码
居然通过了。运行程序,可以正常打印 __exit_funcs 地址:
  1. $ ./dumpexit
  2. __exit_funcs: 0x6c74a0
复制代码
注意这一步需要安装 glibc 静态库:
  1. sudo yum install glibc-static
复制代码
否则报下面的链接错误:
  1. $ make dumpexit
  2. gcc -Wall -g dumpexit.o apue.o -o dumpexit -static
  3. /usr/bin/ld: cannot find -lc
  4. collect2: error: ld returned 1 exit status
  5. make: *** [dumpexit] Error 1
复制代码
下面增加一些打印的代码:
  1. void dump_exit_func (struct exit_function *ef)
  2. {
  3.     switch (ef->flavor)
  4.     {
  5.         case ef_free:
  6.             printf ("free slot\n");
  7.             break;
  8.         case ef_us:
  9.             printf ("occupy slot\n");
  10.             break;
  11.         case ef_on:
  12.             printf ("on_exit function: %p, arg: %p\n", ef->func.on.fn, ef->func.on.arg);
  13.             break;
  14.         case ef_at:
  15.             printf ("atexit function: %p\n", ef->func.at);
  16.             break;
  17.         case ef_cxa:
  18.             printf ("cxa_exit function: %p, arg: %p, dso: %p\n", ef->func.cxa.fn, ef->func.cxa.arg, ef->func.cxa.dso_handle);
  19.             break;
  20.         default:
  21.             printf ("unknown type: %d\n", ef->flavor);
  22.             break;
  23.     }
  24. }
  25. void dump_exit ()
  26. {
  27.     struct exit_function_list *l = __exit_funcs;
  28.     while (l != NULL)
  29.     {
  30.         printf ("total %d record\n", l->idx);
  31.         for (int i=0; i<l->idx; ++ i)
  32.         {
  33.             dump_exit_func (&l->fns[i]);
  34.         }
  35.         l = l->next;
  36.     }
  37. }
复制代码
平平无奇的代码,为了增加可读性,事先注册了几个处理器:
  1. void do_dirty_work ()
  2. {
  3.     printf ("doing dirty works!\n");
  4. }
  5. void bye ()
  6. {
  7.     printf ("bye, forks~\n");
  8. }
  9. void times ()
  10. {
  11.     static int counter = 32;
  12.     printf ("times %d\n", counter--);
  13. }
  14. int main ()
  15. {
  16.   int ret = 0;
  17.   printf ("__exit_funcs: %p\n", __exit_funcs);
  18.   ret = atexit (do_dirty_work);
  19.   if (ret != 0)
  20.       err_sys ("atexit");
  21.   else
  22.       printf ("register do_dirty_work %p\n", (void *)do_dirty_work);
  23.   ret = atexit (bye);
  24.   if (ret != 0)
  25.       err_sys ("bye1");
  26.   else
  27.       printf ("register bye %p\n", (void *)bye);
  28.   ret = atexit (times);
  29.   if (ret != 0)
  30.       err_sys ("times");
  31.   else
  32.       printf ("register times %p\n", (void *)times);
  33.   dump_exit ();
  34.   printf ("main is done!\n");
  35.   return 0;
  36. }
复制代码
运行后效果如下:
  1. $ ./dumpexit
  2. __exit_funcs: 0x6c74a0register do_dirty_work 0x40115aregister bye 0x40116aregister times 0x40117atotal 4 recordcxa_exit function: 0x24a492d7cf90f3f0, arg: (nil), dso: (nil)cxa_exit function: 0x24a492d76ac4f3f0, arg: (nil), dso: (nil)cxa_exit function: 0x24a492d76aa4f3f0, arg: (nil), dso: (nil)cxa_exit function: 0x24a492d76a84f3f0, arg: (nil), dso: (nil)main is done!times 32bye, forks~doing dirty works!
复制代码
看起来有 4 个处理器,然而它们的地址却都一样,和我准备的那三个函数地址完全不同。
不清楚是否因为 glibc 版本变迁,导致 __exit_funcs 的内部结构发生了变化,还是什么其它原因导致成员对齐出了问题,最终没有打印出来预期的结果,有了解的同学不吝赐教。
后记

code browser 已经足够强大,美中不足的是缺少书签功能,在追踪调用栈时回退不是特别方便。
好在 Bing 同学已经贴心的为我们提供了相关的插件:https://github.com/caibingcheng/codebrowser-bookmark
安装之后浏览本文用的到几个关键函数效果如下:

直接点击书签就可以跳转到历史位置了,比之前多次回退方便多了。
实际操作起来非常简单,以我古老的 firefox 为例:

  • 安装油猴脚本管理器:https://addons.mozilla.org/zh-CN/firefox/addon/tampermonkey/,这一步基本是安装了一个浏览器 add-on
  • 导入书签插件:https://greasyfork.org/zh-CN/import,这一步需要填入 Bing 同学提供的脚本地址 (https://raw.githubusercontent.com/caibingcheng/codebrowser-bookmark/master/index.js),然后点击导入:

在新页面中安装导入的插件:

从弹出的窗口中选择直接安装:

这里会提示安装油猴脚本管理器,如果已经安装可以忽略提示:

点击安装后就可以看到脚本版本了:

回到 code browser,刷新下页面就可以看到书签小窗口啦~
需要注意的是,书签是本地存储的,在一台设备上创建的书签,不会自动同步到另一台设备哦。
参考

[1]. code browser
[2]. glibc-exit源码阅读
[3]. codebrowser书签插件

 

来源:https://www.cnblogs.com/goodcitizen/p/how_exit_calls_atexit_functions.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具