strace工具是一个用户态的应用程序,用来追踪进程的系统调用。它的基础就是ptrace系统调用。。
最简单的strace命令的用法就是:strace PROG,PROG是要执行的程序,例如
strace ./main
strace命令执行的结果就是按照调用顺序打印出所有的系统调用,包括函数名、参数列表以及返回值。
execve("./Hello", ["./Hello"], [/* 67 vars */]) = 0
brk(0) = 0x804a000
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7f18000
所以在这里我们仅仅简单的实现下strace的返回函数名,参数列表和返回值。
函数原型:
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
ptraced的四个参数:
enum __ptrace_request request:表示ptrace执行的命令。
pid_t pid: 指示ptrace要跟踪的进程。
void *addr: 指示要监控的内存地址。
void *data: 存放读取出的或者要写入的数据。
基本原理
ptrace可以将所有发送给被跟踪子进程的信号转发给父进程,然后阻塞子进程。然后系统将子进程的状态标记为TASA_TRACED。父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。
几个用得到的requst参数
PTRACE_TRACEME :本进程被其父进程所跟踪。
PTRACE_PEEKUSR:从USER区域中读取一个字节,偏移量为addr
PTRACE_SYSCALL:进程重新运行。
pid_t pid;
pid = fork();
long int syscallID;
int flag =0, newline = 0;
unsigned int value;
Macroc();
if (pid < 0)
{
printf ("fork fail!\n");
return -1;
}
else if (pid == 0)
{
ptrace(PTRACE_TRACEME, 0,NULL , NULL);
execl("./hello","hello", NULL);
}
else
{
int val;
wait (&val);
if (WIFEXITED(val))
return 0;
syscallID = ptrace (PTRACE_PEEKUSER, pid, ORIG_RAX*8, NULL);
printf ("Process executed system call ID:%ld\n", syscallID);
printf("\n函数名为 %s\n", name[syscallID]);
ptrace (PTRACE_SYSCALL, pid, NULL, NULL);
while(1)
{
wait(&val);
if (WIFEXITED (val))
return 0;
if (flag == 0)
{
if(newline) newline = 2;
syscallID = ptrace (PTRACE_PEEKUSER, pid, ORIG_RAX*8, NULL);
// printf("\nthe name is %s\n", name[syscallID]);
// int syscallNAME;
// ptrace(PTRACE_GETREGS, pid, 0x1C*4, syscallNAME);
// printf("%d\n", syscallNAME);
unsigned int syscallspara[7];
syscallspara[0] = ptrace (PTRACE_PEEKUSER, pid, 0x00*4, NULL);
syscallspara[1] = ptrace (PTRACE_PEEKUSER, pid, 0x04*4, NULL);
syscallspara[2] = ptrace (PTRACE_PEEKUSER, pid, 0x08*4, NULL);
syscallspara[3] = ptrace (PTRACE_PEEKUSER, pid, 0x0C*4, NULL);
syscallspara[4] = ptrace (PTRACE_PEEKUSER, pid, 0x10*4, NULL);
syscallspara[5] = ptrace (PTRACE_PEEKUSER, pid, 0x14*4, NULL);
printf("参数列表为#x %#x %#x %#x %#x %#x\n", syscallspara[0],syscallspara[1],syscallspara[2],syscallspara[3],syscallspara[4],syscallspara[5]);
if(syscallID == 1) newline = 1;
printf ("Process executed system call ID:%ld",syscallID);
printf("\n函数名为%s\n", name[syscallID]);
if(newline == 2) printf("\n");
flag = 1;
}
else
{
value = ptrace (PTRACE_PEEKUSER, pid, RAX*4,NULL);
printf ("返回值为t%#x\n", value);
flag = 0;
}
ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
}
}
syscallID
负责返回函数ID
value
负责返回函数值
name
负责存储返回函数名
fork出的子进程先调用了ptrace(PTRACE_TRACEME)表示子进程让父进程跟踪自己。然后子进程调用execl加载执行了HelloWorld。而在父进程中则使用wait系统调用等待子进程的状态改变。子进程因为设置了PTRACE_TRACEME而在执行系统调用被系统停止(设置为TASK_TRACED),这时父进程被唤醒,使用ptrace(PTRACE_PEEKUSER)分别去读取子进程执行的系统调用ID(放在ORIG_EAX中)以及系统调用返回时的值(放在EAX中)。然后使用ptrace(PTRACE_SYSCALL)指示子进程运行到下一次执行系统调用的时候(进入或者退出),直到子进程退出为止。
由于返回得到的只是函数的ID,这里需要获取函数的值,由于具体strace内部如何把寄存器里得到的值转换成函数名这里不清楚,这里用了取巧的方法。
获取系统调用表有很多方法,这里简答介绍一种:
cd /usr/src/linux-4.16.10/arch/x86/entry/syscalls
vim syscalls_64.tbl
在目录里找到系统调用名和对应的系统调用ID,将内容另存为txt,然后在文本的最后一行添加!
作为结束标志,方便处理。
难度不大,思路也很简单,这里就不多解释了。
下面是代码:
#ifndef TEST1_H
#define TEST1_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MaxLen 100000
extern char *src;
extern int token;
extern int count;
extern char name_croc[1500][100];
extern char name[500][100];
extern int is_over;
unsigned long readFile(char *filePath, char str[]);
void Scaner();
void GetMacroc();
void Macroc();
#endif
#include "macroc.h"
char *src;
int token;
int count = 0;
char name_croc[1500][100];
char name[500][100];
int is_over = 0;
unsigned long readFile(char *filePath, char str[]) {
FILE *fp = fopen(filePath, "r");//打开文件
if (fp == NULL) {
printf("打开文件出错,请确认文件存在当前目录下!\n");
exit(0);
}
unsigned long i;
i = fread(str, 1, MaxLen, fp);
if (i >= MaxLen) {
printf("文件过大!请重新输入一个更小的文件。\n");
exit(1);
}
str[i] = '\0';
fclose(fp);
return i;
}
void Scaner() {
int i;
while (token = *src) {
src++;
//词法分析开始
if (token == '\n')
continue;
else if (token == '_') {
if (*src == '_') {
while (*src != '\n')
src++;
return;
}
else {
char *front = src - 1;
while ((*src >= 'a' && *src <= 'z') || (*src >= 'A' && *src <= 'Z') || (*src >= '0' && *src <= '9') || *src == '_')
src++;
int n = src - front;
for (i = 0; i < n; i++)
name_croc[count][i] = *front++;
name_croc[count++][i] = '\0';
return;
}
}
//处理标识符
else if ((token >= 'a' && token <= 'z') || (token >= 'A' && token <= 'Z') || (token == '_')) {
char *front = src - 1;
while ((*src >= 'a' && *src <= 'z') || (*src >= 'A' && *src <= 'Z') || (*src >= '0' && *src <= '9') || *src == '_')
src++;
int n = src - front;
for (i = 0; i < n; i++)
name_croc[count][i] = *front++;
name_croc[count++][i] = '\0';
return;
}
//处理整数,浮点数
else if (token >= '0' && token <= '9') {
char *front = src - 1;
while (*src >= '0' && *src <= '9')
src++;
int n = src - front;
for (i = 0; i < n; i++)
name_croc[count][i] = *front++;
name_croc[count++][i] = '\0';
return;
}
else if (token == '\t')
continue;
else if (token == '!') {
is_over = 1;
return;
}
else
return;
}
}
void GetMacroc() {
int i, n;
int j;
for (i = 0; i < count + 1; i++) {
switch (i % 3)
{
case(0): j = 0; n = 0; while (name_croc[i][j] != '\0') {
n = (name_croc[i][j] - '0') + n * 10;
j++;
}break;
case(1):break;
case(2):strcpy(name[n], name_croc[i]); break;
default:
break;
}
}
}
void Macroc() {
char str[MaxLen];
char path[MaxLen] = "1.txt";
readFile(path, str);
if (!is_over) {
src = str;
Scaner();
while (token != '!')
Scaner();
}
GetMacroc();
}
readFile()
:将文件读入
Scanner()
:将文本分割成单词
Macroc()
:对单词进行分类,将系统调用名和ID号分别获取
GetMacroc()
:main函数的功能
main: mstrace.o macroc.o
gcc mstrace.o macroc.o -o main
mstrace.o: mstrace.c
gcc -c mstrace.c -o mstrace.o
macroc.o: macroc.c macroc.h
gcc -c macroc.c -o macroc.o
clean:
-rm -rf *.o
1.ORIG_EAX 会报错not find,可以使用find
命令找到宏定义的头文件
find /usr/include/ -name *.h | xargs grep 'ORIG_EAX'
2.即使添加头文件reg.h
ORIG_EAX仍然会报错,原因是ORIG_EAX在最新的64位linux中已经不再支持,现在支持的是ORIG_RAX,同时后面也要*8,因为是64位系统,要乘上对应的字节数。
3.系统调用ID和返回值都存在寄存器EAX中,而系统调用的参数分别存在寄存器:EBX、ECX、EDX、ESI、EDI、EBP中,可以用ptrace去取出值来,但是如何将取出来的值转换成对应的名称,这里不清楚如何去做,留待以后解决。
关于系统调用可以参照这个:
链接: 系统调用.
最后,strace还有很多具体的功能,这里只是实现了最简单的系统调用值、名称、参数列表的返回,更为深入的理解留待以后再深入探究,这里先当作一次作业完成。
这篇博客讲的很好,这里参考了其中的一些内容,但是其中的代码有些许问题,需要改正后使用:
https://www.cnblogs.com/tangr206/articles/3094358.html.