我今天一直在玩,我想知道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";
}
经过一番研究后,我意识到存在一些重大问题
我不熟悉arm64汇编或c/c++内联汇编(该指令看起来正确吗?):)
因为 Linux 将自身插入到 CPU 异常和用户空间端信号处理程序之间(如
signal(7)
提到的),signal_handler
运行时的堆栈/帧/LR 指针很可能在 signal_handler
与 *root_ptr
不同
类似地,所有其他寄存器也可以也不同!
信号处理程序在信号上下文中运行,这是有限制的。
我认为只需在适当的时刻将寄存器保存/恢复到内存中的静态位置即可解决(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
所以我有两个问题
while(true)
?+4
,而不是 +8
。b %0
程序集无法编译。 b
用于直接分支(即通过标签/PC 相对偏移量),用于间接分支(即通过寄存器)您想要的 br
。setjmp
/longjmp
实现。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
会调用未定义的行为,并且可能会因打开优化而中断,等等。