如何在进程信号处理程序(armv7-uclibc)中获得正确的回溯?

How to get proper backtrace in process signal handler (armv7-uclibc)?

我已经 google 多次在信号处理程序中找到 backtrace() 的正确解决方案并尝试了几乎所有方法但我无法在我的信号处理程序中成功获得回溯 - 这不是 SIGUSR1 处理程序.

但是,我无法从信号处理程序中获得完整的回溯。 只打印了我在信号处理程序中调用的函数地址。

如果我使用 target-gdb 二进制文件并使用 gdb --pid 命令附加进程,我能够正确获得完整的回溯。

此外,我尝试了 pstack,但是(pstack-1.2 - 尝试了 arm-patch 但它太可怕了......没有打印出来)不是很有帮助。

有什么建议吗?


1) Makefile 中的编译器选项

CFLAGS += -g -fexceptions -funwind-tables -Werror $(WARN) ...

2) 代码

代码极其简单

#define CALLSTACK_SIZE 10

static void print_stack(void) {
    int i, nptrs;
    void *buf[CALLSTACK_SIZE + 1];
    char **strings;

    nptrs = backtrace(buf, CALLSTACK_SIZE);
    printf("%s: backtrace() returned %d addresses\n", __func__, nptrs);

    strings = backtrace_symbols(buf, nptrs);

    if(strings == NULL) {
        printf("%s: no backtrace captured\n", __func__);
        return;
    }

    for(i = 0; i < nptrs; i++) {
        printf("%s\n", strings[i]);
    }

    free(strings);
}

...
static void sigHandler(int signum)
{
    printf("%s: signal %d\n", __FUNCTION__, signum);
    switch(signum ) {
    case SIGUSR2:
        // told to quit
        print_stack();
        break;
    default:
        break;
    }
}

仔细阅读signal(7) and signal-safety(7).

信号处理程序仅限于(直接或间接)调用 async-signal-safe-functions(实际上,大多数 syscalls(2) only) and backtrace(3) or even printf(3) or malloc(3)free 不是异步信号安全的。因此您的代码不正确:信号处理程序 sigHandler 正在调用 printf 并间接(通过 print_stackfree 并且它们不是异步的-信号安全。

所以你唯一的选择是使用 gdb 调试器。

阅读有关 POSIX signal.h & signal concepts. Practically speaking, the nearly only sensible thing a signal handler can do is set some global, thread-local, or static volatile sig_atomic_t flag, which has to be tested elsewhere. It could also directly write(2) a few bytes into a pipe(7) 的更多信息,您的应用程序将在其他地方阅读(例如,如果它是 GUI 应用程序,则在其事件循环中)。

您也可以使用 Ian Taylor 的 libbacktrace from inside GCC(假设您的程序是使用调试信息编译的,例如 -g)。它不能保证在信号处理程序中工作(因为它不只使用异步信号安全函数),但它实际上非常有用。

注意内核正在设置调用帧(在 call stack) for sigreturn(2) 处理信号时。

您也可以使用(特别是如果您的应用程序是单线程的)sigaltstack(2) 来获得备用信号堆栈。我不确定它是否有帮助。

如果您有事件循环,您可以考虑使用 Linux 特定的 signalfd(2) 并要求您的事件循环 poll 它。对于 SIGTERMSIGQUITSIGALRM 这是一个非常有用的技巧。

我想对@Basile Starynkevitch 的回答补充一些内容,该回答过于迂腐。 虽然您的信号处理程序确实不是 async-signal-safe,但它很有可能经常在 Linux 上工作,所以如果您看到打印出结果,那不是导致您出现问题的原因没有看到相关的堆栈信息。

一些更可能出现的问题包括:

  1. 您平台的编译器标志不正确。回溯通常在没有特殊标志的 x86 上工作良好,但 ARM 可能更挑剔。有一些我试过但记不清了,但最重要的是 -fno-omit-frame-pointer-fasynchronous-unwind-tables.

  2. 崩溃的代码是通过未使用正确标志编译的代码调用的,以获取堆栈跟踪。例如,源自未使用正确编译器标志编译的 .so 回调的代码的堆栈跟踪通常会导致重复或截断的回溯。

  3. 您获取回溯的信号不是线程导向信号,而是进程导向信号。实际上,线程定向信号是线程崩溃时的 SIGSEGV 之类的信号,或者另一个线程向特定线程发送的信号,例如 pthread_kill 之类的信号。有关详细信息,请参阅 man 7 signal

除此之外,我想谈谈您可以在信号处理程序中执行哪些操作来获取回溯。的确,你不应该调用任何 stdio 函数,malloc()free() 等,但你不能调用 not 是真的backtrace 与 glibc/libgcc 的正常版本。来自 here, you can see that backtrace_symbols_fd is currently async-signal-safe. You can also see that backtrace is not. It looks very unsafe. However, man 3 backtrace 告诉我们为什么这些限制适用:

backtrace_symbols_fd() does not call malloc(3), and so can be employed in situations where the latter function might fail, but see NOTES.

后来:

backtrace() and backtrace_symbols_fd() don't call malloc() explicitly, but they are part of libgcc, which gets loaded dynamically when first used. Dynamic loading usually triggers a call to malloc(3). If you need certain calls to these two functions to not allocate memory (in signal handlers, for example), you need to make sure libgcc is loaded beforehand.

快速查看 source for backrace 确认不安全部分涉及动态加载 libgcc。您可以通过静态链接 glibclibgcc 来解决这个问题,但最可靠的方法是确保在生成任何信号之前加载 libgcc

我这样做的方法是在程序启动期间调用一次 backtrace。请注意,您必须至少请求一个符号,或者在不加载 libgcc 的情况下提前退出函数。像这样的东西会起作用:

// On linux, especially on ARM, you want to use the sigaction version of this call.
// See my comments below.
static void
handle_signal(int sig)
{
    // Check signal type or whatever you want to do.
    // ...
    
    void* symbols[100];
    int n = backtrace(symbols, 100);
    
    // You could also either call a string formatting routine that you know
    // is async-signal-safe or save your backtrace and let another thread know
    // that this thread has crashed and the backtrace needs to be printed.
    //
    write(STDERR_FILENO, "Crash:\n", 7);
    backtrace_symbols_fd(symbols, n, STDERR_FILENO);

    // In the case of notifying another thread, which is what I do, you would
    // do something like this:
    //
    // threadLocalSymbolCount = backtrace(threadLocalSymbols, 100);
    // sem_post() or write() to an eventfd or whatever.
}

int main(int argc, char** argv)
{
    void* dummy = NULL;
    backtrace(&dummy, 1);
    
    // Setup custom signal handling
    // ...

    function_that_crashes();

    return 0;
}

编辑:OP 提到他们使用的是 uclibc 而不是 glibc,但同样的论点适用,因为它动态加载 libgcc 以获取回溯。有趣的一点是 source for uclibc's bactrace 提到 -fasynchronous-unwind-tables 是必要的。

注意:我正计划编写一个完整的工作代码示例,但我记得您必须使用 sigaction 版本的信号处理并做一些特殊的事情来获取堆栈跟踪手臂。我有在工作中执行此操作的代码,我将在获得后编辑此 post。