Ker*_* SB 12 c linux multithreading ptrace pthreads
当我正在研究这个问题时,我遇到了一个可能的想法ptrace
,但是我无法正确理解ptrace
与线程的交互方式.
假设我有一个给定的多线程主进程,我想附加到其中的特定线程(可能来自一个分叉的子进程).
我可以附加到特定的帖子吗?(手册在这个问题上有所不同.)
如果是这样,这是否意味着单步执行只会逐步完成一个线程的指令?它是否会停止所有进程的线程?
如果是这样,而我呼吁所有其他线程保持停止PTRACE_SYSCALL
或PTRACE_SINGLESTEP
,或者说所有的线程继续?有没有办法只在一个线程中前进,但保证其他线程保持停止状态?
基本上,我想通过强制所有线程停止来同步原始程序,然后通过单步执行一个跟踪线程来执行一小组单线程指令.
到目前为止,我的个人尝试看起来有点像这样:
pid_t target = syscall(SYS_gettid); // get the calling thread's ID
pid_t pid = fork();
if (pid > 0)
{
waitpid(pid, NULL, 0); // synchronise main process
important_instruction();
}
else if (pid == 0)
{
ptrace(target, PTRACE_ATTACH, NULL, NULL); // does this work?
// cancel parent's "waitpid" call, e.g. with a signal
// single-step to execute "important_instruction()" above
ptrace(target, PTRACE_DETACH, NULL, NULL); // parent's threads resume?
_Exit(0);
}
Run Code Online (Sandbox Code Playgroud)
但是,我不确定,并且找不到合适的引用,这是同时正确的,并且important_instruction()
只有在所有其他线程都停止时才能保证执行.我也明白,当父母收到来自其他地方的信号时,可能会有竞争条件,我听说我应该使用PTRACE_SEIZE
,但这似乎并不存在.
任何澄清或参考将不胜感激!
Nom*_*mal 24
I wrote a second test case. I had to add a separate answer, since it was too long to fit into the first one with example output included.
First, here is tracer.c
:
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ptrace.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <dirent.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <stdio.h>
#ifndef SINGLESTEPS
#define SINGLESTEPS 10
#endif
/* Similar to getline(), except gets process pid task IDs.
* Returns positive (number of TIDs in list) if success,
* otherwise 0 with errno set. */
size_t get_tids(pid_t **const listptr, size_t *const sizeptr, const pid_t pid)
{
char dirname[64];
DIR *dir;
pid_t *list;
size_t size, used = 0;
if (!listptr || !sizeptr || pid < (pid_t)1) {
errno = EINVAL;
return (size_t)0;
}
if (*sizeptr > 0) {
list = *listptr;
size = *sizeptr;
} else {
list = *listptr = NULL;
size = *sizeptr = 0;
}
if (snprintf(dirname, sizeof dirname, "/proc/%d/task/", (int)pid) >= (int)sizeof dirname) {
errno = ENOTSUP;
return (size_t)0;
}
dir = opendir(dirname);
if (!dir) {
errno = ESRCH;
return (size_t)0;
}
while (1) {
struct dirent *ent;
int value;
char dummy;
errno = 0;
ent = readdir(dir);
if (!ent)
break;
/* Parse TIDs. Ignore non-numeric entries. */
if (sscanf(ent->d_name, "%d%c", &value, &dummy) != 1)
continue;
/* Ignore obviously invalid entries. */
if (value < 1)
continue;
/* Make sure there is room for another TID. */
if (used >= size) {
size = (used | 127) + 128;
list = realloc(list, size * sizeof list[0]);
if (!list) {
closedir(dir);
errno = ENOMEM;
return (size_t)0;
}
*listptr = list;
*sizeptr = size;
}
/* Add to list. */
list[used++] = (pid_t)value;
}
if (errno) {
const int saved_errno = errno;
closedir(dir);
errno = saved_errno;
return (size_t)0;
}
if (closedir(dir)) {
errno = EIO;
return (size_t)0;
}
/* None? */
if (used < 1) {
errno = ESRCH;
return (size_t)0;
}
/* Make sure there is room for a terminating (pid_t)0. */
if (used >= size) {
size = used + 1;
list = realloc(list, size * sizeof list[0]);
if (!list) {
errno = ENOMEM;
return (size_t)0;
}
*listptr = list;
*sizeptr = size;
}
/* Terminate list; done. */
list[used] = (pid_t)0;
errno = 0;
return used;
}
static int wait_process(const pid_t pid, int *const statusptr)
{
int status;
pid_t p;
do {
status = 0;
p = waitpid(pid, &status, WUNTRACED | WCONTINUED);
} while (p == (pid_t)-1 && errno == EINTR);
if (p != pid)
return errno = ESRCH;
if (statusptr)
*statusptr = status;
return errno = 0;
}
static int continue_process(const pid_t pid, int *const statusptr)
{
int status;
pid_t p;
do {
if (kill(pid, SIGCONT) == -1)
return errno = ESRCH;
do {
status = 0;
p = waitpid(pid, &status, WUNTRACED | WCONTINUED);
} while (p == (pid_t)-1 && errno == EINTR);
if (p != pid)
return errno = ESRCH;
} while (WIFSTOPPED(status));
if (statusptr)
*statusptr = status;
return errno = 0;
}
void show_registers(FILE *const out, pid_t tid, const char *const note)
{
struct user_regs_struct regs;
long r;
do {
r = ptrace(PTRACE_GETREGS, tid, ®s, ®s);
} while (r == -1L && errno == ESRCH);
if (r == -1L)
return;
#if (defined(__x86_64__) || defined(__i386__)) && __WORDSIZE == 64
if (note && *note)
fprintf(out, "Task %d: RIP=0x%016lx, RSP=0x%016lx. %s\n", (int)tid, regs.rip, regs.rsp, note);
else
fprintf(out, "Task %d: RIP=0x%016lx, RSP=0x%016lx.\n", (int)tid, regs.rip, regs.rsp);
#elif (defined(__x86_64__) || defined(__i386__)) && __WORDSIZE == 32
if (note && *note)
fprintf(out, "Task %d: EIP=0x%08lx, ESP=0x%08lx. %s\n", (int)tid, regs.eip, regs.esp, note);
else
fprintf(out, "Task %d: EIP=0x%08lx, ESP=0x%08lx.\n", (int)tid, regs.eip, regs.esp);
#endif
}
int main(int argc, char *argv[])
{
pid_t *tid = 0;
size_t tids = 0;
size_t tids_max = 0;
size_t t, s;
long r;
pid_t child;
int status;
if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
fprintf(stderr, " %s COMMAND [ ARGS ... ]\n", argv[0]);
fprintf(stderr, "\n");
fprintf(stderr, "This program executes COMMAND in a child process,\n");
fprintf(stderr, "and waits for it to stop (via a SIGSTOP signal).\n");
fprintf(stderr, "When that occurs, the register state of each thread\n");
fprintf(stderr, "is dumped to standard output, then the child process\n");
fprintf(stderr, "is sent a SIGCONT signal.\n");
fprintf(stderr, "\n");
return 1;
}
child = fork();
if (child == (pid_t)-1) {
fprintf(stderr, "fork() failed: %s.\n", strerror(errno));
return 1;
}
if (!child) {
prctl(PR_SET_DUMPABLE, (long)1);
prctl(PR_SET_PTRACER, (long)getppid());
fflush(stdout);
fflush(stderr);
execvp(argv[1], argv + 1);
fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
return 127;
}
fprintf(stderr, "Tracer: Waiting for child (pid %d) events.\n\n", (int)child);
fflush(stderr);
while (1) {
/* Wait for a child event. */
if (wait_process(child, &status))
break;
/* Exited? */
if (WIFEXITED(status) || WIFSIGNALED(status)) {
errno = 0;
break;
}
/* At this point, only stopped events are interesting. */
if (!WIFSTOPPED(status))
continue;
/* Obtain task IDs. */
tids = get_tids(&tid, &tids_max, child);
if (!tids)
break;
printf("Process %d has %d tasks,", (int)child, (int)tids);
fflush(stdout);
/* Attach to all tasks. */
for (t = 0; t < tids; t++) {
do {
r = ptrace(PTRACE_ATTACH, tid[t], (void *)0, (void *)0);
} while (r == -1L && (errno == EBUSY || errno == EFAULT || errno == ESRCH));
if (r == -1L) {
const int saved_errno = errno;
while (t-->0)
do {
r = ptrace(PTRACE_DETACH, tid[t], (void *)0, (void *)0);
} while (r == -1L && (errno == EBUSY || errno == EFAULT || errno == ESRCH));
tids = 0;
errno = saved_errno;
break;
}
}
if (!tids) {
const int saved_errno = errno;
if (continue_process(child, &status))
break;
printf(" failed to attach (%s).\n", strerror(saved_errno));
fflush(stdout);
if (WIFCONTINUED(status))
continue;
errno = 0;
break;
}
printf(" attached to all.\n\n");
fflush(stdout);
/* Dump the registers of each task. */
for (t = 0; t < tids; t++)
show_registers(stdout, tid[t], "");
printf("\n");
fflush(stdout);
for (s = 0; s < SINGLESTEPS; s++) {
do {
r = ptrace(PTRACE_SINGLESTEP, tid[tids-1], (void *)0, (void *)0);
} while (r == -1L && errno == ESRCH);
if (!r) {
for (t = 0; t < tids - 1; t++)
show_registers(stdout, tid[t], "");
show_registers(stdout, tid[tids-1], "Advanced by one step.");
printf("\n");
fflush(stdout);
} else {
fprintf(stderr, "Single-step failed: %s.\n", strerror(errno));
fflush(stderr);
}
}
/* Detach from all tasks. */
for (t = 0; t < tids; t++)
do {
r = ptrace(PTRACE_DETACH, tid[t], (void *)0, (void *)0);
} while (r == -1 && (errno == EBUSY || errno == EFAULT || errno == ESRCH));
tids = 0;
if (continue_process(child, &status))
break;
if (WIFCONTINUED(status)) {
printf("Detached. Waiting for new stop events.\n\n");
fflush(stdout);
continue;
}
errno = 0;
break;
}
if (errno)
fprintf(stderr, "Tracer: Child lost (%s)\n", strerror(errno));
else
if (WIFEXITED(status))
fprintf(stderr, "Tracer: Child exited (%d)\n", WEXITSTATUS(status));
else
if (WIFSIGNALED(status))
fprintf(stderr, "Tracer: Child died from signal %d\n", WTERMSIG(status));
else
fprintf(stderr, "Tracer: Child vanished\n");
fflush(stderr);
return status;
}
Run Code Online (Sandbox Code Playgroud)
tracer.c
executes the specified command, waiting for the command to receive a SIGSTOP
signal. (tracer.c
does not send it itself; you can either have the tracee stop itself, or send the signal externally.)
When the command has stopped, tracer.c
attaches a ptrace to every thread, and single-steps one of the threads a fixed number of steps (SINGLESTEPS
compile-time constant), showing the pertinent register state for each thread.
After that, it detaches from the command, and sends it a SIGCONT
signal to let it continue its operation normally.
Here is a simple test program, worker.c
, I used for testing:
#include <pthread.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#ifndef THREADS
#define THREADS 2
#endif
volatile sig_atomic_t done = 0;
void catch_done(int signum)
{
done = signum;
}
int install_done(const int signum)
{
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_handler = catch_done;
act.sa_flags = 0;
if (sigaction(signum, &act, NULL))
return errno;
else
return 0;
}
void *worker(void *data)
{
volatile unsigned long *const counter = data;
while (!done)
__sync_add_and_fetch(counter, 1UL);
return (void *)(unsigned long)__sync_or_and_fetch(counter, 0UL);
}
int main(void)
{
unsigned long counter = 0UL;
pthread_t thread[THREADS];
pthread_attr_t attrs;
size_t i;
if (install_done(SIGHUP) ||
install_done(SIGTERM) ||
install_done(SIGUSR1)) {
fprintf(stderr, "Worker: Cannot install signal handlers: %s.\n", strerror(errno));
return 1;
}
pthread_attr_init(&attrs);
pthread_attr_setstacksize(&attrs, 65536);
for (i = 0; i < THREADS; i++)
if (pthread_create(&thread[i], &attrs, worker, &counter)) {
done = 1;
fprintf(stderr, "Worker: Cannot create thread: %s.\n", strerror(errno));
return 1;
}
pthread_attr_destroy(&attrs);
/* Let the original thread also do the worker dance. */
worker(&counter);
for (i = 0; i < THREADS; i++)
pthread_join(thread[i], NULL);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
Compile both using e.g.
gcc -W -Wall -O3 -fomit-frame-pointer worker.c -pthread -o worker
gcc -W -Wall -O3 -fomit-frame-pointer tracer.c -o tracer
Run Code Online (Sandbox Code Playgroud)
and run either in a separate terminal, or on the background, using e.g.
./tracer ./worker &
Run Code Online (Sandbox Code Playgroud)
The tracer shows the PID of the worker:
Tracer: Waiting for child (pid 24275) events.
Run Code Online (Sandbox Code Playgroud)
At this point, the child is running normally. The action starts when you send a SIGSTOP
to the child. The tracer detects it, does the desired tracing, then detaches and lets the child continue normally:
kill -STOP 24275
Process 24275 has 3 tasks, attached to all.
Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a5d, RSP=0x00007f399cfa6ee8.
Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a5d, RSP=0x00007f399cfa6ee8. Advanced by one step.
Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a63, RSP=0x00007f399cfa6ee8. Advanced by one step.
Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a65, RSP=0x00007f399cfa6ee8. Advanced by one step.
Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a58, RSP=0x00007f399cfa6ee8. Advanced by one step.
Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a5d, RSP=0x00007f399cfa6ee8. Advanced by one step.
Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a63, RSP=0x00007f399cfa6ee8. Advanced by one step.
Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a65, RSP=0x00007f399cfa6ee8. Advanced by one step.
Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a58, RSP=0x00007f399cfa6ee8. Advanced by one step.
Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a5d, RSP=0x00007f399cfa6ee8. Advanced by one step.
Task 24275: RIP=0x0000000000400a5d, RSP=0x00007fff6895c428.
Task 24276: RIP=0x0000000000400a5d, RSP=0x00007f399cfb7ee8.
Task 24277: RIP=0x0000000000400a63, RSP=0x00007f399cfa6ee8. Advanced by one step.
Detached. Waiting for new stop events.
Run Code Online (Sandbox Code Playgroud)
You can repeat the above as many times as you wish. Note that I picked the SIGSTOP
signal as the trigger, because this way tracer.c
is also useful as a basis for generating complex multithreaded core dumps per request (as the multithreaded process can simply trigger it by sending itself a SIGSTOP
).
The disassembly of the worker()
function the threads are all spinning in the above example:
0x400a50: eb 0b jmp 0x400a5d
0x400a52: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
0x400a58: f0 48 83 07 01 lock addq $0x1,(%rdi) = fourth step
0x400a5d: 8b 05 00 00 00 00 mov 0x0(%rip),%eax = first step
0x400a63: 85 c0 test %eax,%eax = second step
0x400a65: 74 f1 je 0x400a58 = third step
0x400a67: 48 8b 07 mov (%rdi),%rax
0x400a6a: 48 89 c2 mov %rax,%rdx
0x400a6d: f0 48 0f b1 07 lock cmpxchg %rax,(%rdi)
0x400a72: 75 f6 jne 0x400a6a
0x400a74: 48 89 d0 mov %rdx,%rax
0x400a77: c3 retq
Run Code Online (Sandbox Code Playgroud)
Now, this test program does only show how to stop a process, attach to all of its threads, single-step one of the threads a desired number of instructions, then letting all the threads continue normally; it does not yet prove that the same applies for letting specific threads continue normally (via PTRACE_CONT
). However, the detail I describe below indicates, to me, that the same approach should work fine for PTRACE_CONT
.
The main problem or surprise I encountered while writing the above test programs was the necessity of the
long r;
do {
r = ptrace(PTRACE_cmd, tid, ...);
} while (r == -1L && (errno == EBUSY || errno == EFAULT || errno == ESRCH));
Run Code Online (Sandbox Code Playgroud)
loop, especially for the ESRCH
case (the others I only added due to the ptrace man page description).
You see, most ptrace commands are only allowed when the task is stopped. However, the task is not stopped when it is still completing e.g. a single-step command. Thus, using the above loop -- perhaps adding a millisecond nanosleep or similar to avoid wasting CPU -- makes sure the previous ptrace command has completed (and thus the task stopped) before we try to supply the new one.
Kerrek SB, I do believe at least some of the troubles you've had with your test programs are due to this issue? To me, personally, it was a kind of a D'oh! moment to realize that of course this is necessary, as ptracing is inherently asynchronous, not synchronous.
(This asynchronicity is also the cause for the SIGCONT
-PTRACE_CONT
interaction I mentioned above. I do believe with proper handling using the loop shown above, that interaction is no longer a problem -- and is actually quite understandable.)
Adding to the comments to this answer:
The Linux kernel uses a set of task state flags in the task_struct structure (see include/linux/sched.h
for definition) to keep track of the state of each task. The userspace-facing side of ptrace()
is defined in kernel/ptrace.c
.
When PTRACE_SINGLESTEP
or PTRACE_CONT
is called, kernel/ptrace.c
:ptrace_continue()
handles most of the details. It finishes by calling wake_up_state(child, __TASK_TRACED)
(kernel/sched/core.c::try_to_wake_up(child, __TASK_TRACED, 0)
).
When a process is stopped via SIGSTOP
signal, all tasks will be stopped, and end up in the "stopped, not traced" state.
附加到每一个任务(通过PTRACE_ATTACH或PTRACE_SEIZE,请参阅kernel/ptrace.c
:ptrace_attach()
)修改任务状态.但是,ptrace状态位(参见include/linux/ptrace.h:PT_
常量)与任务可运行状态位(参见include/linux/sched.h:TASK_
常量)是分开的.
在附加到任务并向进程发送SIGCONT
信号之后,停止状态不会立即被修改(我相信),因为任务也被跟踪.执行PTRACE_SINGLESTEP或PTRACE_CONT会kernel/sched/core.c::try_to_wake_up(child, __TASK_TRACED, 0)
更新任务状态,并将任务移动到运行队列.
Now, the complicated part that I haven't yet found the code path, is how the task state gets updated in the kernel when the task is next scheduled. My tests indicate that with single-stepping (which is yet another task state flag), only the task state gets updated, with the single-step flag cleared. It seems that PTRACE_CONT is not as reliable; I believe it is because the single-step flag "forces" that task state change. Perhaps there is a "race condition" wrt. the continue signal delivery and state change?
(Further edit: the kernel developers definitely expect wait()
to be called, see for example this thread.)
In other words, after noticing that the process has stopped (note that you can use /proc/PID/stat
or /proc/PID/status
if the process is not a child, and not yet attached to), I believe the following procedure is the most robust one:
pid_t pid, p; /* Process owning the tasks */
tid_t *tid; /* Task ID array */
size_t tids; /* Tasks */
long result;
int status;
size_t i;
for (i = 0; i < tids; i++) {
while (1) {
result = ptrace(PTRACE_ATTACH, tid[i], (void *)0, (void *)0);
if (result == -1L && (errno == ESRCH || errno == EBUSY || errno == EFAULT || errno == EIO)) {
/* To avoid burning up CPU for nothing: */
sched_yield(); /* or nanosleep(), or usleep() */
continue;
}
break;
}
if (result == -1L) {
/*
* Fatal error. First detach from tid[0..i-1], then exit.
*/
}
}
/* Send SIGCONT to the process. */
if (kill(pid, SIGCONT)) {
/*
* Fatal error, see errno. Exit.
*/
}
/* Since we are attached to the process,
* we can wait() on it. */
while (1) {
errno = 0;
status = 0;
p = waitpid(pid, &status, WCONTINUED);
if (p == (pid_t)-1) {
if (errno == EINTR)
continue;
else
break;
} else
if (p != pid) {
errno = ESRCH;
break;
} else
if (WIFCONTINUED(status)) {
errno = 0;
break;
}
}
if (errno) {
/*
* Fatal error. First detach from tid[0..tids-1], then exit.
*/
}
/* Single-step each task to update the task states. */
for (i = 0; i < tids; i++) {
while (1) {
result = ptrace(PTRACE_SINGLESTEP, tid[i], (void *)0, (void *)0);
if (result == -1L && errno == ESRCH) {
/* To avoid burning up CPU for nothing: */
sched_yield(); /* or nanosleep(), or usleep() */
continue;
}
break;
}
if (result == -1L) {
/*
* Fatal error. First detach from tid[0..i-1], then exit.
*/
}
}
/* Obtain task register structures, to make sure the single-steps
* have completed and their states have stabilized. */
for (i = 0; i < tids; i++) {
struct user_regs_struct regs;
while (1) {
result = ptrace(PTRACE_GETREGS, tid[i], ®s, ®s);
if (result == -1L && (errno == ESRCH || errno == EBUSY || errno == EFAULT || errno == EIO)) {
/* To avoid burning up CPU for nothing: */
sched_yield(); /* or nanosleep(), or usleep() */
continue;
}
break;
}
if (result == -1L) {
/*
* Fatal error. First detach from tid[0..i-1], then exit.
*/
}
}
Run Code Online (Sandbox Code Playgroud)
After the above, all tasks should be attached and in the expected state, so that e.g. PTRACE_CONT works without further tricks.
If the behaviour changes in future kernels -- I do believe the interaction between the STOP/CONT signals and ptracing is something that might change; at least a question to the LKML developers about this behaviour would be warranted! --, the above procedure will still work robustly. (Erring on the side of caution, by using a loop to PTRACE_SINGLESTEP a few times, might also be a good idea.)
The difference to PTRACE_CONT is that if the behaviour changes in the future, the initial PTRACE_CONT might actually continue the process, causing the ptrace()
that follow it to fail. With PTRACE_SINGLESTEP, the process will stop, allowing further ptrace()
calls to succeed.
Questions?
我可以附加到特定的帖子吗?
是的,至少在当前的内核上.
这是否意味着单步执行仅通过一个线程的指令?它是否会停止所有进程的线程?
是.它不会阻止其他线程,只会阻止其他线程.
有没有办法只在一个线程中前进,但保证其他线程保持停止状态?
是.发送SIGSTOP
到进程(用于waitpid(PID,,WUNTRACED)
等待进程停止),然后PTRACE_ATTACH
发送到进程中的每个线程.发送SIGCONT
(使用waitpid(PID,,WCONTINUED)
等待进程继续).
由于所有线程在连接时都已停止,并且连接会停止线程,因此所有线程在SIGCONT
传递信号后都会停止.您可以按您喜欢的任何顺序单步执行线程.
我觉得这很有趣,可以用来测试一个测试用例.(好吧,实际上我怀疑没有人会接受我的话,所以我决定最好显示你可以自己复制的证据.)
我的系统似乎遵循man 2 ptrace
的描述为Linux的人的页面项目,并Kerrisk似乎是在与内核行为保持它们同步就不错了.一般来说,我更喜欢kernel.org来源wrt.Linux内核到其他来源.
摘要:
附加到进程本身(TID == PID)仅停止原始线程,而不是所有线程.
附加到特定线程(使用TID /proc/PID/task/
)会停止该线程.(换句话说,TID == PID的线程并不特殊.)
发送SIGSTOP
到进程将停止所有线程,但ptrace()
仍然可以正常工作.
如果您向SIGSTOP
进程发送了一个,请ptrace(PTRACE_CONT, TID)
在分离之前不要调用.PTRACE_CONT
似乎干扰了SIGCONT
信号.
你可以先发一个SIGSTOP
,然后PTRACE_ATTACH
,再发送SIGCONT
,没有任何问题; 线程将保持停止(由于ptrace).换句话说,PTRACE_ATTACH
并PTRACE_DETACH
与拌匀SIGSTOP
和SIGCONT
,无任何毒副作用,我可以看到.
SIGSTOP
并且SIGCONT
影响整个过程,即使您尝试使用tgkill()
(或pthread_kill()
)将信号发送到特定线程.
要停止并继续特定的线程,PTHREAD_ATTACH
它; 停止并继续进程的所有线程,分别向进程发送SIGSTOP
和发送SIGCONT
信号.
就个人而言,我认为这证实了我在另一个问题中提出的方法.
这是您可以编译并运行以便自己测试的丑陋测试代码,traces.c
:
#define GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <sys/syscall.h>
#include <dirent.h>
#include <pthread.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#ifndef THREADS
#define THREADS 3
#endif
static int tgkill(int tgid, int tid, int sig)
{
int retval;
retval = syscall(SYS_tgkill, tgid, tid, sig);
if (retval < 0) {
errno = -retval;
return -1;
}
return 0;
}
volatile unsigned long counter[THREADS + 1] = { 0UL };
volatile sig_atomic_t run = 0;
volatile sig_atomic_t done = 0;
void handle_done(int signum)
{
done = signum;
}
int install_done(int signum)
{
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_handler = handle_done;
act.sa_flags = 0;
if (sigaction(signum, &act, NULL))
return errno;
return 0;
}
void *worker(void *data)
{
volatile unsigned long *const counter = data;
while (!run)
;
while (!done)
(*counter)++;
return (void *)(*counter);
}
pid_t *gettids(const pid_t pid, size_t *const countptr)
{
char dirbuf[128];
DIR *dir;
struct dirent *ent;
pid_t *data = NULL, *temp;
size_t size = 0;
size_t used = 0;
int tid;
char dummy;
if ((int)pid < 2) {
errno = EINVAL;
return NULL;
}
if (snprintf(dirbuf, sizeof dirbuf, "/proc/%d/task/", (int)pid) >= (int)sizeof dirbuf) {
errno = ENAMETOOLONG;
return NULL;
}
dir = opendir(dirbuf);
if (!dir)
return NULL;
while (1) {
errno = 0;
ent = readdir(dir);
if (!ent)
break;
if (sscanf(ent->d_name, "%d%c", &tid, &dummy) != 1)
continue;
if (tid < 2)
continue;
if (used >= size) {
size = (used | 127) + 129;
temp = realloc(data, size * sizeof data[0]);
if (!temp) {
free(data);
closedir(dir);
errno = ENOMEM;
return NULL;
}
data = temp;
}
data[used++] = (pid_t)tid;
}
if (errno) {
free(data);
closedir(dir);
errno = EIO;
return NULL;
}
if (closedir(dir)) {
free(data);
errno = EIO;
return NULL;
}
if (used < 1) {
free(data);
errno = ENOENT;
return NULL;
}
size = used + 1;
temp = realloc(data, size * sizeof data[0]);
if (!temp) {
free(data);
errno = ENOMEM;
return NULL;
}
data = temp;
data[used] = (pid_t)0;
if (countptr)
*countptr = used;
errno = 0;
return data;
}
int child_main(void)
{
pthread_t id[THREADS];
int i;
if (install_done(SIGUSR1)) {
fprintf(stderr, "Cannot set SIGUSR1 signal handler.\n");
return 1;
}
for (i = 0; i < THREADS; i++)
if (pthread_create(&id[i], NULL, worker, (void *)&counter[i])) {
fprintf(stderr, "Cannot create thread %d of %d: %s.\n", i + 1, THREADS, strerror(errno));
return 1;
}
run = 1;
kill(getppid(), SIGUSR1);
while (!done)
counter[THREADS]++;
for (i = 0; i < THREADS; i++)
pthread_join(id[i], NULL);
printf("Final counters:\n");
for (i = 0; i < THREADS; i++)
printf("\tThread %d: %lu\n", i + 1, counter[i]);
printf("\tMain thread: %lu\n", counter[THREADS]);
return 0;
}
int main(void)
{
pid_t *tid = NULL;
size_t tids = 0;
int i, k;
pid_t child, p;
if (install_done(SIGUSR1)) {
fprintf(stderr, "Cannot set SIGUSR1 signal handler.\n");
return 1;
}
child = fork();
if (!child)
return child_main();
if (child == (pid_t)-1) {
fprintf(stderr, "Cannot fork.\n");
return 1;
}
while (!done)
usleep(1000);
tid = gettids(child, &tids);
if (!tid) {
fprintf(stderr, "gettids(): %s.\n", strerror(errno));
kill(child, SIGUSR1);
return 1;
}
fprintf(stderr, "Child process %d has %d tasks.\n", (int)child, (int)tids);
fflush(stderr);
for (k = 0; k < (int)tids; k++) {
const pid_t t = tid[k];
if (ptrace(PTRACE_ATTACH, t, (void *)0L, (void *)0L)) {
fprintf(stderr, "Cannot attach to TID %d: %s.\n", (int)t, strerror(errno));
kill(child, SIGUSR1);
return 1;
}
fprintf(stderr, "Attached to TID %d.\n\n", (int)t);
fprintf(stderr, "Peeking the counters in the child process:\n");
for (i = 0; i <= THREADS; i++) {
long v;
do {
errno = 0;
v = ptrace(PTRACE_PEEKDATA, t, &counter[i], NULL);
} while (v == -1L && (errno == EIO || errno == EFAULT || errno == ESRCH));
fprintf(stderr, "\tcounter[%d] = %lu\n", i, (unsigned long)v);
}
fprintf(stderr, "Waiting a short moment ... ");
fflush(stderr);
usleep(250000);
fprintf(stderr, "and another peek:\n");
for (i = 0; i <= THREADS; i++) {
long v;
do {
errno = 0;
v = ptrace(PTRACE_PEEKDATA, t, &counter[i], NULL);
} while (v == -1L && (errno == EIO || errno == EFAULT || errno == ESRCH));
fprintf(stderr, "\tcounter[%d] = %lu\n", i, (unsigned long)v);
}
fprintf(stderr, "\n");
fflush(stderr);
usleep(250000);
ptrace(PTRACE_DETACH, t, (void *)0L, (void *)0L);
}
for (k = 0; k < 4; k++) {
const pid_t t = tid[tids / 2];
if (k == 0) {
fprintf(stderr, "Sending SIGSTOP to child process ... ");
fflush(stderr);
kill(child, SIGSTOP);
} else
if (k == 1) {
fprintf(stderr, "Sending SIGCONT to child process ... ");
fflush(stderr);
kill(child, SIGCONT);
} else
if (k == 2) {
fprintf(stderr, "Sending SIGSTOP to TID %d ... ", (int)tid[0]);
fflush(stderr);
tgkill(child, tid[0], SIGSTOP);
} else
if (k == 3) {
fprintf(stderr, "Sending SIGCONT to TID %d ... ", (int)tid[0]);
fflush(stderr);
tgkill(child, tid[0], SIGCONT);
}
usleep(250000);
fprintf(stderr, "done.\n");
fflush(stderr);
if (ptrace(PTRACE_ATTACH, t, (void *)0L, (void *)0L)) {
fprintf(stderr, "Cannot attach to TID %d: %s.\n", (int)t, strerror(errno));
kill(child, SIGUSR1);
return 1;
}
fprintf(stderr, "Attached to TID %d.\n\n", (int)t);
fprintf(stderr, "Peeking the counters in the child process:\n");
for (i = 0; i <= THREADS; i++) {
long v;
do {
errno = 0;
v = ptrace(PTRACE_PEEKDATA, t, &counter[i], NULL);
} while (v == -1L && (errno == EIO || errno == EFAULT || errno == ESRCH));
fprintf(stderr, "\tcounter[%d] = %lu\n", i, (unsigned long)v);
}
fprintf(stderr, "Waiting a short moment ... ");
fflush(stderr);
usleep(250000);
fprintf(stderr, "and another peek:\n");
for (i = 0; i <= THREADS; i++) {
long v;
do {
errno = 0;
v = ptrace(PTRACE_PEEKDATA, t, &counter[i], NULL);
} while (v == -1L && (errno == EIO || errno == EFAULT || errno == ESRCH));
fprintf(stderr, "\tcounter[%d] = %lu\n", i, (unsigned long)v);
}
fprintf(stderr, "\n");
fflush(stderr);
usleep(250000);
ptrace(PTRACE_DETACH, t, (void *)0L, (void *)0L);
}
kill(child, SIGUSR1);
do {
p = waitpid(child, NULL, 0);
if (p == -1 && errno != EINTR)
break;
} while (p != child);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
使用例如编译和运行
gcc -DTHREADS=3 -W -Wall -O3 traces.c -pthread -o traces
./traces
Run Code Online (Sandbox Code Playgroud)
输出是子进程计数器的转储(每个进程在一个单独的线程中递增,包括使用最终计数器的原始线程).比较短暂等待的计数器.例如:
Child process 18514 has 4 tasks.
Attached to TID 18514.
Peeking the counters in the child process:
counter[0] = 0
counter[1] = 0
counter[2] = 0
counter[3] = 0
Waiting a short moment ... and another peek:
counter[0] = 18771865
counter[1] = 6435067
counter[2] = 54247679
counter[3] = 0
Run Code Online (Sandbox Code Playgroud)
如上所示,只有使用最终计数器的初始线程(其TID == PID)才会停止.对于其他三个线程也是如此,它们按顺序使用前三个计数器:
Attached to TID 18515.
Peeking the counters in the child process:
counter[0] = 25385151
counter[1] = 13459822
counter[2] = 103763861
counter[3] = 560872
Waiting a short moment ... and another peek:
counter[0] = 25385151
counter[1] = 69116275
counter[2] = 120500164
counter[3] = 9027691
Attached to TID 18516.
Peeking the counters in the child process:
counter[0] = 25397582
counter[1] = 105905400
counter[2] = 155895025
counter[3] = 17306682
Waiting a short moment ... and another peek:
counter[0] = 32358651
counter[1] = 105905400
counter[2] = 199601078
counter[3] = 25023231
Attached to TID 18517.
Peeking the counters in the child process:
counter[0] = 40600813
counter[1] = 111675002
counter[2] = 235428637
counter[3] = 32298929
Waiting a short moment ... and another peek:
counter[0] = 48727731
counter[1] = 143870702
counter[2] = 235428637
counter[3] = 39966259
Run Code Online (Sandbox Code Playgroud)
接下来的两个案例检查了SIGCONT
/ SIGSTOP
wrt.整个过程:
Sending SIGSTOP to child process ... done.
Attached to TID 18516.
Peeking the counters in the child process:
counter[0] = 56887263
counter[1] = 170646440
counter[2] = 235452621
counter[3] = 48077803
Waiting a short moment ... and another peek:
counter[0] = 56887263
counter[1] = 170646440
counter[2] = 235452621
counter[3] = 48077803
Sending SIGCONT to child process ... done.
Attached to TID 18516.
Peeking the counters in the child process:
counter[0] = 64536344
counter[1] = 182359343
counter[2] = 253660731
counter[3] = 56422231
Waiting a short moment ... and another peek:
counter[0] = 72029244
counter[1] = 182359343
counter[2] = 288014365
counter[3] = 63797618
Run Code Online (Sandbox Code Playgroud)
正如您所看到的,发送SIGSTOP
将停止所有线程,但不会阻止ptrace()
.同样,之后SIGCONT
,线程继续正常运行.
最后两个案例检查使用tgkill()
发送SIGSTOP
/ SIGCONT
到特定线程(对应于第一个计数器的线程)的效果,同时附加到另一个线程:
Sending SIGSTOP to TID 18514 ... done.
Attached to TID 18516.
Peeking the counters in the child process:
counter[0] = 77012930
counter[1] = 183059526
counter[2] = 344043770
counter[3] = 71120227
Waiting a short moment ... and another peek:
counter[0] = 77012930
counter[1] = 183059526
counter[2] = 344043770
counter[3] = 71120227
Sending SIGCONT to TID 18514 ... done.
Attached to TID 18516.
Peeking the counters in the child process:
counter[0] = 88082419
counter[1] = 194059048
counter[2] = 359342314
counter[3] = 84887463
Waiting a short moment ... and another peek:
counter[0] = 100420161
counter[1] = 194059048
counter[2] = 392540525
counter[3] = 111770366
Run Code Online (Sandbox Code Playgroud)
不幸的是,正如您所见,正如预期的那样,处理(停止/运行)是整个流程,而不是特定于线程的.这意味着要停止特定线程并让其他线程正常运行,您需要单独PTHREAD_ATTACH
指向要停止的线程.
为证明上述所有陈述,您可能需要添加测试用例; 我最终得到了相当多的代码副本,都进行了轻微编辑,以测试所有代码,我不确定我选择了最完整的代码.如果您发现遗漏,我很乐意扩展测试计划.
有问题吗?