如您所知,除了使用处理程序执行kill和stop/count之外,还可以捕获任何信号。
有三种无效地址访问:
试图在无效地址执行/跳转。
试图读取无效地址。
试图在无效地址写入。
我只对拒绝无效的读取访问感兴趣。因此,我们的想法是捕获所有分段错误,如果不是无效的读取访问,则捕获abort()
。
到目前为止,我只知道如何使用SEGV_MAPERR
和SEGV_ACCERR
与sigaction
无关。
最佳答案
事实证明,在x86-64(又称AMD64)架构的Linux中,这实际上是非常可行的。
下面是一个示例程序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] != '\0') {
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]
寄存器(从信号处理程序上下文)获得的;它与x86_pf_error_code
in the Linux kernel sources中定义的arch/x86/mm/fault.c
枚举匹配。崩溃处理程序本身非常简单,只需要对前面提到的“register”进行exmine,就可以获得OP所要查找的信息。
为了输出崩溃报告,我打开了代码
write()
系统调用。(由于某些原因,wrh()
函数所需的小缓冲区不能在堆栈上,所以我只是将其设为静态。)我没有费心实现
mincore()
系统调用来验证堆栈地址(sp
函数中的crash_handler()
);可能需要避免双重错误(SIGSEGV
本身发生)。类似地,我不必在
crash_handler()
结尾打开raise()
代码,因为现在在x86-64上,它是在使用crash_handler()
系统调用的C库中实现的,这意味着我还必须打开tgkill(pid, tid, signum)
和getpid()
系统调用的代码。我只是懒惰。最后,上面的代码编写得很粗心,因为我自己是在与OP user2284570交换了注释之后才发现这一点的,只是想把一些东西放在一起,看看这种方法是否真正可靠地工作。(看起来确实如此,但我只是在一台机器上进行了简单的测试。)因此,如果您注意到代码中有任何错误、拼写错误、thinkos或其他需要修复的地方,请在评论中告诉我,这样我就可以修复它。
关于c - 从内部捕获SIGSEGV时,如何知道所涉及的无效访问类型?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/43033177/