从内部捕获 SIGSEGV 时,如何知道涉及的无效访问类型?

When catching SIGSEGV from within, how to known the kind of invalid access involved?

如您所知,可以使用处理程序捕获除 kill 和 stop/count 之外的任何信号。
存在三种无效地址访问:

我只对拒绝无效读取访问感兴趣。所以我的想法是捕获所有分段错误,如果不是无效读取访问,abort()

到目前为止,我只知道如何将 SEGV_MAPERRSEGV_ACCERRsigaction 一起使用,这当然是无关紧要的。

事实证明,在Linux x86-64 (aka AMD64) 架构上,这实际上是非常可行的。

这是一个示例程序,crasher.c:

#define  _POSIX_C_SOURCE 200809L
#define  _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <ucontext.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

#if !defined(__linux__) || !defined(__x86_64__)
#error This example only works in Linux on x86-64.
#endif

#define  ALTSTACK_SIZE  262144

static const char hex_digit[16] = {
    '0', '1', '2', '3', '4', '5', '6', '7',
    '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};

static inline const char *signal_name(const int signum)
{
    switch (signum) {
    case SIGSEGV: return "SIGSEGV";
    case SIGBUS:  return "SIGBUS";
    case SIGILL:  return "SIGILL";
    case SIGFPE:  return "SIGFPE";
    case SIGTRAP: return "SIGTRAP";
    default:      return "(unknown)";
    }
}

static inline ssize_t internal_write(int fd, const void *buf, size_t len)
{
    ssize_t retval;
    asm volatile ( "syscall\n\t"
                 : "=a" (retval)
                 : "a" (1), "D" (fd), "S" (buf), "d" (len)
                 : "rcx", "r11" );
    return retval;
}

static inline int wrerr(const char *p, const char *q)
{
    while (p < q) {
        ssize_t n = internal_write(STDERR_FILENO, p, (size_t)(q - p));
        if (n > 0)
            p += n;
        else
        if (n == 0)
            return EIO;
        else
            return -n;
    }
    return 0;
}

static inline int wrs(const char *p)
{
    if (p) {
        const char *q = p;
        while (*q)
            q++;
        return wrerr(p, q);
    }
    return 0;
}

static inline int wrh(unsigned long h)
{
    static char buffer[4 + 2 * sizeof h];
    char       *p = buffer + sizeof buffer;

    do {
        *(--p) = hex_digit[h & 15];
        h /= 16UL;
    } while (h);

    *(--p) = 'x';
    *(--p) = '0';

    return wrerr(p, buffer + sizeof buffer);
}

static void crash_handler(int signum, siginfo_t *info, void *contextptr)
{
    if (info) {
        ucontext_t *const ctx = (ucontext_t *const)contextptr;
        wrs(signal_name(signum));
        if (ctx->uc_mcontext.gregs[REG_ERR] & 16) {
            const unsigned long sp = ctx->uc_mcontext.gregs[REG_RSP];
            /* Instruction fetch */
            wrs(": Bad jump to ");
            wrh((unsigned long)(info->si_addr));
            if (sp && !(sp & 7)) {
                wrs(" probably by the instruction just before ");
                wrh(*(unsigned long *)sp);
            }
            wrs(".\n");
        } else
        if (ctx->uc_mcontext.gregs[REG_ERR] & 2) {
            /* Write access */
            wrs(": Invalid write attempt to ");
            wrh((unsigned long)(info->si_addr));
            wrs(" by instruction at ");
            wrh(ctx->uc_mcontext.gregs[REG_RIP]);
            wrs(".\n");
        } else {
            /* Read access */
            wrs(": Invalid read attempt from ");
            wrh((unsigned long)(info->si_addr));
            wrs(" by instruction at ");
            wrh(ctx->uc_mcontext.gregs[REG_RIP]);
            wrs(".\n");
        }
    }

    raise(SIGKILL);
}

static int install_crash_handler(void)
{
    stack_t           altstack;
    struct sigaction  act;

    altstack.ss_size = ALTSTACK_SIZE;
    altstack.ss_flags = 0;
    altstack.ss_sp = mmap(NULL, altstack.ss_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN, -1, 0);
    if (altstack.ss_sp == MAP_FAILED) {
        const int retval = errno;
        fprintf(stderr, "Cannot map memory for alternate stack: %s.\n", strerror(retval));
        return retval;
    }
    if (sigaltstack(&altstack, NULL)) {
        const int retval = errno;
        fprintf(stderr, "Cannot use alternate signal stack: %s.\n", strerror(retval));
        return retval;
    }

    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_SIGINFO | SA_ONSTACK;
    act.sa_sigaction = crash_handler;
    if (sigaction(SIGSEGV, &act, NULL) == -1 ||
        sigaction(SIGBUS,  &act, NULL) == -1 ||
        sigaction(SIGILL,  &act, NULL) == -1 ||
        sigaction(SIGFPE,  &act, NULL) == -1) {
        const int retval = errno;
        fprintf(stderr, "Cannot install crash signal handlers: %s.\n", strerror(retval));
        return retval;
    }

    return 0;
}

int main(int argc, char *argv[])
{
    void         (*jump)(void) = 0;
    unsigned char *addr = (unsigned char *)0;

    if (argc < 2 || argc > 3 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s call [ address ]\n", argv[0]);
        fprintf(stderr, "       %s read [ address ]\n", argv[0]);
        fprintf(stderr, "       %s write [ address ]\n", argv[0]);
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    }
    if (argc > 2 && argv[2][0] != '[=10=]') {
        char          *end = NULL;
        unsigned long  val;

        errno = 0;
        val = strtoul(argv[2], &end, 0);
        if (errno) {
            fprintf(stderr, "%s: %s.\n", argv[2], strerror(errno));
            return EXIT_FAILURE;
        }
        if (end)
            while (*end == '\t' || *end == '\n' || *end == '\v' ||
                   *end == '\f' || *end == '\r' || *end == ' ')
                end++;
        if (!end || end <= argv[2] || *end) {
            fprintf(stderr, "%s: Not a valid address.\n", argv[2]);
            return EXIT_FAILURE;
        }

        jump = (void *)val;
        addr = (void *)val;
    }

    if (install_crash_handler())
        return EXIT_FAILURE;

    if (argv[1][0] == 'c' || argv[1][0] == 'C') {
        printf("Calling address %p: ", (void *)jump);
        fflush(stdout);
        jump();
        printf("Done.\n");

    } else
    if (argv[1][0] == 'r' || argv[1][0] == 'R') {
        unsigned char  val;

        printf("Reading from address %p: ", (void *)addr);
        fflush(stdout);
        val = *addr;
        printf("0x%02x, done.\n", val);

    } else
    if (argv[1][0] == 'w' || argv[1][1] == 'W') {
        printf("Writing 0xC4 to address %p: ", (void *)addr);
        fflush(stdout);
        *addr = 0xC4;
        printf("Done.\n");
    }

    printf("No crash.\n");
    return EXIT_SUCCESS;
}

使用例如

编译它
gcc -Wall -O2 crasher.c -o crasher

您可以通过在命令行上指定操作和可选地址来测试对任意地址的调用、读取或写入。 运行不带参数看用法

一些示例在我的机器上运行:

./crasher call 0x100
Calling address 0x100: SIGSEGV: Bad jump to 0x100 probably by the instruction just before 0x400c4e.
Killed

./crasher write 0x24
Writing 0xC4 to address 0x24: SIGSEGV: Invalid write attempt to 0x24 by instruction at 0x400bad.
Killed

./crasher read 0x16
Reading from address 0x16: SIGSEGV: Invalid read attempt from 0x16 by instruction at 0x400ca3.
Killed

./crasher write 0x400ca3
Writing 0xC4 to address 0x400ca3: SIGSEGV: Invalid write attempt to 0x400ca3 by instruction at 0x400bad.
Killed

./crasher read 0x400ca3
Reading from address 0x400ca3: 0x41, done.
No crash.

请注意,访问的类型是从 ((ucontext_t *)contextptr)->uc_mcontext.gregs[REG_ERR] 寄存器(来自信号处理程序上下文)中获得的;它匹配 arch/x86/mm/fault.c in the Linux kernel sources 中定义的 x86_pf_error_code 枚举。

崩溃处理程序本身非常简单,只需检查上述 "register" 即可获取 OP 寻求的信息。

为了输出崩溃报告,我对 write() 系统调用进行了开放编码。 (由于某些原因,wrh() 函数所需的小缓冲区不能在堆栈上,所以我只是将其设为静态。)

我没有费心去实现 mincore() 系统调用来验证堆栈地址(crash_handler() 函数中的 sp);可能有必要避免双重错误(SIGSEGV 发生在 crash_handler() 本身)。

同样,我没有费心去打开 crash_handler() 末尾的 raise() 代码,因为如今在 x86-64 上它是在 C 库中使用 tgkill(pid, tid, signum) 系统调用,这意味着我还必须打开 getpid()gettid() 系统调用的代码。我只是懒惰。

最后,上面的代码写得相当粗心,因为我自己是在与OP user2284570交换意见后才发现的,只是想把一些东西放在一起看看这种方法是否真的可靠。 (似乎是这样,但我只在一台机器上对此进行了轻微测试。)因此,如果您发现代码中有任何错误、错别字、想法或其他需要修复的问题,请在评论中告诉我,所以我可以修复它。