hus*_*ade 0 c linux assembly multithreading
假设我们有一个单线程程序,并且希望在发生预定义的中断(例如定时器中断)时捕获程序计数器(PC)的值。
如您所知,这似乎很容易,我们只需要使用一个特殊的关键字编写一个特定的汇编代码__asm__
,然后在将移位4字节后将值弹出堆栈顶部即可。
那多线程程序呢?
我们如何从同一进程中运行的另一个线程获取所有线程的值?(从运行在多核处理器中单独内核上的线程获取值似乎非常不可思议)。(在多线程程序中,每个线程都有其堆栈并进行注册)。
我想实现破坏分子。
为了在目标多线程程序中执行故障注入,故障模型为SEU(单错误翻转),这意味着程序计数器寄存器中的任意位被随机修改(位翻转),从而导致违反正确的程序顺序。因此,会发生控制流错误(CFE)。
由于我们的目标程序是一个多线程程序,因此我们必须在所有线程的PC上执行故障注入。这是破坏分子的任务。它应该能够获得线程的PC来执行故障注入。假设我们有这段代码,
main ()
{
foo
}
void foo()
{
__asm__{
pop "%eax"
pop "%ebx" // now ebx holds porgram counter value (for main thread)
// her code injection like 00000111 XOR ebx for example
push ...
push ...
};
}
Run Code Online (Sandbox Code Playgroud)
如果我们的程序是多线程程序。这是否意味着我们有多个堆栈?
当OS执行上下文切换时,这意味着正在运行的线程的堆栈和寄存器已移动到内存中的某个位置。这是否意味着如果我们要获取这些线程的程序计数器的值,就可以在内存中找到它们?哪里?并且在运行时有可能吗?
当您在标志中使用sigaction()
with 安装信号处理程序时SA_SIGINFO
,信号处理程序获得的第二个参数是的指针siginfo_t
,而第三个参数是的指针ucontext_t
。在Linux中,此结构除其他外包含内核中断线程时的寄存器值集,包括程序计数器。
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <signal.h>
#include <ucontext.h>
#if defined(__x86_64__)
#define PROGCOUNTER(ctx) (((ucontext *)ctx)->uc_mcontext.greg[REG_RIP])
#elif defined(__i386__)
#define PROGCOUNTER(ctx) (((ucontext *)ctx)->uc_mcontext.greg[REG_EIP])
#else
#error Unsupported architecture.
#endif
void signal_handler(int signum, siginfo_t *info, void *context)
{
const size_t program_counter = PROGCOUNTER(context);
/* Do something ... */
}
Run Code Online (Sandbox Code Playgroud)
与往常一样,printf()等。不是异步信号安全的,这意味着在信号处理程序中使用它们并不安全。如果希望将程序计数器输出到例如标准错误,则不应使用任何标准I / O进行打印stderr
,而应构造要手工打印的字符串,并使用循环来write()
处理字符串的内容。例如,
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
static void wrerr(const char *p)
{
const int saved_errno = errno;
const char *q = p;
ssize_t n;
/* Nothing to print? */
if (!p || !*p)
return;
/* Find end of q. strlen() is not async-signal safe. */
while (*q) q++;
/* Write data from p to q. */
while (p < q) {
n = write(STDERR_FILENO, p, (size_t)(q - p));
if (n > 0)
p += n;
else
if (n != -1 || errno != EINTR)
break;
}
errno = saved_errno;
}
Run Code Online (Sandbox Code Playgroud)
请注意,您将希望errno
在信号处理程序中保持值的不变,这样,如果在失败的库函数后被中断,则被中断的线程仍会看到正确的errno
值。(这主要是调试问题,并且是“良好形式”;一些白痴po之以鼻,因为“这种情况经常发生,不足以让我担心”)。
您的程序可以检查/proc/self/maps
伪文件(它不是真实文件,而是内核在读取文件时动态生成的东西),以查看程序使用的内存区域,以确定程序是否正在运行C库函数。 (非常常见)或传送中断时的其他资讯。
如果要中断多线程程序中的特定线程,请使用pthread_kill()
。否则,信号或多或少随机地传递到尚未阻塞信号的线程之一。
这是一个示例程序,当使用GCC-4.8.4进行编译时,使用-Wall -O2
以下命令在x86-64(AMD64)和x86中进行了测试:
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <ucontext.h>
#include <time.h>
#include <stdio.h>
#if defined(__x86_64__)
#define PROGRAM_COUNTER(mctx) ((mctx).gregs[REG_RIP])
#define STACK_POINTER(mctx) ((mctx).gregs[REG_RSP])
#elif defined(__i386__)
#define PROGRAM_COUNTER(mctx) ((mctx).gregs[REG_EIP])
#define STACK_POINTER(mctx) ((mctx).gregs[REG_ESP])
#else
#error Unsupported hardware architecture.
#endif
#define MAX_SIGNALS 64
#define MCTX(ctx) (((ucontext_t *)ctx)->uc_mcontext)
static void wrerr(const char *p, const char *q)
{
while (p < q) {
ssize_t n = write(STDERR_FILENO, p, (size_t)(q - p));
if (n > 0)
p += n;
else
if (n != -1 || errno != EINTR)
break;
}
}
static const char hexc[16] = "0123456789abcdef";
static inline char *prehex(char *before, size_t value)
{
do {
*(--before) = hexc[value & 15];
value /= (size_t)16;
} while (value);
*(--before) = 'x';
*(--before) = '0';
return before;
}
static volatile sig_atomic_t done = 0;
static void handle_done(int signum)
{
done = signum;
}
static int install_done(const int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_handler = handle_done;
act.sa_flags = 0;
if (sigaction(signum, &act, NULL) == -1)
return errno;
return 0;
}
static size_t jump_target[MAX_SIGNALS] = { 0 };
static size_t jump_stack[MAX_SIGNALS] = { 0 };
static void handle_jump(int signum, siginfo_t *info, void *context)
{
const int saved_errno = errno;
char buffer[128];
char *p = buffer + sizeof buffer;
*(--p) = '\n';
p = prehex(p, STACK_POINTER(MCTX(context)));
*(--p) = ' ';
*(--p) = 'k';
*(--p) = 'c';
*(--p) = 'a';
*(--p) = 't';
*(--p) = 's';
*(--p) = ' ';
*(--p) = ',';
p = prehex(p, PROGRAM_COUNTER(MCTX(context)));
*(--p) = ' ';
*(--p) = '@';
wrerr(p, buffer + sizeof buffer);
if (signum >= 0 && signum < MAX_SIGNALS) {
if (jump_target[signum])
PROGRAM_COUNTER(MCTX(context)) = jump_target[signum];
if (jump_stack[signum])
STACK_POINTER(MCTX(context)) = jump_stack[signum];
}
errno = saved_errno;
}
static int install_jump(const int signum, void *target, size_t stack)
{
struct sigaction act;
if (signum < 0 || signum >= MAX_SIGNALS)
return errno = EINVAL;
jump_target[signum] = (size_t)target;
jump_stack[signum] = (size_t)stack;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_sigaction = handle_jump;
act.sa_flags = SA_SIGINFO;
if (sigaction(signum, &act, NULL) == -1)
return errno;
return 0;
}
int main(int argc, char *argv[])
{
const struct timespec sec = { .tv_sec = 1, .tv_nsec = 0L };
const int pid = (int)getpid();
ucontext_t ctx;
printf("Run\n");
printf("\tkill -KILL %d\n", pid);
printf("\tkill -TERM %d\n", pid);
printf("\tkill -HUP %d\n", pid);
printf("\tkill -INT %d\n", pid);
printf("or press Ctrl+C to stop this process, or\n");
printf("\tkill -USR1 %d\n", pid);
printf("\tkill -USR2 %d\n", pid);
printf("to send the respective signal to this process.\n");
fflush(stdout);
if (install_done(SIGTERM) ||
install_done(SIGHUP) ||
install_done(SIGINT) ) {
printf("Cannot install signal handlers: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
getcontext(&ctx);
if (install_jump(SIGUSR1, &&usr1_target, STACK_POINTER(MCTX(&ctx))) ||
install_jump(SIGUSR2, &&usr2_target, STACK_POINTER(MCTX(&ctx))) ) {
printf("Cannot install signal handlers: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
/* These are expressions that should evaluate to false, but the compiler
* should not be able to optimize them away. */
if (argv[0][1] == 'A') {
usr1_target:
fputs("USR1\n", stdout);
fflush(stdout);
}
if (argv[0][1] == 'B') {
usr2_target:
fputs("USR2\n", stdout);
fflush(stdout);
}
while (!done) {
putchar('.');
fflush(stdout);
nanosleep(&sec, NULL);
}
fputs("\nAll done.\n", stdout);
fflush(stdout);
return EXIT_SUCCESS;
}
Run Code Online (Sandbox Code Playgroud)
如果将以上内容另存为example.c
,则可以使用进行编译
gcc -Wall -O2 example.c -o example
Run Code Online (Sandbox Code Playgroud)
并运行它
./example
Run Code Online (Sandbox Code Playgroud)
按Ctrl+ C退出程序。复制命令(用于发送SIGUSR1
和发送SIGUSR2
信号),然后在另一个窗口中运行它们,您将看到它们修改了当前执行的位置。(这些信号使程序计数器/指令指针跳回if子句,否则不应执行该子句。)
有两组信号处理程序。handle_done()
只是设置done
标志。handle_jump()
将消息输出到标准错误(使用低级I / O),如果指定,则更新程序计数器(指令指针)和堆栈指针。
创建这样的示例程序时,堆栈指针是棘手的部分。如果我们只对程序崩溃感到满意,那将很容易。然而,一个例子是,如果它唯一有用的工作。
当我们任意更改程序计数器/指令指针,并且在函数调用(大多数C库函数...)中传递了中断时,返回地址保留在堆栈中。内核可以在任何时候传递中断,因此我们甚至都不能假设在函数调用中传递了中断!因此,为确保测试程序不会崩溃,我必须将程序计数器/指令指针和堆栈指针成对更新。
收到跳转信号后,堆栈指针将重置为使用所获得的值getcontext()
。不能保证它适合任何跳跃位置;对于一个最小的例子,这是我所能做的最好的事情。我绝对认为跳转标签在附近,而不是在子范围内,在此范围内编译器可能会弄乱堆栈,请注意。
同样重要的是要记住,因为我们正在处理留给C编译器的细节,所以我们必须遵循编译器生成的任何二进制代码,而不是相反。为了对流程及其线程进行可靠的操作,ptrace()是一个更好(并且坦率地说更容易)的接口。您只需设置一个父进程,然后在目标跟踪的子进程中显式允许该跟踪。我在此处和此处(有关同一问题的两个答案)都展示了有关如何在目标进程中启动,停止和单步执行单个线程的示例。最难的部分是了解整体方案,概念;代码本身比这种信号处理程序上下文操纵方式更容易-而且更坚固。
对于自我引入的寄存器错误(程序计数器/指令指针或任何其他寄存器),假设大多数情况下导致进程崩溃,则此信号处理程序上下文操作应足够。