5

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

  • 在 uclibc 配置中启用 UCLIBC_HAS_BACKTRACE=y 并编译它
  • 验证 libubacktrace.so 已创建
  • 使用以下选项编译我的应用程序二进制文件 -g -rdynamic -fexception 或 -funwind-tables
  • 二进制文件本身似乎被“剥离”了

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

如果我使用 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;
    }
}
4

2 回答 2

9

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

信号处理程序仅限于(直接或间接)调用异步信号安全函数(实际上,大多数系统调用(2)仅限)和回溯(3)甚至printf(3)malloc(3)free不是异步信号安全。因此,您的代码不正确:信号处理程序正在间接sigHandler调用(通过),并且它们不是异步信号安全的。printfprint_stackfree

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

阅读有关 POSIX signal.h信号概念的更多信息。实际上,信号处理程序可以做的几乎唯一明智的事情是设置一些全局、线程本地或静态volatile sig_atomic_t 标志,这些标志必须在其他地方进行测试。它也可以直接几个字节写入管道(7),您的应用程序将在其他地方读取(例如,如果它是一个 GUI 应用程序,则在其事件循环中)。

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

请注意,内核在处理信号时正在为sigreturn(2)设置一个调用帧(在调用堆栈中)。

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

如果你有一个事件循环,你可以考虑使用 Linux 特定的signalfd(2)并询问你的事件循环poll。For SIGTERMor SIGQUITorSIGALRM这是一个非常有用的技巧。

于 2015-05-01T06:40:42.990 回答
5

我想在@Basile Starynkevitch 的回答中添加一些内容,这过于迂腐。虽然您的信号处理程序确实不是async-signal-safe,但它很有可能经常在 Linux 上运行,所以如果您看到打印结果,这并不是导致您看不到相关堆栈问题的原因信息。

一些更可能的问题包括:

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

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

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

有了这个,我想解决你可以在你的信号处理程序中做什么来获得回溯。确实,您不应该调用任何 stdio 函数、malloc()free()等,但您不能使用健全的 glibc/libgcc 版本进行调用是不正确的。backtrace这里,您可以看到backtrace_symbols_fd当前是异步信号安全的。你也可以看到backtrace不是。看起来很不安全。然而,man 3 backtrace告诉我们为什么这些限制适用:

backtrace_symbols_fd() 不调用 malloc(3),因此可以在后一个函数可能失败的情况下使用,但请参阅 NOTES。

之后:

backtrace() 和 backtrace_symbols_fd() 不会显式调用 malloc(),但它们是 libgcc 的一部分,在第一次使用时会动态加载。动态加载通常会触发对 malloc(3) 的调用。如果您需要对这两个函数的某些调用来不分配内存(例如在信号处理程序中),您需要确保预先加载了 libgcc。

快速查看backrace 的源代码可以确认不安全部分涉及动态加载libgcc。您可以通过静态链接glibc和来解决这个问题libgcc,但最可靠的方法是确保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 以获取回溯。有趣的一点是uclibc 的 bactrace 的来源提到这-fasynchronous-unwind-tables是必要的。

注意:我正计划编写一个完整的工作代码示例,但我记得您必须使用sigaction信号处理版本并执行一些特殊操作才能在 ARM 上获取堆栈跟踪。我有代码可以在工作中完成它,一旦我拥有它,我将编辑这篇文章。

于 2020-05-17T23:35:30.507 回答