glibc-abort源码阅读

问题

关于abort函数,有几个想调查的问题:

  1. abort是退出当前线程还是退出当前进程?
  2. abortraise/exit的区别是什么?

通过测试代码, 我们可以验证问题一, 通过阅读源码, 尽量来理解问题二.

测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <thread>
#include <cstdlib>
#include <iostream>
#include <chrono>
#include <exception>

using namespace std;

int main()
{
    thread t1([]() {
        int cnt = 20;
        while (cnt-- > 0)
        {
            cout << "cnt=" << cnt << endl;
            this_thread::sleep_for(std::chrono::seconds(1));
        }
    });

    thread t2([]() {
        this_thread::sleep_for(std::chrono::seconds(2));
        cout << "t2 abort" << endl;
        try
        {
            abort();
        }
        catch (exception &e)
        {
        }
    });

    if (t1.joinable())
        t1.join();
    if (t2.joinable())
        t2.join();

    return 1;
}

开始编译:

1
g++ main.cpp -o main --std=c++11 -lpthread

输出结果:

1
2
3
4
cnt=19
cnt=18
t2 abort
Aborted (core dumped)

通过这个测试可以直到, abort会导致进程的退出.

源码

首先是与abort相关的一些全局变量的定义, 如下, 有stage和lock, 并且此处的lock是递归lock. 这里可以看到递归锁是通过一个计数器实现的, 这里想到了信号量, 可以参考或对比《《UCB CS61a SICP Python 中文》一周目笔记(四)》《进程控制和通信(三) · 消息、信号、共享内存》.

为什么使用递归锁? 有些系统调用函数也不太清楚, 所以不太好下结论. 不过, 这里的递归锁可以保护stage的状态, 至少是有这个用处的. 使用递归的目的, 也是因为abort确实可能存在递归调用(用户性为上的).

从这里我们也可以看到abort的行为是对进程的.

1
2
3
4
5
/* We must avoid to run in circles.  Therefore we remember how far we
   already got.  */
static int stage;
/* We should be prepared for multiple threads trying to run abort.  */
__libc_lock_define_initialized_recursive (static, lock);

接下来是处理函数, 我们分为几个部分来看:

补充一个知识点: 信号掩码是对线程而言的. 第一部分是设置信号掩码, 允许SIGABRT信号.

尝试第一次raise SIGABRT信号, 用户可能绑定SIGABRT的信号处理函数, 这时候会去处理用户的信号函数. 这里考虑的问题是raise可能失败(去处理用户信号函数, 可能不引起SIGABRT退出), 所以会有后续操作.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* Cause an abnormal program termination with core-dump.  */
void
abort (void)
{
  struct sigaction act;
  sigset_t sigs;
  /* First acquire the lock.  */
  __libc_lock_lock_recursive (lock);
  /* Now it's for sure we are alone.  But recursive calls are possible.  */
  /* Unblock SIGABRT.  */
  if (stage == 0)
    {
      ++stage;
      __sigemptyset (&sigs);
      __sigaddset (&sigs, SIGABRT);
      __sigprocmask (SIG_UNBLOCK, &sigs, 0);
    }
  /* Send signal which possibly calls a user handler.  */
  if (stage == 1)
    {
      /* This stage is special: we must allow repeated calls of
         `abort' when a user defined handler for SIGABRT is installed.
         This is risky since the `raise' implementation might also
         fail but I don't see another p
         ossibility.  */
      int save_stage = stage;
      stage = 0;
      __libc_lock_unlock_recursive (lock);
      raise (SIGABRT);
      __libc_lock_lock_recursive (lock);
      stage = save_stage + 1;
    }
  //...

执行用户绑定的信号函数后, 现在将SIG_DFL绑定到SIGABRT上, 然后raise SIGABRT信号.

SIG_DFL是默认信号处理函数, 其值是0, 调用SIG_DFL相当于是访问0地址, 被禁止访问, 这时候就会退出进程了.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  /* There was a handler installed.  Now remove it.  */
  if (stage == 2)
    {
      ++stage;
      memset (&act, '\0', sizeof (struct sigaction));
      act.sa_handler = SIG_DFL;
      __sigfillset (&act.sa_mask);
      act.sa_flags = 0;
      __sigaction (SIGABRT, &act, NULL);
    }
  /* Try again.  */
  if (stage == 3)
    {
      ++stage;
      raise (SIGABRT);
    }

又担心raise失败, 这时候调用汇编的hlt命令(ABORT_INSTRUCTION调用的是htl命令), 使得逻辑CPU处于睡眠状态, 这时候相当于不再给当前进程CPU资源了.

如果hlt失败, 则又尝试exit退出.

如果exit失败, 则又不停地尝试hlt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

  /* Now try to abort using the system specific command.  */
  if (stage == 4)
    {
      ++stage;
      ABORT_INSTRUCTION;
    }
  /* If we can't signal ourselves and the abort instruction failed, exit.  */
  if (stage == 5)
    {
      ++stage;
      _exit (127);
    }
  /* If even this fails try to use the provided instruction to crash
     or otherwise make sure we never return.  */
  while (1)
    /* Try for ever and ever.  */
    ABORT_INSTRUCTION;
}

现在我们知道abort是通过调用raise/exit/hlt这些命令使得进程退出的. 不过这里也有疑问, 为什么需要做这么多后备处理? raise/exit/hlt在哪些情况下会失败呢? (TODO)

(这里的exit是指系统调用的_exit,可以关联《glibc-exit源码阅读》)