获取生成UNIX信号的故障地址

9

我对能够识别出问题所在指令地址的信号处理程序很感兴趣。

我了解 siginfo_t__builtin_return_address,但似乎都无法实现:

#include <iostream>
#include <signal.h>

void handler (int, siginfo_t *, void *);

int main ()
{
begin:
    std :: cerr << &&begin << " ~ " << &&before << " ~ " << &&after << "\n";

    struct sigaction s;
    s .sa_flags = SA_SIGINFO;
    sigemptyset (& s .sa_mask);
    s .sa_sigaction = handler;
    sigaction (SIGSEGV, &s, NULL);

    int * i = NULL;
before:
    *i = 0;
after:
    std :: cout << "End.\n";
}

void handler (int, siginfo_t *si, void *)
{
    std :: cerr << "si:" << si -> si_addr << "\n";
    std :: cerr << "At: " << __builtin_return_address (0) << "\n";
    std :: cerr << "At: " << __builtin_return_address (1) << "\n";
    std :: cerr << "At: " << __builtin_return_address (2) << "\n";
    std :: cerr << "At: " << __builtin_return_address (3) << "\n";
    std :: cerr << "At: " << __builtin_return_address (4) << "\n";
    std :: cerr << "At: " << __builtin_return_address (5) << "\n";
}

这将输出类似于以下内容:
0x10978 ~ 0x10a4c ~ 0x10a54
si:0
At: 0xfb945364
At: 0xfb939e64
At: 0x10a40
At: 0x10740
At: 0
At: Segmentation Fault

所以,siginfo_t是NULL,而__builtin_return_address在命名标签之间产生值。

我原本期望这两个函数都返回&&before的值。我是否正确使用了这些函数?

在Linux 2.6.9-89.0.9.Elsmp和SunOS上进行了测试。


请记住,另一个进程可以向此进程发送信号,因此“故障地址”可能不会告诉您任何重要信息。 另一方面,这是一个相当不太可能发生的事件。 - Jonathan Leffler
@Jonathan:使用实时信号(使用sigactionSA_SIGINFO,以及siginfo_t),内核需要保护免受欺骗。在siginfo_t中有一个字段指示了信号的来源,而sigqueue无法伪造信号源作为内核。 - R.. GitHub STOP HELPING ICE
2个回答

8
使用 SA_SIGINFO 安装的处理程序的第三个参数(声明为 void *)是指向 ucontext_t 结构的指针。该结构的内容是特定于架构和操作系统的,不属于任何标准,但它包含您需要的信息。以下是您的程序的一个版本,适用于使用它的情况(适用于 Linux/x86-64;对于感兴趣的每个架构和操作系统,您都需要使用 #ifdef):
#define _GNU_SOURCE 1
#include <iostream>
#include <iomanip>
#include <signal.h>
#include <ucontext.h>

using std::cout;

static volatile int *causecrash;

static void handler(int, siginfo_t *si, void *ptr)
{
   ucontext_t *uc = (ucontext_t *)ptr;

   cout << "si:" << si->si_addr << '\n';
   cout << "ip:" << std::hex << uc->uc_mcontext.gregs[REG_RIP] << '\n';
}

int main()
{
begin:
    cout.setf(std::ios::unitbuf);
    cout << &&begin << " ~ " << &&before << " ~ " << &&after << '\n';

    struct sigaction s;
    s.sa_flags = SA_SIGINFO|SA_RESETHAND;
    s.sa_sigaction = handler;
    sigemptyset(&s.sa_mask);
    sigaction(SIGSEGV, &s, 0);

before:
    *causecrash = 0;
after:
    cout << "End.\n";
}

顺便提一下,GCC有一个讨厌的习惯,即移动标签,但如果控制传输操作中未使用它们的地址(就它所知道的而言)。比较如下:

$ g++ -O0 -W -Wall test.cc && ./a.out 
0x400a30 ~ 0x400acd ~ 0x400ada
si:0
ip:400ad4
Segmentation fault
$ g++ -O2 -W -Wall test.cc && ./a.out 
0x4009f0 ~ 0x4009f0 ~ 0x4009f0
si:0
ip:400ab4
Segmentation fault

看到优化版本中所有标签都在同一个地址上了吗?这将阻止任何试图通过调整PC来从故障中恢复的尝试。我记得有一种方法可以让GCC不这样做,但我不知道具体是什么,也无法在手册中找到。


2
试图通过调整PC来恢复故障,这将进入危险的未定义行为领域。您可以尝试通过longjmp跳转到已知的代码片段,但即使如此也可能充满风险;最好的选择是转储核心并退出。 - Adam Rosenfield
@Adam:你可以在失败访问的地址上面映射一些新的东西,然后返回。这可能是解决由于截断的mmapped文件、过度提交等导致的SIGSEGV或SIGBUS的可行方案。 - R.. GitHub STOP HELPING ICE
真的,但同时,这是人们真正做到的一件事情,并且通常可以可靠地使其工作达到他们的目的。例如,持久化对象数据库和GC写屏障经常使用这种技巧。(虽然我不知道有谁依赖于GCC地址标签扩展来实现它。) - zwol
@R 你不需要为此调整电脑(我同意这样做会更加安全)。 - zwol

2
siginfo_t 不适用,因为它包含了被访问的内存地址,并非导致该错误的指令地址
__builtin_return_address 很有趣。在我的机器上,它返回一些无意义的东西:
0x40089f ~ 0x400935 ~ 0x40093f
si:0
At: 0x7fe22916fc20
At: 0x7fe22915ad8e

我不知道为什么。但是我检查了核心转储:

(gdb) bt
#0  0x00000000004009ff in handler(int, siginfo*, void*) ()
#1  <signal handler called>
#2  0x0000000000400939 in main ()

正如您所看到的,在您的情况下,有问题的地址位于标签位置之间。这很容易解释。只需查看main()的反汇编:

(gdb) disas
Dump of assembler code for function main:
   ...
   ; the label is here:
   0x0000000000400935 <+161>:   mov    -0x8(%rbp),%rax
=> 0x0000000000400939 <+165>:   movl   $0x0,(%rax)
   0x000000000040093f <+171>:   mov    $0x400c32,%esi

当使用任何非零参数调用__builtin_return_address时,它假定存在一个帧指针链。这在x86-64中并不是必需的,即使您强制编译器生成一个,内核制造的特殊“信号处理程序的调用者”帧也会破坏该链。 - zwol

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