深入了解ptrace


主要介绍基于LINUX 64的ptrace

fork()

在Linux中,每一个进程都有一个唯一的编号,被称作pid(Process ID)。在Linux中,进程不能凭空产生(init进程是个例外),只能从一个已有进程衍生出来。原来的进程被称做父进程,衍生出来的进程叫子进程。一个系统中所有进程以父子关系相连接,形成一棵树,这棵“树”的树根就是init进程,它是在系统启动时被直接启动的,因此它没有父进程。并且系统中所有其他进程都直接或间接地是它的子进程。

在Linux系统中,实现“把一个进程变成两个”这一功能的有三个系统调用,即fork()vfork()clone(),这里主要介绍__fork()__

fork()将当前进程所有数据复制一份,产生一个和父进程一模一样的子进程。并在两个进程中返回不同的返回值.

int pid=fork();
if (pid==0){
  	//子进程的工作
}else{
  //父进程的工作
}

一般来说,子进程的工作就是调用exec族函数,启动另一个程序(把自己替换掉)。如果子进程还在执行而父进程已结束,那么它就成为“孤儿”进程,成为init进程的子进程。

fork() + exec()

exec

int execl  (const char *path, const char *arg, ...);
int execlp (const char *file, const char *arg, ...);
int execle (const char *path, const char *arg, ..., char * const envp[]);
int execv  (const char *path, char *const argv[]);
int execve (const char *path, char *const argv[], char *const envp[]);
int execvp (const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

他们的作用就是把某个进程(通常是fork出来的子进程)从里到外,完完整整,包括代码、堆栈,全部换成另一个程序,然后从头开始运行。

它们的调用效果是一样的,区别在于调用方式。

  • l(代表list)的函数使用了一种比较接近人类方法来表示程序参数表,即以(char *)NULL作为结尾的变参列表
  • e(environment),则该函数接受一个字符串数组表示的环境变量表;反之,则会默认传递所有当前环境变量
  • p,那么你就不必在第一个参数中列出完整路径,系统会自动检查当前目录和PATH环境变量

不管你使用那种方法表示程序参数表,第0个参都应当和可执行文件路径保持一致,虽然不一致依然可以正确运行,但有可能出现奇奇怪怪的问题。

组合介绍

一般来说,exec结合fork这么使用:

pid_t pid = fork();
if (pid == -1) {
  	// error, no child created
}
else if (pid == 0) {
  // child
}
else {
  // parent
  int status;
  if (waitpid(pid, &status, 0) == -1) {
    // handle error
  }
  else {
    // child exit code in status
    // use WIFEXITED, WEXITSTATUS, etc. on status
  }
}

下面是一个执行ls的例子:

+--------+
| pid=7  |
| ppid=4 |
| bash   |
+--------+
	|
    | calls fork
	V
+--------+             +--------+
| pid=7  |    forks    | pid=22 |
| ppid=4 | ----------> | ppid=7 |
| bash   |             | bash   |
+--------+             +--------+
	|                      |
    | waits for pid 22     | calls exec to run ls
	|                      V
    |                  +--------+
	|                  | pid=22 |
    |                  | ppid=7 |
	|                  | ls     |
    V                  +--------+
+--------+                 |
| pid=7  |                 | exits
| ppid=4 | <---------------+
| bash   |
+--------+
	|
    | continues
	V

获取子进程的exitcode

int status;
pid_t child = fork();
if (child == -1) return 1; //Failed
if (child > 0) { /* I am the parent - wait for the child to finish */
  	pid_t pid = waitpid(child, &status, 0);
  if (pid != -1 && WIFEXITED(status)) {
	 int low8bits = WEXITSTATUS(status);
     printf("Process %d returned %d", pid,low8bits);
  }
} 
else { /* I am the child */
 // do something interesting
  execl("/bin/ls", "/bin/ls", ".", (char *) NULL); //"ls ."
}

返回值

The exec() functions return only if an error has occurred.  The return value is -1, and errno is set to indicate the error.

#Process and Signal 在使用wait4后,程序的信息被存储在staus变量中,这些信息被存储在这个整数的不同二进制位上,这儿有一系列宏用于帮我们提取这些信息。

  • WIFEXITED 如果进程正常退出,返回一个非0值(通常是进程调用了exit()或是_exit())
  • WIFSIGNALED 如果进程由于一个未被捕获的信号而被终止,返回一个非0值
  • WIFSTOPPED 当进程被停止(非终止)时,返回一个非0值(通常发生在当进程处于traced状态时)
  • WEXITSTATUS 当WIFEXITED为非0值,获得进程main()函数的返回值
  • WTERMSIG 如果WIFSIGNALED为非0值,获得引起进程终止的信号代码
  • WSTOPSIG 如果WIFSTOPPED为非0值,获得引起进程停止的信号代码

当进程自行终止时,WIFEXITED即为true,配套使用WEXITSTATUS获得返回值

当子进程进行系统调用时,WIFSTOPPED为true,同时WSTOPSIG等于SIGTRAP(信号代码为7),我们可以用这种方法区分syscall-stop和signal-delivery-stop。当有一个外部信号要发送给子进程,这个信号会先到达父进程,使WIFSTOPPED为true,同时WSTOPSIG等于该信号的信号代码。父进程可以选择将这个信号继续传递或是不传递,甚至传递另一个信号给子进程。一旦信号真正到达子进程,就进入子进程自己的处理流程或是系统默认动作,可能触发WIFSIGNALED,比如SIGINT。 在所有信号中,SIGKILL是一个例外,它不会经过父进程引发WIFSTOPPED,而是直接传递到子进程,引发WIFSIGNALED。

如果父进程需要将信号传递给子进程,这是由ptrace(PTRACE_SYSCALL,pid,0,0)的第四个参数决定的。如果为0,就不传递信号,否则传递对应代码的信号,比如ptrace(PTRACE_SYSCALL,pid,0,9)就将信号9(SIGKILL)传递给了子进程。

系统调用

对于64位系统,系统调用号存放在RAX寄存器,参数依次放入RDI、RSI、RDX、R10 … 返回值位于RAX寄存器

参考文章

https://en.wikipedia.org/wiki/Fork-exec

http://man7.org/linux/man-pages/man3/exec.3.html

https://github.com/angrave/SystemProgramming/wiki/Forking,-Part-2:-Fork,-Exec,-Wait-Kill