7.2.1-内核bpf的实现原理
基本格式 (在终端)
bpftrace -e
'
BEGIN //事件 {
printf("start\n"); //行为
}
'
tracepoint:syscalls:sys_enter_accept //事件
/comm == "sshd"/ 条件
{
printf("accept\n"); //行为
}
END //事件 {
printf("end\n"); //行为
}
'
BEGIN和END是特殊的挂载点,它们不是由内核事件(如系统调用、函数执行)触发的,而是由bpftrace工具本身的启动动作 触发的, 且只执行一次。- 常见的挂载类型有
tp(tracepoint的缩写),kprobe,uprobe,kretprobe。
tcp_connect.bt
#include <net/sock.h>
#include <linux/socket.h>
BEGIN
{
printf("Enter tcp_connect Case \n");
printf("%8s %6s %16s" , "TIME" , "PID" , "COMM");
printf("%19s:%5s %16s:%5s\n","SADDR","SPORT","DADDR","DPORT");
}
kprobe:tcp_connect
{
$sk = (struct sock*)arg0;
$inet_family = $sk->__sk_common.skc_family;
if ($inet_family == AF_INET) { //IPV4
$daddr = ntop($sk->__sk_common.skc_daddr);
$saddr = ntop($sk->__sk_common.skc_rcv_saddr);
}
$dport = $sk->__sk_common.skc_dport;
$lport = $sk->__sk_common.skc_num;
$dport = bswap($dport);
time("%H:%M:%S ");
printf("%6d %16s",pid,comm);
printf("%19s:%5d %16s:%5d\n",$saddr,$lport,$daddr,$dport);
}
END
{
printf("Exit tcp_connect Case\n");
}
-
#include <linux/socket.h>常用到,用于获取struct sock(套接字结构体)的定义以及AF_INET等常量的定义。 -
kprobe-
优先使用
Tracepoint再去使用Kprobe,因为Kprobe的开销比较大。 -
当挂载
Kprobe时,内核会将目标函数入口的第一条指令替换为int3(断点指令) , 再跳转到Kprobe对应的bpftrace的代码。 -
Tracepoint是 "预埋点",当开启时,内核会修改内存中的那条指令,使其跳转到追踪函数, 少了中断的代价。 -
Kprobe能覆盖大部分函数,能不能用,指令查就好,比如这个例子,肯定要去查tcp相关的函数-
bpftrace -l 'tracepoint:tcp*' -
bpftrace -l 'kprobe:tcp*'
-
-
-
$sk = (struct sock*)arg0;-
以
$标志起来定义与引用变量,例如:$idx = 0; -
思考一个问题:既然代码是在
kprobe的处理程序里运行的,怎么能拿原本属于tcp_connect的参数呢?- 虽然
kprobe代码是挂载的,但在挂载触发的那一瞬间,原本用来传递参数的硬件状态(寄存器)还没有被改变,你完全可以读取它。
- 虽然
-
怎么知道这些参数的含义? 直接查内核代码 (
https://elixir.bootlin.com/linux/v6.8/source)



-
-
ntop($sk->__sk_common.skc_daddr)- 内核里存 IP 地址是一个 32 位的整数(比如
0x0100007f),人看不懂。ntop会自动把它转换成字符串形式(比如127.0.0.1)。
- 内核里存 IP 地址是一个 32 位的整数(比如
-
端口的字节序
-
skc_dport(目的端口):通常存储为 网络字节序 (Big Endian)。读取后必须用ntohs()或bpf_ntohs()转换。 -
skc_num(本地端口):Linux内核为了优化查找速度,通常存储为 主机字节序 (Host Endian)。注意!它通常不需要转字节序,或者处理方式不同。(这是一个非常反直觉的知识点,也是面试常考题)。 -
$dport = bswap($dport);,字节翻转。
-
-
结果
- 操作在本机上
curl www.baidu.com,通过ping检查发现是对的,80是http的端口。


- 操作在本机上
tcp_accept.bt
#include <net/sock.h>
#include <linux/socket.h>
BEGIN
{
printf("BEGIN\n");
printf("Enter tcp_connect Case \n");
printf("%8s %6s %16s" , "TIME" , "PID" , "COMM");
printf("%19s:%5s %16s:%5s\n","SADDR","SPORT","DADDR","DPORT");
}
kretprobe:inet_csk_accept
{
$sk = (struct sock*)retval;
$inet_family = $sk->__sk_common.skc_family;
if ($inet_family == AF_INET) {
$daddr = ntop($sk->__sk_common.skc_daddr);
$saddr = ntop($sk->__sk_common.skc_rcv_saddr);
}
$dport = $sk->__sk_common.skc_dport;
$lport = $sk->__sk_common.skc_num;
$dport = bswap($dport);
time("%H:%M:%S ");
printf("%6d %16s",pid,comm);
printf("%19s:%5d %16s:%5d\n",$saddr,$lport,$daddr,$dport);
}
END
{
printf("END\n");
}
-
kretprobe-
主要和
kprobe对比,之前说过了,在函数的入口出软断,再跳转到kprobe代码中,这个时候能拿到的数据就是函数参数(arg0, arg1...) -
而
kretprobe是在函数执行完毕准备返回时,再软中断,能拿到返回值 (retval)。
-
-
看一下挂载点

-
retval是bpftrace的内置关键字,代表返回值。 -
结果


tcp_life.bt
#include <net/sock.h>
#include <linux/socket.h>
BEGIN
{
printf("%8s %6s %16s" , "TIME" , "PID" , "COMM");
printf("%19s:%5s %16s:%5s %s\n","SADDR","SPORT","DADDR","DPORT","DURms");
}
kprobe:tcp_set_state
{
$sk = (struct sock*)arg0;
$newstate = arg1;
if ($newstate <= TCP_SYN_RECV) { //begin
@birth[$sk] = nsecs;
}
if ($newstate == TCP_CLOSE) { //end
$dur = (nsecs - @birth[$sk]) / 1e6;
$inet_family = $sk->__sk_common.skc_family;
if ($inet_family == AF_INET) {
$daddr = ntop($sk->__sk_common.skc_daddr);
$saddr = ntop($sk->__sk_common.skc_rcv_saddr);
}
$dport = $sk->__sk_common.skc_dport;
$lport = $sk->__sk_common.skc_num;
$dport = bswap($dport);
time("%H:%M:%S ");
printf("%6d %16s",pid,comm);
printf("%19s:%5d %16s:%5d %d\n",$saddr,$lport,$daddr,$dport,$dur);
}
printf("tcp_set_state: %d\n",$newstate);
}
END
{
printf("END\n");
}
-
看一下挂载点


-
nsecs是内置变量,代表当前的时间戳,单位是ns -
$newstate <= TCP_SYN_RECV-
newstate的几种状态
-
分析情况
<= 3的状态包括:-
(
ESTABLISHED):瞬间建立完成。 -
(
SYN_SENT):客户端刚发起。 -
(
SYN_RECV):服务端刚收到。
-
逻辑:只要连接进入了这三个“开局”状态中的任何一个,就视为生命周期的开始,记录时间戳。
-
当时的实验是令
$newstate == SYN_SENT是不行的。这样,map[sock]的值是零,用当前的nsecs - 0就是一个很大的值。
-
-
-
讨论一种情况(这里为什么要用
@)伪代码
//begin if (state == SYN_SENT) { $start = nsecs; } //end if (state == CLOSED) { $dur = nsecs - $start }问题:当有多个进程进行
tcp连接的时候,公共变量$start会被反复刷新 -
这个时候引用内置函数
map
//begin
if (state == SYN_SENT) {
@start[sk] = nsecs;
}
//end
if (state == CLOSED) {
@dur[sk] = nsecs - @start[sk];
}
-
注意
@x(@+ 单变量),x这是为全局变量。 -
结果

-
这里的
comm为什么是irq/xxx-iwlwifi, 不是networkio
-
bpftrace里的comm和pid变量,并不是指“这个Socket属于谁”,而是指 “此时此刻,是谁在CPU上执行tcp_set_state这行代码”
-
nginx.bt
#include <net/sock.h>
#include <linux/socket.h>
BEGIN
{
printf("NGINX\n");
}
uprobe:/usr/sbin/nginx:ngx_close_connection
{
printf("ngx_close_connection\n");
}
END
{
printf("Exit\n");
}
-
这个问题实际上是在探讨应用层(
Nginx)与内核层(Kernel)在连接关闭行为上的因果关系。是Nginx觉得自己处理完了(或者等烦了)主动挂的电话?还是Nginx发现对面已经挂了(或者网断了),被迫挂的电话? -
uprobe是检测用户态的,注意它和其他的挂载点不一样的,它是要用程序绝对地址:函数 -
nginx是80端口 -
nginx主动调用了ngx_close_connection断开服务(netassist没有发消息)
redis.bt
#include <net/sock.h>
#include <linux/socket.h>
BEGIN
{
printf("Redis\n");
}
uprobe:/usr/bin/redis-server:call
{
printf("redis --> call\n");
}
END
{
printf("Exit\n");
}
- 结果


mysqld.bt
BEGIN
{
printf("Tracing MySQL Server\n");
printf("%10s %6s %s\n","TIME(ns)","PID","SQL");
}
uprobe:/usr/sbin/mysqld:*dispatch_command*
{
@start = nsecs;
}
uprobe:/usr/sbin/mysqld:*dispatch_command*
/@start/
{
$dur = nsecs - @start;
printf("%10u %6d %s\n",$dur,pid,str(*arg1));
}
END
{
printf("Exit\n");
}
- 结果


malloc.bt
BEGIN
{
printf("%10s %16s %6s\n","TYPE","NAME","PID");
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc
/comm == "King"/
{
$size = arg0;
printf("%10s %16s %6d\n","malloc",comm,$size);
}
uprobe:/lib/x86_64-linux-gnu/libc.so.6:free
/comm == "King"/
{
$ptr = arg0;
printf("%10s %16s %6p\n","free",comm,$ptr);
}
END
{
printf("Exit\n");
}
-
普及: 动态库和静态库的区别:
-
.so--> 动态库,进程运行时加载进来的(如果.exe用到了.so,.so文件必须存在)。
.a--> 静态库,编译的时候,一起编入到可执行程序 -
总结:动态库挂载到
.so文件上;静态库挂载到可执行文件上
-
-
malloc和free内存管理函数都依托于libc.so.6这个动态库 -
结果


-
malloc是申请大小,free是回收的地址
ntyco.bt
BEGIN
{
printf("Tracing NtyCo\n");
}
uprobe:/home/king/share/2404/7.2.1-bpf/NtyCo/nty_server:nty_coroutine_create
{
printf("name: %s, pid: %d",comm,pid);
}
END
{
printf("Exit\n");
}
-
静态库追踪不追踪
.a,反而追踪可执行文件?-
看到的
nty_server这个可执行文件,已经把静态库.a“吃” 进肚子里了。 -
所谓的“静态库追踪”,指的不是去追踪那个躺在硬盘上的
.a文件,而是追踪被编译进可执行文件里的那个库的函数。
-

浙公网安备 33010602011771号