深入了解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