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

use*_*111 4 linux arm uclibc

我已经做了很多次google来在信号处理程序中找到backtrace()的正确解决方案,并尝试了几乎所有方法,但是我无法在我的信号处理程序中成功获取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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Bas*_*tch 6

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

一种信号处理程序被限制到呼叫(直接或间接)仅异步信号安全函数(实际地说,大多数系统调用(2)只)和回溯(3)或甚至的printf(3)或malloc的(3)或free不异步信号安全。因此,您的代码是错误的:信号处理程序sigHandler正在printf间接调用(thru print_stack),free并且它们不是异步信号安全的。

因此,您唯一的选择是使用gdb调试器。

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

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

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

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

如果您有事件循环,则可以考虑使用Linux特定的signalfd(2)并向其询问事件循环poll。对于SIGTERM或SIGQUIT或SIGALRM这是一个非常有用的技巧。

  • 使用 signalfd() 将完全违背在信号处理程序中尝试获取回溯的目的,因为此时回溯将始终 100% 指向您从 poll() 循环调用的函数。 (3认同)

rat*_*der 5

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

一些更可能的问题包括:

  1. 您平台的编译器标志不正确。回溯通常在没有特殊标志的 x86 上工作正常,但 ARM 可能更挑剔。有一些我尝试过但我不记得了,但最重要的尝试是-fno-omit-frame-pointer和-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;
}
Run Code Online (Sandbox Code Playgroud)

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

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