diff options
author | Petr Machata <pmachata@redhat.com> | 2011-09-27 02:27:58 +0200 |
---|---|---|
committer | Petr Machata <pmachata@redhat.com> | 2011-10-06 14:30:37 +0200 |
commit | cbe29c6c0ad01839a81272c4715ea73d17e89611 (patch) | |
tree | a4e6b7c254b142ff11aa000468e2a2398e5c7c76 | |
parent | 02bd9eca0b439b6950ab72ba64b67181e5bf0fbd (diff) | |
download | ltrace-cbe29c6c0ad01839a81272c4715ea73d17e89611.tar.gz |
Basic support for tracing vfork
- allow even individual tasks to have their event handlers. These are
called in precedence to the thread group event handlers.
- distinguish CLONE and VFORK events
- add methods for dynamic changes of leader
- add new process status ps_sleeping
-rw-r--r-- | common.h | 3 | ||||
-rw-r--r-- | handle_event.c | 44 | ||||
-rw-r--r-- | ltrace.h | 1 | ||||
-rw-r--r-- | proc.c | 66 | ||||
-rw-r--r-- | sysdeps/linux-gnu/events.c | 21 | ||||
-rw-r--r-- | sysdeps/linux-gnu/proc.c | 5 | ||||
-rw-r--r-- | sysdeps/linux-gnu/trace.c | 175 | ||||
-rw-r--r-- | testsuite/ltrace.minor/trace-clone.c | 1 |
8 files changed, 249 insertions, 67 deletions
@@ -255,6 +255,7 @@ enum process_status { ps_invalid, /* Failure. */ ps_stop, /* Job-control stop. */ ps_tracing_stop, + ps_sleeping, ps_zombie, ps_other, /* Necessary other states can be added as needed. */ }; @@ -268,6 +269,7 @@ enum pcb_status { extern Process * pid2proc(pid_t pid); extern void add_process(Process * proc); extern void remove_process(Process * proc); +extern void change_process_leader(Process * proc, Process * leader); extern Process *each_process(Process * start, enum pcb_status (* cb)(Process * proc, void * data), void * data); @@ -338,6 +340,7 @@ extern int syscall_p(Process * proc, int status, int * sysnum); extern void continue_process(pid_t pid); extern void continue_after_signal(pid_t pid, int signum); extern void continue_after_breakpoint(Process * proc, Breakpoint * sbp); +extern void continue_after_vfork(Process * proc); extern void ltrace_exiting(void); extern long gimme_arg(enum tof type, Process * proc, int arg_num, arg_type_info * info); extern void save_register_args(enum tof type, Process * proc); diff --git a/handle_event.c b/handle_event.c index 0aa40f7..86788e0 100644 --- a/handle_event.c +++ b/handle_event.c @@ -35,6 +35,18 @@ static char * shortsignal(Process *proc, int signum); static char * sysname(Process *proc, int sysnum); static char * arch_sysname(Process *proc, int sysnum); +static Event * +call_handler(Process * proc, Event * event) +{ + assert(proc != NULL); + + Event_Handler * handler = proc->event_handler; + if (handler == NULL) + return event; + + return (*handler->on_event) (handler, event); +} + void handle_event(Event *event) { if (exiting == 1) { @@ -44,15 +56,22 @@ handle_event(Event *event) { } debug(DEBUG_FUNCTION, "handle_event(pid=%d, type=%d)", event->proc ? event->proc->pid : -1, event->type); - /* If the thread group defines an overriding event handler, - give it a chance to kick in. */ - if (event->proc != NULL - && event->proc->leader != NULL) { - Event_Handler * handler = event->proc->leader->event_handler; - if (handler != NULL) { - event = (*handler->on_event) (handler, event); + + /* If the thread group or an individual task define an + overriding event handler, give them a chance to kick in. + We will end up calling both handlers, if the first one + doesn't sink the event. */ + if (event->proc != NULL) { + event = call_handler(event->proc, event); + if (event == NULL) + /* It was handled. */ + return; + + /* Note: the previous handler has a chance to alter + * the event. */ + if (event->proc->leader != NULL) { + event = call_handler(event->proc->leader, event); if (event == NULL) - /* It was handled. */ return; } } @@ -102,6 +121,7 @@ handle_event(Event *event) { handle_arch_sysret(event); return; case EVENT_CLONE: + case EVENT_VFORK: debug(1, "event: clone (%u)", event->e_un.newpid); handle_clone(event); return; @@ -239,7 +259,11 @@ handle_clone(Event * event) { p->state = STATE_BEING_CREATED; add_process(p); } - continue_process(event->proc->pid); + + if (event->type == EVENT_VFORK) + continue_after_vfork(p); + else + continue_process(event->proc->pid); } static void @@ -253,8 +277,6 @@ handle_new(Event * event) { pending_new_insert(event->e_un.newpid); } else { assert(proc->state == STATE_BEING_CREATED); - if (proc->event_handler != NULL) - destroy_event_handler(proc); if (options.follow) { proc->state = STATE_ATTACHED; } else { @@ -9,6 +9,7 @@ enum Event_type { EVENT_ARCH_SYSCALL, EVENT_ARCH_SYSRET, EVENT_CLONE, + EVENT_VFORK, EVENT_EXEC, EVENT_BREAKPOINT, EVENT_LIBCALL, @@ -154,9 +154,30 @@ pid2proc(pid_t pid) { return each_process(NULL, &find_proc, (void *)(uintptr_t)pid); } - static Process * list_of_processes = NULL; +static void +unlist_process(Process * proc) +{ + Process *tmp; + + if (list_of_processes == proc) { + list_of_processes = list_of_processes->next; + return; + } + + for (tmp = list_of_processes; ; tmp = tmp->next) { + /* If the following assert fails, the process wasn't + * in the list. */ + assert(tmp->next != NULL); + + if (tmp->next == proc) { + tmp->next = tmp->next->next; + return; + } + } +} + Process * each_process(Process * proc, enum pcb_status (* cb)(Process * proc, void * data), @@ -213,6 +234,23 @@ add_process(Process * proc) *leaderp = proc; } +void +change_process_leader(Process * proc, Process * leader) +{ + Process ** leaderp = &list_of_processes; + if (proc->leader == leader) + return; + + assert(leader != NULL); + unlist_process(proc); + if (proc != leader) + leaderp = &leader->next; + + proc->leader = leader; + proc->next = *leaderp; + *leaderp = proc; +} + static enum pcb_status clear_leader(Process * proc, void * data) { @@ -242,31 +280,14 @@ delete_events_for(Process * proc) void remove_process(Process *proc) { - Process *tmp, *tmp2; - debug(DEBUG_FUNCTION, "remove_proc(pid=%d)", proc->pid); if (proc->leader == proc) each_task(proc, &clear_leader, NULL); - if (list_of_processes == proc) { - tmp = list_of_processes; - list_of_processes = list_of_processes->next; - delete_events_for(tmp); - free(tmp); - return; - } - tmp = list_of_processes; - while (tmp->next) { - if (tmp->next == proc) { - tmp2 = tmp->next; - tmp->next = tmp->next->next; - delete_events_for(tmp2); - free(tmp2); - return; - } - tmp = tmp->next; - } + unlist_process(proc); + delete_events_for(proc); + free(proc); } void @@ -283,7 +304,8 @@ destroy_event_handler(Process * proc) Event_Handler * handler = proc->event_handler; debug(DEBUG_FUNCTION, "destroy_event_handler(pid=%d, %p)", proc->pid, handler); assert(handler != NULL); - handler->destroy(handler); + if (handler->destroy != NULL) + handler->destroy(handler); free(handler); proc->event_handler = NULL; } diff --git a/sysdeps/linux-gnu/events.c b/sysdeps/linux-gnu/events.c index 8a79583..0685342 100644 --- a/sysdeps/linux-gnu/events.c +++ b/sysdeps/linux-gnu/events.c @@ -240,13 +240,20 @@ next_event(void) if (errno != 0) perror("syscall_p"); } - if (WIFSTOPPED(status) && ((status>>16 == PTRACE_EVENT_FORK) || (status>>16 == PTRACE_EVENT_VFORK) || (status>>16 == PTRACE_EVENT_CLONE))) { - unsigned long data; - ptrace(PTRACE_GETEVENTMSG, pid, NULL, &data); - event.type = EVENT_CLONE; - event.e_un.newpid = data; - debug(DEBUG_EVENT, "event: CLONE: pid=%d, newpid=%d", pid, (int)data); - return &event; + if (WIFSTOPPED(status)) { + int what = status >> 16; + if (what == PTRACE_EVENT_VFORK + || what == PTRACE_EVENT_FORK + || what == PTRACE_EVENT_CLONE) { + unsigned long data; + event.type = what == PTRACE_EVENT_VFORK + ? EVENT_VFORK : EVENT_CLONE; + ptrace(PTRACE_GETEVENTMSG, pid, NULL, &data); + event.e_un.newpid = data; + debug(DEBUG_EVENT, "event: CLONE: pid=%d, newpid=%d", + pid, (int)data); + return &event; + } } if (WIFSTOPPED(status) && (status>>16 == PTRACE_EVENT_EXEC)) { event.type = EVENT_EXEC; diff --git a/sysdeps/linux-gnu/proc.c b/sysdeps/linux-gnu/proc.c index e3b71e5..a99593c 100644 --- a/sysdeps/linux-gnu/proc.c +++ b/sysdeps/linux-gnu/proc.c @@ -148,7 +148,7 @@ process_status_cb(const char * line, const char * prefix, void * data) switch (c) { case 'Z': RETURN(ps_zombie); case 't': RETURN(ps_tracing_stop); - case 'T': { + case 'T': /* This can be either "T (stopped)" or, for older * kernels, "T (tracing stop)". */ if (!strcmp(status, "T (stopped)\n")) @@ -161,7 +161,8 @@ process_status_cb(const char * line, const char * prefix, void * data) RETURN(ps_stop); /* Some sort of stop * anyway. */ } - } + case 'D': + case 'S': RETURN(ps_sleeping); } RETURN(ps_other); diff --git a/sysdeps/linux-gnu/trace.c b/sysdeps/linux-gnu/trace.c index f8a1779..fef81df 100644 --- a/sysdeps/linux-gnu/trace.c +++ b/sysdeps/linux-gnu/trace.c @@ -164,9 +164,10 @@ continue_process(pid_t pid) struct pid_task { pid_t pid; /* This may be 0 for tasks that exited * mid-handling. */ - int sigstopped; - int got_event; - int delivered; + int sigstopped : 1; + int got_event : 1; + int delivered : 1; + int vforked : 1; } * pids; struct pid_set { @@ -213,23 +214,6 @@ struct process_stopping_handler struct pid_set pids; }; -static enum pcb_status -task_stopped(Process * task, void * data) -{ - /* If the task is already stopped, don't worry about it. - * Likewise if it managed to become a zombie or terminate in - * the meantime. This can happen when the whole thread group - * is terminating. */ - switch (process_status(task->pid)) { - case ps_invalid: - case ps_tracing_stop: - case ps_zombie: - return pcb_cont; - default: - return pcb_stop; - } -} - static struct pid_task * get_task_info(struct pid_set * pids, pid_t pid) { @@ -261,6 +245,57 @@ add_task_info(struct pid_set * pids, pid_t pid) } static enum pcb_status +task_stopped(Process * task, void * data) +{ + enum process_status st = process_status(task->pid); + if (data != NULL) + *(enum process_status *)data = st; + + /* If the task is already stopped, don't worry about it. + * Likewise if it managed to become a zombie or terminate in + * the meantime. This can happen when the whole thread group + * is terminating. */ + switch (st) { + case ps_invalid: + case ps_tracing_stop: + case ps_zombie: + return pcb_cont; + default: + return pcb_stop; + } +} + +/* Task is blocked if it's stopped, or if it's a vfork parent. */ +static enum pcb_status +task_blocked(Process * task, void * data) +{ + struct pid_set * pids = data; + struct pid_task * task_info = get_task_info(pids, task->pid); + if (task_info != NULL + && task_info->vforked) + return pcb_cont; + + return task_stopped(task, NULL); +} + +static Event * process_vfork_on_event(Event_Handler * super, Event * event); + +static enum pcb_status +task_vforked(Process * task, void * data) +{ + if (task->event_handler != NULL + && task->event_handler->on_event == &process_vfork_on_event) + return pcb_stop; + return pcb_cont; +} + +static int +is_vfork_parent(Process * task) +{ + return each_task(task->leader, &task_vforked, NULL) != NULL; +} + +static enum pcb_status send_sigstop(Process * task, void * data) { Process * leader = task->leader; @@ -283,9 +318,11 @@ send_sigstop(Process * task, void * data) return pcb_cont; /* Don't bother sending SIGSTOP if we are already stopped, or - * if we sent the SIGSTOP already, which happens when we - * inherit the handler from breakpoint re-enablement. */ - if (task_stopped(task, NULL) == pcb_cont) + * if we sent the SIGSTOP already, which happens when we are + * handling "onexit" and inherited the handler from breakpoint + * re-enablement. */ + enum process_status st; + if (task_stopped(task, &st) == pcb_cont) return pcb_cont; if (task_info->sigstopped) { if (!task_info->delivered) @@ -293,6 +330,16 @@ send_sigstop(Process * task, void * data) task_info->delivered = 0; } + /* Also don't attempt to stop the process if it's a parent of + * vforked process. We set up event handler specially to hint + * us. In that case parent is in D state, which we use to + * weed out unnecessary looping. */ + if (st == ps_sleeping + && is_vfork_parent (task)) { + task_info->vforked = 1; + return pcb_cont; + } + if (task_kill(task->pid, SIGSTOP) >= 0) { debug(DEBUG_PROCESS, "send SIGSTOP to %d", task->pid); task_info->sigstopped = 1; @@ -536,7 +583,7 @@ process_stopping_on_event(Event_Handler * super, Event * event) switch (state) { case psh_stopping: /* If everyone is stopped, singlestep. */ - if (each_task(leader, &task_stopped, NULL) == NULL) { + if (each_task(leader, &task_blocked, &self->pids) == NULL) { debug(DEBUG_PROCESS, "all stopped, now SINGLESTEP %d", teb->pid); if (sbp->enabled) @@ -742,6 +789,86 @@ ltrace_exiting_install_handler(Process * proc) return 0; } +/* + * When the traced process vforks, it's suspended until the child + * process calls _exit or exec*. In the meantime, the two share the + * address space. + * + * The child process should only ever call _exit or exec*, but we + * can't count on that (it's not the role of ltrace to policy, but to + * observe). In any case, we will _at least_ have to deal with + * removal of vfork return breakpoint (which we have to smuggle back + * in, so that the parent can see it, too), and introduction of exec* + * return breakpoint. Since we already have both breakpoint actions + * to deal with, we might as well support it all. + * + * The gist is that we pretend that the child is in a thread group + * with its parent, and handle it as a multi-threaded case, with the + * exception that we know that the parent is blocked, and don't + * attempt to stop it. When the child execs, we undo the setup. + * + * XXX The parent process could be un-suspended before ltrace gets + * child exec/exit event. Make sure this is taken care of. + */ + +static Event * +process_vfork_on_event(Event_Handler * super, Event * event) +{ + struct process_vfork_handler * self = (void *)super; + assert(self != NULL); + + switch (event->type) { + case EVENT_EXIT: + case EVENT_EXIT_SIGNAL: + case EVENT_EXEC: + /* Now is the time to remove the leader that we + * artificially set up earlier. XXX and do all the + * other fun stuff. */ + change_process_leader(event->proc, event->proc); + destroy_event_handler(event->proc); + + /* XXXXX this could happen in the middle of handling + * multi-threaded breakpoint. We must be careful to + * undo the effects that we introduced above (vforked + * = 1 et.al.). */ + + default: + ; + } + + return event; +} + +void +continue_after_vfork(Process * proc) +{ + debug(DEBUG_PROCESS, "continue_after_vfork: pid=%d", proc->pid); + Event_Handler * handler = calloc(sizeof(*handler), 1); + if (handler == NULL) { + perror("malloc vfork handler"); + /* Carry on not bothering to treat the process as + * necessary. */ + continue_process(proc->parent->pid); + return; + } + + /* We must set up custom event handler, so that we see + * exec/exit events for the task itself. */ + handler->on_event = process_vfork_on_event; + install_event_handler(proc, handler); + + /* Make sure that the child is sole thread. */ + assert(proc->leader == proc); + assert(proc->next == NULL || proc->next->leader != proc); + + /* Make sure that the child's parent is properly set up. */ + assert(proc->parent != NULL); + assert(proc->parent->leader != NULL); + + change_process_leader(proc, proc->parent->leader); + continue_process(proc->parent->pid); +} + /* If ltrace gets SIGINT, the processes directly or indirectly run by * ltrace get it too. We just have to wait long enough for the signal * to be delivered and the process terminated, which we notice and diff --git a/testsuite/ltrace.minor/trace-clone.c b/testsuite/ltrace.minor/trace-clone.c index 6e1c809..db1936d 100644 --- a/testsuite/ltrace.minor/trace-clone.c +++ b/testsuite/ltrace.minor/trace-clone.c @@ -3,7 +3,6 @@ clone called. This file was written by Yao Qi <qiyao@cn.ibm.com>. */ - #define _GNU_SOURCE #include <stdio.h> #include <sys/types.h> |