一、实验三 系统调用
此次实验的基本内容是:在 Linux 0.11 上添加两个系统调用,并编写两个简单的应用程序测试它们。
(一)知识点
调用系统调用和调用一个普通的自定义函数的区别:
调用自定义函数是通过 call 指令直接跳转到该函数的地址,继续运行
调用系统调用是调用系统库中为该系统调用编写的一个接口函数,叫 API,API 并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用
close() 的 API:
int close(int fd)
{
long __res;
__asm__ volatile ("int $0x80"
: "=a" (__res)
: "0" (__NR_close),"b" ((long)(fd)));
if (__res >= 0)
return (int) __res;
errno = -__res;
return -1;
}
先将宏 NR_close(其中 NR_close 就是系统调用的编号) 存入 EAX,将参数 fd 存入 EBX,然后进行 0x80 中断调用。**调用返回后,**从 EAX 取出返回值,存入 res,再通过对 __res 的判断决定传给 API 的调用者什么样的返回值。
(二)实验过程
**代码修改参考:**https://blog.csdn.net/leoabcd12/article/details/122268321
主要做了以下修改:
- 为新增的系统调用编写代码实现,在linux-0.11/kernel目录下,创建一个文件 who.c
- 添加iam和whoami系统调用编号的宏定义(_NR_xxxxxx),文件:include/unistd.h
- 修改系统调用总数, 文件:kernel/system_call.s
- 声明新增的系统调用函数并维护系统调用表,文件:include/linux/sys.h
- 修改 Makefile
测试程序:
iam.c代码如下:
#define __LIBRARY__
#include <unistd.h>
#include <errno.h>
#include <asm/segment.h>
#include <linux/kernel.h>
_syscall1(int, iam, const char*, name);
int main(int argc, char *argv[])
{
/*调用系统调用iam()*/
iam(argv[1]);
return 0;
}
whoami.c代码如下:
#define __LIBRARY__
#include <unistd.h>
#include <errno.h>
#include <asm/segment.h>
#include <linux/kernel.h>
#include <stdio.h>
_syscall2(int, whoami,char *,name,unsigned int,size);
int main(int argc, char *argv[])
{
char username[64] = {0};
/*调用系统调用whoami()*/
whoami(username, 24);
printf("%s\n", username);
return 0;
}
运行结果:
为什么这里会打印2次?
因为在系统内核中执行了 printk() 函数,在用户模式下又执行了一次 printf() 函数。
(三)总结
在做此实验之前,我先把书上第六章系统调用的内容过了一遍,对系统调用的过程有了一定的认识,再来做这个实验感觉就是在一步一步的落实系统调用,经过这个学习过程,我对系统调用有了更加清晰地认识。
以xyz()系统调用为例说明系统调用的过程,如下图所示:
二、使用strace追踪你写的用户态程序
用户态程序:
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t fpid,pr;
fpid = fork();
if(fpid<0)
printf("error in fork !\n");
else if(fpid==0)
{
printf("it's a child process,my process id is %d\n",getpid());
}
else
{
pr=wait(NULL);
printf("I catched a child process with pid of %d\n",pr);
}
exit(0);
}
**strace:**在Linux系统中, strace是一种相当有效的跟踪工具,它的主要特点是可以被用来监视系统调用。我们不仅可以用strace调试一个新开始的程序,也可以调试一个已经在运行的程序
执行指令strace -T ./fork,-T 显示每一调用所耗的时间,运行结果如下图所示:
输出的每一行对应一次系统调用,其格式为“左边=右边”,等号左边是系统调用的函数名及其参数,右边是该调用的返回值,“<>”内的是该系统调用所耗费的时间
理解:与用户态程序相对应,从上图可以看出,该程序相应的调用了clone()系统调用、write()系统调用、wait4()系统调用、exit_group()系统调用,其他系统调用在用户态程序中无明显对应,这里不做解释,这里重点学习下clone()系统调用:
通过指令man clone查看系统调用clone函数声明
参数child_stack:表示把用户态堆栈指针赋给子进程的esp寄存器,调用进程(父进程)应该总是为子进程分配新的堆栈。ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶
参数flags:标志用来描述你需要从父进程继承哪些资源,“=”后面的就是要继承的资源
参数parent_tidptr:只在创建线程时有意义,如果参数flags指定了标志位CLONE_PARENT_SETTID,那么调用线程需要把新线程的进程标识符写到参数parent_tidptr指定的位置,也就是新线程保存自己的进程标识符的位置。
参数tls:只在创建线程时有意义,如果参数flags指定了标志位CLONE_SETTLS,那么参数tls指定新线程的线程本地存储的地址。
参数child_tidptr:只在创建线程时有意义,存放新线程保存自己的进程标识符的位置。如果参数flags指定了标志位CLONE_CHILD_CLEARTID,那么线程退出时需要清除自己的进程标识符。如果参数flags指定了标志位CLONE_CHILD_SETTID,那么新线程第一次被调度时需要把自己的进程标识符写到参数child_tidptr指定的位置。
三、bpftrace学习
1.列出所有探针
sudo bpftrace -l 列出所有探测点,并且可以添加搜索项,如:
sudo bpftrace -l ‘tracepoint:syscalls:sys_enter_*’,运行结果:
bpftrace 的一个核心概念是探针点,即 eBPF 程序可以连接到的(内核或用户空间的)代码中的测量点,可以分成以下几大类:
kprobe——内核函数的开始处
kretprobe——内核函数的返回处
uprobe——用户级函数的开始处
uretprobe——用户级函数的返回处
tracepoint——内核静态追踪点
usdt——用户级静态追踪点
profile——基于时间的采样
interval——基于时间的输出
software——内核软件事件
hardware——处理器级事件
2.Hello World
sudo bpftrace -e ‘BEGIN { printf(“hello world\n”); }’
参数说明:
-e :指明一个程序,构建一个所谓的“单行程序”
BEGIN :一个特殊的探针名,只在执行一开始生效一次,每次探针命中时,大括号 {} 内的操作(这个例子中只是一个 printf)都会执行
运行结果:
3.跟踪文件打开的时候打印进程名和文件名
sudo bpftrace -e ‘tracepoint:syscalls:sys_enter_openat { printf(“%s %s\n”, comm, str(args->filename)); }’
参数说明:
tracepoint:syscalls:sys_enter_openat:这个是tracepoint探针类型(内核静态跟踪),当进入openat()系统调用时执行该探针
comm:是内核变量,代表当前进程的名字
args:是一个指针,指向该tracepoint的参数,这个结构时由bpftrace根据tracepoint信息自动生成的
args->filename:用来获取args的成员变量filename的值
str():用来把字符串指针转换成字符串
运行结果:
4.进程的系统调用记数统计
sudo bpftrace -e ‘tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }’
参数说明:
@:表示一种特殊的变量类型,称为map,可以以不同的方式来存储和描述数据
[]:可选的中括号允许设置map的关键字
count():这个是一个map函数 - 记录被调用次数
运行结果:
5.read()分布统计
sudo bpftrace -e ‘tracepoint:syscalls:sys_exit_read /pid == 10/ { @bytes = hist(args->ret); }’
参数说明:
/…/:这里设置一个过滤条件(条件判断),满足该过滤条件时才执行{}里面的动作
ret:表示函数的返回值,对于sys_read(),-1表示错误,其它则表示成功读取的字节数。
@: 类似于上面的map,但是这里没有[],使用"bytes"修饰输出
hist():一个map函数,用来描述直方图的参数。输出行以2次方的间隔开始,如[8, 16)表示值大于等于8且小于16,后面跟着位于该区间的个数统计
运行结果:
下面就不再过多举例,推荐两个使用bpftrace的网站:
https://www.linuxprobe.com/ebpf-bpftrace.html
https://blog.csdn.net/Rong_Toa/article/details/115696444
**理解:**bpftrace的程序在终端一行代码即可实现,使用起来相对比较方便和简单,但它只能实现一些简单的应用,我们更应该去掌握ebpf的使用,即bpftrace的扩展技术。