OS——strace的简单实现

[OS] strace的简单实现

  • strace简介
  • ptrace简介
  • 基本流程
  • 函数名的获取
    • 获取ID及对应函数名
    • 处理文本
      • 头文件
      • 代码段
      • Makefile
  • 遇到的一些问题
  • 总结

操作系统课上留了这样一个作业:用ptrace实现一下strace的功能。这里只做到了简单实现,功能不够全,而且不够完善,这里记录一下,留待将来解决。

strace简介

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的返回函数名,参数列表和返回值。

ptrace简介

函数原型:

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内部如何把寄存器里得到的值转换成函数名这里不清楚,这里用了取巧的方法。

获取ID及对应函数名

获取系统调用表有很多方法,这里简答介绍一种:

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函数的功能

Makefile

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.hORIG_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.

你可能感兴趣的:(OS)