Arm64/Linux:通过跳转到(PC + 8)来“处理”用户空间程序中的信号?

问题描述 投票:0回答:1

我今天一直在玩,我想知道Linux用户空间程序是否可以通过跳过有问题的指令来“处理”信号。原型只是

#include <iostream>
#include <csignal>

void signal_handler(int signal, siginfo_t* info, void* unused) {
        if (signal != SIGSEGV) {
                // Just here in case, I've never seen this hit
                std::cerr << "Got an unexpected signal " << signal << std::endl;
                exit(1);
        }

        std::cout << "Got a SIGSEGV with si_addr = " << info->si_addr << std::endl;
        // Without doing anything more here... the failing access will continue to SIGSEGV forever!
        __asm__ volatile (
                "b %0" : : "r" (info->si_addr + 8)
        );
        return;
}

int main() {
        struct sigaction action;
        memset(&action, 0, sizeof(action));
        action.sa_flags = SA_SIGINFO;
        action.sa_sigaction = signal_handler;

        sigaction(SIGSEGV, &action, nullptr);


        const volatile uint64_t* root_ptr = reinterpret_cast<uint64_t*>(0xdaaf3254 << 12);
        // Try to access this: it's probably a segfault due to the memory not being
        // backed by Linux yet!
        *root_ptr;
        std::cout << "Successfully got past the failing access\n";
}

经过一番研究后,我意识到存在一些重大问题

  1. 我不熟悉arm64汇编c/c++内联汇编(该指令看起来正确吗?):)

  2. 因为 Linux 将自身插入到 CPU 异常和用户空间端信号处理程序之间(如

    signal(7)
    提到的),
    signal_handler
    运行时的堆栈/帧/LR 指针很可能在
    signal_handler
    *root_ptr
    不同

  3. 类似地,所有其他寄存器也可以不同!

  4. 信号处理程序在信号上下文中运行,这是有限制的。

我认为只需在适当的时刻将寄存器保存/恢复到内存中的静态位置即可解决(2)和(3)。我的猜测是,如果用户空间代码可以安全地在信号上下文中运行(比如处理一些数字,或者只是永远旋转),则(4)不会起作用。

那么,我们稍微修改一下上面的程序

#include <iostream>
#include <csignal>

static volatile uint64_t register_save_set[32];

// Lines 2-3 save the stack pointer, which is a special snowflake
#define SAVE_REGISTERS() do {          \
    __asm__ volatile (                 \
        "stp x0, x1, [%0]\n"          \
        "mov x0, sp\n"                \
        "str x0, [%0, #248]\n"       \
        "stp x2, x3, [%0, #16]\n"     \
        "stp x4, x5, [%0, #32]\n"     \
        "stp x6, x7, [%0, #48]\n"     \
        "stp x8, x9, [%0, #64]\n"     \
        "stp x10, x11, [%0, #80]\n"   \
        "stp x12, x13, [%0, #96]\n"   \
        "stp x14, x15, [%0, #112]\n"  \
        "stp x16, x17, [%0, #128]\n"  \
        "stp x18, x19, [%0, #144]\n"  \
        "stp x20, x21, [%0, #160]\n"  \
        "stp x22, x23, [%0, #176]\n"  \
        "stp x24, x25, [%0, #192]\n"  \
        "stp x26, x27, [%0, #208]\n"  \
        "stp x28, x29, [%0, #224]\n"  \
        "str x30, [%0, #240]\n"       \
        :                               \
        : "r" (register_save_set)     \
        : "memory", "x0"              \
    );                                  \
} while (0)

// Lines 1-2 load the stack pointer, which is a special snowflake
#define LOAD_REGISTERS() do {          \
    __asm__ volatile (                 \
        "ldr x0, [%0, #248]\n"        \
        "mov sp, x0\n"                \
        "ldp x0, x1, [%0]\n"          \
        "ldp x2, x3, [%0, #16]\n"     \
        "ldp x4, x5, [%0, #32]\n"     \
        "ldp x6, x7, [%0, #48]\n"     \
        "ldp x8, x9, [%0, #64]\n"     \
        "ldp x10, x11, [%0, #80]\n"   \
        "ldp x12, x13, [%0, #96]\n"   \
        "ldp x14, x15, [%0, #112]\n"  \
        "ldp x16, x17, [%0, #128]\n"  \
        "ldp x18, x19, [%0, #144]\n"  \
        "ldp x20, x21, [%0, #160]\n"  \
        "ldp x22, x23, [%0, #176]\n"  \
        "ldp x24, x25, [%0, #192]\n"  \
        "ldp x26, x27, [%0, #208]\n"  \
        "ldp x28, x29, [%0, #224]\n"  \
        "ldr x30, [%0, #240]\n"       \
        :                               \
        : "r" (register_save_set)     \
        :                     \
    );                                  \
} while (0)
// These should be "clobber" in above... but GCC yells at me about
// impossible constraints :)
// "sp", "x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9", "x10", "x11", "x12", "x13", "x14", "x15", "x16", "x17", "x18", "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x27", "x28", "x29", "x30"

void signal_handler(int signal, siginfo_t* info, void* unused) {
        if (signal != SIGSEGV) {
                std::cerr << "Got an unexpected signal " << signal << std::endl;
                exit(1);
        }

        std::cout << "Got a SIGSEGV with si_addr = " << info->si_addr << std::endl;
        // Without doing anything more here... the failing access will continue to SIGSEGV forever!
        LOAD_REGISTERS();
        __asm__ volatile (
                "br %0" : : "r" (info->si_addr + 8)
        );
        return;
}

int main() {
        struct sigaction action;
        memset(&action, 0, sizeof(action));
        action.sa_flags = SA_SIGINFO;
        action.sa_sigaction = signal_handler;

        sigaction(SIGSEGV, &action, nullptr);

        const volatile uint64_t* root_ptr = reinterpret_cast<uint64_t*>(0xdaaf3254 << 12);
        SAVE_REGISTERS();
        *root_ptr;
        volatile int spin = 0;
        while (true) { spin += 1; }
}

我非常确定内联汇编上的输入/输出约束......只是错误的,但我希望通过使所有内容都变得“易失性”,我可以回避这一点。

无论如何,根据我的理解:这应该“正常工作”,即点击

while
循环并永远旋转。但另一方面,当我真正尝试时

$ g++ $PROGRAM -o a.out && ./a.out
Got a SIGSEGV with si_addr = 0xf3254000   # Expected
zsh: bus error  ./build/a.out             # NOT expected

所以我有两个问题

  1. 我这里的概念理解正确吗?如果没有,我错过了什么?
  2. 如果它(或多或少)正确,为什么程序没有按预期达到
    while(true)
linux arm signals arm64
1个回答
0
投票
  1. 要跳过一条指令,您需要使用
    +4
    ,而不是
    +8
  2. 您的
    b %0
    程序集无法编译。
    b
    用于直接分支(即通过标签/PC 相对偏移量),用于间接分支(即通过寄存器)您想要的
    br
  3. 编写干扰编译器分配的寄存器并可能在任何地方分支的内联汇编是一件令人难以忍受的具有挑战性的事情,要正确完成 - 在函数边界更容易做到。
  4. 除了通用寄存器之外,您还必须保存和恢复 FP/SIMD 寄存器...
  5. 但是一旦你完成了这一切,你就到达了你的自制程序
    setjmp
    /
    longjmp
    实现。
  6. 甚至来自信号处理程序的
    longjmp
    仍然是一个非常糟糕的主意。

但是您确实不需要跳过任何这些障碍,甚至不需要编写任何程序集。您只需要利用您的

void* unused
。你在那里得到了完整的寄存器状态!只需包含
<ucontext.h>
,投射到
ucontext_t*
并根据需要进行修改。

我修补了你的代码来做到这一点:

#include <iostream>
#include <csignal>
#include <ucontext.h>

void signal_handler(int signal, siginfo_t *info, void *context) {
    if (signal != SIGSEGV) {
        // Just here in case, I've never seen this hit
        std::cerr << "Got an unexpected signal " << signal << std::endl;
        exit(1);
    }

    std::cout << "Got a SIGSEGV with si_addr = " << info->si_addr << std::endl;
    ((ucontext_t*)context)->uc_mcontext.pc += 4;
}

int main() {
    struct sigaction action = {};
    action.sa_flags = SA_SIGINFO;
    action.sa_sigaction = signal_handler;
    sigaction(SIGSEGV, &action, nullptr);

    const volatile uint64_t* root_ptr = reinterpret_cast<uint64_t*>(0xdaaf3254 << 12);
    // Try to access this: it's probably a segfault due to the memory not being
    // backed by Linux yet!
    *root_ptr;
    std::cout << "Successfully got past the failing access\n";
}

必须注意,

*root_ptr
会调用未定义的行为,并且可能会因打开优化而中断,等等。

© www.soinside.com 2019 - 2024. All rights reserved.