Linux fork/execve子进程执行异常 基于ptrace统计clone调用次数
代码逻辑错误排查
原有实现存在6个核心逻辑错误,是导致execve调用后程序异常、计数不准的根本原因:
- 调用顺序错误:fork完成后立刻在父子进程公共逻辑里调用
wait,既没有先判断fork返回值(fork失败时wait会传入无效pid触发异常),也没有给子进程留出执行ptrace(PTRACE_TRACEME)、发送SIGSTOP的时机,wait等待目标完全错误。 - 父子进程逻辑混写:父进程的ptrace配置、事件计数、进程恢复逻辑全部写在
child_pid == 0的子进程分支中,子进程调用execve成功后会直接替换整个进程地址空间,这些代码永远不会被执行,同时父进程拿不到任何trace逻辑,自然无法跟踪后续事件。 - 对execve的行为理解错误:execve调用成功后不会返回,原有代码把ptrace配置、事件判断逻辑全写在execve调用之后,子进程跑到execve就直接加载bash程序,后续代码全部被覆写丢弃。
- ptrace调用主体错误:
PTRACE_TRACEME确实由子进程调用,但PTRACE_SETOPTIONS、PTRACE_CONT必须由作为tracer的父进程调用,子进程自行调用这些接口完全不符合ptrace的设计,且PTRACE_TRACEME的第一个参数应传0,不需要传入自身pid。 - wait循环判断逻辑完全失效:原有判断条件
while(status != child_pid)把存储进程状态的status变量和pid做比较,二者语义、数据范围完全不匹配,循环根本不会按预期执行;且原有逻辑只等待初始fork的子进程,bash执行管道命令时clone出的echo、wc等后代进程的事件完全不会被捕获。 - 事件判断位置错误:ptrace的clone事件是发送给tracer(父进程)的,原有逻辑把事件判断写在子进程分支,子进程永远收不到自身的clone事件,计数逻辑永远不会触发。
正确实现方案
严格限定仅使用ptrace、fork、wait/waitpid三类接口的前提下,实现逻辑如下:
- 彻底拆分父子进程逻辑,子进程仅负责标记自身被trace、主动停住等待父进程配置、调用execve加载目标程序,execve之后不写任何业务逻辑。
- 父进程首先等待初始子进程因SIGSTOP暂停,待子进程停稳后配置ptrace选项,除
PTRACE_O_TRACECLONE外,同时开启PTRACE_O_TRACEFORK、PTRACE_O_TRACEVFORK,确保子进程通过任意clone flag创建的后代进程都会被自动纳入trace,无需手动attach。 - 父进程进入事件循环,调用
waitpid(-1, &status, 0)等待所有被trace进程(含初始子进程和所有后代)的状态变化,不限制等待的pid范围。 - 每次wait返回后先判断事件类型:如果是进程退出/被信号杀死,就减少存活被trace进程计数,等所有被trace进程退出后跳出循环;如果是ptrace触发的SIGTRAP停住事件,判断事件类型为clone/fork/vfork时计数器加1,同时增加存活被trace进程计数。
- 所有停住的进程处理完事件后,统一调用
ptrace(PTRACE_CONT)恢复运行。
修正后可运行代码
#include <sys/ptrace.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> int main(int argc, char *argv[]) { if (argc < 2) { fprintf(stderr, "usage: %s <cmd> [args...]\n", argv[0]); exit(1); } pid_t child_pid = fork(); if (child_pid == -1) { perror("fork failed"); exit(1); } if (child_pid == 0) { // 子进程:被trace端 ptrace(PTRACE_TRACEME, 0, 0, 0); raise(SIGSTOP); execve(argv[1], &argv[1], NULL); perror("execve failed"); // execve成功则永远走不到这里 exit(1); } else { // 父进程:tracer端 int status; int clone_counter = 0; int alive_tracee = 1; // 初始仅1个被trace的子进程 // 等待初始子进程触发SIGSTOP waitpid(child_pid, &status, 0); // 配置ptrace选项,跟踪所有新创建的后代进程 ptrace(PTRACE_SETOPTIONS, child_pid, 0, PTRACE_O_TRACECLONE | PTRACE_O_TRACEFORK | PTRACE_O_TRACEVFORK); // 恢复初始子进程运行 ptrace(PTRACE_CONT, child_pid, 0, 0); while (alive_tracee > 0) { pid_t cur_pid = waitpid(-1, &status, 0); if (cur_pid == -1) break; // 处理进程退出事件 if (WIFEXITED(status) || WIFSIGNALED(status)) { alive_tracee--; continue; } // 处理ptrace事件 if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) { unsigned long event_code = (status >> 16) & 0xffff; if (event_code == PTRACE_EVENT_CLONE || event_code == PTRACE_EVENT_FORK || event_code == PTRACE_EVENT_VFORK) { clone_counter++; alive_tracee++; } } // 恢复当前停住的进程运行 ptrace(PTRACE_CONT, cur_pid, 0, 0); } printf("# of clone executions: %d\n", clone_counter); } return 0; }
测试说明
传入测试参数/bin/bash -c "echo 'first test' | wc -c"运行时,程序不会出现execve后整体终止的问题——父进程逻辑运行在独立地址空间,不会被子进程的execve覆写。计数结果包含bash启动流程、创建echo进程、创建wc进程的所有clone类系统调用,符合统计要求。
内容的提问来源于stack exchange,提问作者Olivia22
相关产品推荐
相关产品推荐

