通过实验学习系统调用

一、实验三 系统调用

此次实验的基本内容是:在 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

主要做了以下修改:

  1. 为新增的系统调用编写代码实现,在linux-0.11/kernel目录下,创建一个文件 who.c
  2. 添加iam和whoami系统调用编号的宏定义(_NR_xxxxxx),文件:include/unistd.h
  3. 修改系统调用总数, 文件:kernel/system_call.s
  4. 声明新增的系统调用函数并维护系统调用表,文件:include/linux/sys.h
  5. 修改 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的扩展技术。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值