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

8

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

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

然而,我无法从信号处理程序中获取完整的backtrace。 只打印了我在信号处理程序中调用的函数地址。

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

此外,我尝试了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;
    }
}

你需要展示一些代码,特别是你的信号处理器代码。请编辑你的问题以改进它。 - Basile Starynkevitch
你正在编写什么类型的应用程序?你有事件循环吗? - Basile Starynkevitch
有点儿像……这个过程正在等待来自其他进程的“消息”。SysV IPC 消息在多个进程之间设置,并且它们互相发布消息。除此之外,没有太多特别的操作…不涉及 socket I/O。它通过调用 open() 和 write() 更新字符设备,但是只写入了 1 字节。 - user2526111
为什么不使用AF_UNIX套接字?或者管道,或者FIFO? - Basile Starynkevitch
你能告诉我它如何帮助吗?无论如何,这不是我的代码,我需要调试一些应用程序过程中的问题。这就是为什么我正在尝试设置信号处理程序并查看发生了什么......无论如何,非常感谢! - user2526111
1
没有绝对可靠的方法,因为您正在违反异步信号安全要求。您可以尝试使用Ian Taylor的libbacktrace,但您真的应该重新设计和重构您的应用程序以遵守signal(7)要求。为了调试目的,使用gdb要简单得多! - Basile Starynkevitch
3个回答

11
我想在@Basile Starynkevitch的回答中补充一些内容,这个回答过于学究。 虽然你的信号处理程序不是async-signal-safe,但在Linux上它很有可能经常工作,所以如果你看到结果被打印出来,那不是导致你无法看到相关堆栈信息的原因。
更有可能的问题包括:
1. 你的平台上编译器标志设置不正确。在x86上,回溯通常可以正常工作,无需特殊标志,但在ARM上可能会更加棘手。我试过一些我记不清的标志,但最重要的是尝试以下几个:-fno-omit-frame-pointer-fasynchronous-unwind-tables。你还可以尝试 -fnon-call-exceptions,它是 -fasynchronous-unwind-tables 的一个较弱版本,对于追踪崩溃应该足够了,至少在64位ARM上是如此。在32位ARM上,由于目前尚未实现 -fasynchronous-unwind-tables(据我所知),你可能需要使用 -fnon-call-exceptions
2. 引发崩溃的代码是通过未使用正确标志编译的代码调用的。例如,源自调用未使用正确编译器标志的 .so 的堆栈跟踪通常会导致重复或截断的回溯。
3. 你获取回溯的信号不是针对线程的信号,而是针对进程的信号。实际上,线程定向信号是指当线程崩溃时的 SIGSEGV,或者另一个线程通过类似 pthread_kill 的方式向特定线程发送的信号。有关更多信息,请参阅 man 7 signal

先把这个问题解决了,我想谈谈在信号处理程序中你可以做些什么来获取回溯信息。确实,你不应该调用任何stdio函数、malloc()、free()等等,但是并不是说你不能调用带有合理版本的glibc/libgcc的backtrace函数。从这里可以看到,backtrace_symbols_fd目前是异步信号安全的。你还可以看到backtrace函数不是。看起来非常不安全。然而,man 3 backtrace告诉我们为什么会有这些限制:

backtrace_symbols_fd()不调用malloc(3),因此可以在后者可能失败的情况下使用,但请参阅注意事项。

后续:

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的源代码提到-fasynchronous-unwind-tables是必需的。

10

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

一个信号处理程序只能直接或间接地调用异步信号安全函数(实际上,大多数syscalls(2)都是),而backtrace(3),甚至printf(3)malloc(3)free都不是异步信号安全的。因此你的代码是错误的:信号处理程序sigHandler正在调用printf并间接地(通过print_stack)调用free,它们都不是异步信号安全的。

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

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

你也可以在GCC内部使用Ian Taylor的libbacktrace(假设您的程序已编译为调试信息,例如-g)。它不能保证在信号处理程序中工作(因为它不仅使用了异步信号安全函数),但实际上非常有用。
请注意,内核在处理信号时为sigreturn(2)设置了一个调用帧(在调用堆栈中)。
如果您的应用程序是单线程的,还可以使用sigaltstack(2)来拥有备用信号堆栈。我不确定它是否有帮助。
如果您有一个事件循环,可以考虑使用Linux特定的signalfd(2)并要求您的事件循环对其进行poll。对于SIGTERMSIGQUITSIGALRM,这是一个非常有用的技巧。

3
在信号处理程序中使用signalfd()将完全破坏获取回溯信息的目的,因为此时您的回溯信息始终会100%指向从poll()循环调用的函数。 - user2679859

0

也许你可以尝试使用 boost::stacktrace::safe_dump_to

可能的代码可以是(fprintf 和 dladdr 看起来没问题)

void signal_safe_dump_bt_buf(uint64_t *bt_buf, size_t bt_cnt) {
  for (size_t i = 0; i < bt_cnt; i++) {
    uint64_t addr = bt_buf[i];
    Dl_info info;
    if (dladdr((void *)addr, &info) != 0) {
      if (info.dli_saddr) {
        fprintf(stderr, "0x%016lx: %s (offset 0x%lx) at %s\n",
                addr - (uint64_t)info.dli_fbase,
                info.dli_sname ? info.dli_sname : "?",
                (uint64_t)info.dli_saddr - (uint64_t)info.dli_fbase,
                info.dli_fname ? info.dli_fname : "?");
      } else {
        fprintf(stderr, "0x%016lx: %s at %s\n", addr - (uint64_t)info.dli_fbase,
                info.dli_sname ? info.dli_sname : "?",
                info.dli_fname ? info.dli_fname : "?");
      }
    }
  }
}

void signal_safe_dump_bt() {
  size_t max_bt_count = 100;
  uint64_t bt_buf[max_bt_count];
  size_t bt_cnt =
      boost::stacktrace::safe_dump_to(bt_buf, sizeof(decltype(bt_buf)));

  signal_safe_dump_bt_buf(bt_buf, bt_cnt);
}

相关博客在这里


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接