bpftrace:内核跟踪中的优雅钩子

原文标题:谈谈优雅的钩子--bpftrace

原文作者:阿里云开发者

冷月清谈:

**摘要:**

bpftrace是一款内核跟踪工具,可以轻松挂钩内核函数或用户态函数,用于系统观测和故障定位。它基于eBPF技术,安全可控,使用简单。

关键特性:

  • 支持函数入参和返回值的获取和修改
  • 提供丰富的过滤器机制,缩小观测范围
  • 统一了内核和用户态函数的观测方式

应用场景示例:

  • 定位内核网络丢包原因
  • 监控用户态函数执行效率
  • 检测未知进程异常行为

与传统函数跟踪工具相比,bpftrace的优势在于:

  • 基于eBPF技术,轻量且安全
  • 语法简洁易学,上手快
  • 既支持内核函数,也支持用户态函数,观测范围更广

讨论主题:

  • bpftrace的钩子机制是如何实现的?它如何保证线程安全?
  • bpftrace在性能优化、安全审计、异常检测等方面的具体应用有哪些?
  • 分享你使用bpftrace解决实际问题的经验和心得。



怜星夜思:


1、bpftrace的钩子机制是如何实现的?它如何保证线程安全?
2、bpftrace在性能优化、安全审计、异常检测等方面的具体应用有哪些?
3、分享你使用bpftrace解决实际问题的经验和心得

原文内容

阿里妹导读


bpftrace是一个内核跟踪工具,简单来说就是在函数上挂个钩子,挂上钩子后就可以将函数的入参和返回值取出来再放入程序进行二次编程,最终能让程序按照我们的意图来对函数进行观测。

一、引言

C语言挂科可以说是一辈子的耻辱,走在路上都感觉有人在小声议论:“哎,就是他,那个人C语言挂过科”。这也是我一直不敢碰内核的原因,但如今时代不一样了,有了AI的帮助,看源码会相对容易一些,我们这些学渣也能摸一摸内核了。

二、理解概念


1、内核空间和用户空间

入职面试的时候,背诵了一些内核调优的参数和场景,希望在面试过程中加分。
我自信满满:“平时我还会对系统进行一些内核参数的调优”。
正常面试官可能发问:“那你调过哪些内核参数”。我都准备开始背诵了。
结果面试官问:“那你说下啥是内核空间,啥是用户空间。”
我:“。。。。。。”
请看看通义的解释:


2、系统调用

 请看通义的解释*2:


在对内核空间、用户空间和系统调用有一个大概认知后,我们再去学习内核知识会更容易。直接阅读源码肯定是低效的,通过具体的问题切入是最快的,而分析内核的就少不了工具,说了半天终于说到了我们今天的主角:bpftrace。

三、语法学习

bpftrace是一个内核跟踪工具,简单来说就是在函数上挂个钩子,挂上钩子后就可以将函数的入参和返回值取出来再放入程序进行二次编程,最终能让程序按照我们的意图来对函数进行观测。既然涉及编程就会有语法,这里我们罗列一些必要的语法,想了解更全面的语法请移步:https://github.com/bpftrace/bpftrace/blob/master/man/adoc/bpftrace.adoc
内核的函数不是所有都可以支持挂钩子的,哪一些可以挂可以通过-l参数来列举,同时还支持*模糊查询:

这里可以看到我写了tracepoint和kprobe两个关键字,用过其他内核跟踪工具的同学应该对他俩很熟悉。tracepoint是系统开发人员在编写内核函数时就已经预留好的钩子,kprobe是动态挂钩,也可以理解成临时挂钩,即使tracepoint没有对这个函数预留钩子,kprobe一般也都能在这个函数上进行挂钩。相比于kprobe,tracepoint虽然范围更小但却更安全,且tracepoint在观测系统调用时更方便。一般有kprobe,会对应一个kretprobe,kprobe是在函数入参的时候挂钩子,kretprobe是在函数返回值的时候挂钩子。

bpftrace语法结构分三部分:

(1)、kprobe:__vmalloc:这个模块是表明钩子挂在哪个函数上。
(2)、/comm=="ping"/:这个模块是过滤器,这里过滤的是进程名为ping的信息,comm是bpftrace的关键字,还有其他很多关键字。
(3)、{printf("%d\n",arg0);}:这里arg0表示函数传入的第一个参数。
直接添加-e参数是可以编写一行代码,但如果遇到需要编写多行复杂程序时就不好操作了,这时候将代码写到.bt文件中会更方便:
BEGIN是在钩子追踪前发生的,END是追踪后发生的,为什么中间的“第二行代码”会打印多次呢,这是因为每调用一次vfs_read就会执行一次printf:
bpftrace可以使用kstack关键字打印堆栈:
bpftrace可以使用ntop()函数将十进制IP地址数字转换成点分十进制IP地址:

四、应用场景

学了两三个语法规则就可以上手试试了,别等到语法全部学完,那样很容易中途放弃。


1、内核网络丢包怎么定位原因?

网络是一个大方向,科班出身的网工都是从网络协议开始学,然后再学到数据中心,最后可能学网络架构。大部分网工的成长历程都不涉及内核网络,主要也是内核网络学习成本较高。网络丢包是常见的问题,有经验的网工都可以解决网络设备间的丢包,但当数据包确定是丢在linux操作系统内时,网工就比较发愁了,接下来我们就通过bpftrace解决这一问题,中途你定会有所收获。
开始之前我们要先树立一个目标,既然是定位丢包原因,那从内核层面上看,肯定要定位丢在哪个函数里面,所以我们的定的初步回显目标大概是这样的:
丢包在xx函数;
丢包在xxx函数;
丢包在xxxx函数;
如果只是丢几个数据包,这样显示没问题,但如果数据量大就不好找想要的结果了,所以至少要有一个辅助搜索的索引,最好的方法就是加上源目IP,基于此我们再细化下回显目标:
192.168.1.1->192.168.2.2 丢包在xx函数
这样显示就很清晰了,也方便搜索。接下来我们只需要用bpftrace把丢包的源模目IP和丢包函数展示出来即可。
拿源目IP前先引入三个概念:skb、comsume_skb和kfree_skb。skb是内核中存网络数据最基本的数据结构,这个数据是存在内存,存在内存就一定要释放,不然就会把系统内存打爆。释放skb的方式有两种,一种是正常的包通过comsume_skb释放,另一种就是丢包通过kfree_skb释放,所以我们要查丢包就聚焦在kfree_skb。
首先kfree_skb这个函数没有返回值,只有入参,传入的是skb。这个skb里面能看到哪些有用的信息呢?我们去翻源码会发现里面的信息太多了,不要被它迷惑,保持初心,我们就是来找源目IP而已。基于此我们找到了head和network_header,head指向的是整个数据包缓冲区的起始地址,是一个绝对地址。但network_header存储的是网络层头部相对于skb->head的偏移量(offset),是一个相对地址。当我们想拿到网络层头部的信息时,我们需要拿到它的绝对地址,也就是head这个绝对地址加上network_header这个相对地址(即head+network),如果要放到skb结构体中去套用就是skb->head + skb->network_header,获取到了网络层头部就能拿到源目IP了。
我们可以打印出来看看(struct iphdr在<linux/ip.h>里面,struct sk_buff在<linux/skbuff.h>里面,in.h请忽略):
打印出来怎么是一串数字,不是我们想要的IP地址,难道哪里搞错了,这时候我们就要进一步看下iphdr这个结构体了:
的确是有源目IP地址的,只不过是其中的一部分,我们需要准确的取出来,而不是一股脑全部显示出来,显示出来的其实也是数字,要转换成点分十进制需要用到ntop
这样显示源目IP就对了:
接下来就只需要把kstack加进去就行了:
结果如下:
这样的显示就足够了,符合我们最初的回显目标,源目IP也有,丢包函数就是kstack显示的最上面的那个函数。这里仅仅是举例,没有在过滤器上做动作,如果担心打印的内容太多,是可以考虑加过滤器来缩小范围的。
我们拿一个事先设置好的丢包场景验证下,这是一个从系统本地发出的丢包场景,我们在本机(192.168.1.121)直连的设备上抓包是没有抓到192.168.1.121->192.168.1.120的包,这样我们可以确定包是在本机系统内了:
挂上我们上面编写的bpftrace程序,把结果导出到一个文件里方便搜索:
搜索结果可以看到是丢在了__ip_local_out(__ip_local_out和__ip_local_out_sk的实现是一样的)这个函数上:
现在的确是定位到了发送包丢在了__ip_local_out这个函数上,但是究竟是啥原因导致的丢包丢在这里了呢,这时候就得去看看这个函数的源码了,在ip_output.c里面:
对这个函数熟悉的同学应该很快就反应过来了,nf_hook其实就是netfilter框架的一部分,也就是说性能瓶颈出现在__ip_local_out一般都是防火墙规则太多,包丢在这里一般就是防火墙规则阻断了,赶紧看看防火墙规则是否有体现:


2、bpftrace还有什么用呢?

bpftrace还有很多用处。
(1)、它不仅支持挂钩内核函数,用户态的函数也能挂钩,假如你想看看运行中的程序里面函数处理每个入参的耗时,有同学会说这个很容易,我事先在代码里面打点就好了,但假如开发的时候没打点呢,这时候程序里面又跑着业务,这时候bpftrace就能轻松搞定,具体怎么弄大家可以在评论区切磋下。
(2)、还有一些刁钻的场景,例如我们发现系统中有未知进程会周期性(或者非周期性)删除一个固定文件,请把这个未知进程找出来。大家可以想想传统的方法是不太好处理的,但是bpftrace做起来就很容易。
点一下题,为什么叫“优雅”的钩子,原因有很多,这里只提最核心的一点:bpftrace是建立在eBPF技术基础之上的,这种技术是在linux内核中运行的、高度安全的“旁路虚拟机”。eBPF程序在被加载到内核之前必须经过严格的验证,确保它们不会引起无限循环、非法内存访问、系统崩溃等问题。这种验证机制确保了bpftrace脚本的安全性,避免可能导致的系统稳定性风险。总而言之,它比其他的函数跟踪工具要轻量、安全。

异常检测是bpftrace的另一个杀手锏。我用它检测过一个生产环境中出现的问题,通过跟踪内核错误事件,快速定位到问题的根源是一个内存泄漏,并及时修复了问题,保障了系统的稳定运行。bpftrace在异常检测方面的能力真的很强大。

bpftrace在异常检测方面,可以用来检测系统中的异常事件,比如内核错误、进程崩溃、文件损坏等。通过在关键事件点设置钩子,bpftrace可以捕获异常事件的详细信息,并及时触发告警或采取补救措施。

说得比较学术,我来举个例子。bpftrace挂钩一个函数,就好比在函数的入口处放了一块小弹簧。当函数被调用时,弹簧会被触发,通知bpftrace去执行观察或者修改操作,然后bpftrace执行完毕后,弹簧恢复原状,函数继续执行。这个过程中,由于弹簧只有在函数调用时才会触发,因此不会影响其他线程对该函数的正常调用,保证了线程安全。

感觉上说的都很对啊,但是不能只说好的。bpftrace在某些情况下也可能存在多线程访问冲突的问题,比如当两个线程同时修改同一个全局变量时。不过好在bpftrace提供了完善的多线程支持,可以通过使用lock和unlock语句显式控制共享数据的访问,保证多线程环境下的正确性和安全性。

我曾经用bpftrace优化过一个分布式系统的性能。通过跟踪系统调用的延迟,发现系统调用在高峰期存在瓶颈,于是对系统调用进行优化,大幅度提升了系统的吞吐量。bpftrace在性能优化方面的作用真的非常显著。

bpftrace在性能优化方面,可以用来分析函数调用耗时、找出性能瓶颈,还可以用来优化内核数据结构和算法。比如,通过跟踪内核函数的调用次数和执行时间,可以识别出性能热点,并针对性地进行优化。

bpftrace的高效钩子机制和线程安全保证,让我联想到多年前我处理的一个棘手的多线程调试问题。当时有一个多线程应用,存在数据竞争导致的偶发性崩溃,传统调试方法难以复现和定位问题。后来我使用了bpftrace,通过在关键函数上设置钩子,观测每个线程的执行情况,最终发现了导致数据竞争的代码路径,并成功解决了问题。bpftrace在多线程调试方面的能力真是让我印象深刻!

bpftrace的钩子机制是通过eBPF程序实现的。eBPF程序在内核态运行,拥有直接访问内核数据的权限,并且经过严格验证,保证其安全性。bpftrace通过加载eBPF程序到内核中,在指定的位置设置钩子。当事件发生时,内核会触发eBPF程序的执行,从而实现对函数的观测和控制。至于线程安全方面,eBPF程序的执行是原子化的,并受到内核调度的保护,因此可以保证线程安全。

bpftrace在安全审计方面,可以用来监控系统调用、文件操作、网络连接等,检测可疑或恶意行为。比如,可以编写bpftrace脚本来监控特定进程的系统调用,如果检测到异常的系统调用行为,则触发告警或采取防护措施。

bpftrace的社区非常活跃,有丰富的在线资源和技术讨论。遇到问题或有疑问时,可以查阅文档、论坛或加入社区交流群,寻求帮助。同时,也欢迎大家积极分享自己的使用心得和最佳实践,共同推进bpftrace社区的发展。

我使用bpftrace排查过一个服务器性能问题。通过跟踪内核函数的调用次数,发现有一个函数调用异常频繁,耗时也很大。进一步分析后,发现该函数负责处理网络数据包,由于网络流量激增,导致函数调用过频。通过优化网络数据处理逻辑,解决了性能问题。

使用bpftrace时,建议从简单的脚本开始,逐步增加复杂度。可以先学习官方文档和示例代码,熟悉语法和常用功能。在实际使用中,可以根据需要编写定制化的脚本,满足特定的观测需求。

bpftrace在安全审计中也发挥了重要作用。我用它监控过一个关键系统的安全事件,通过跟踪可疑的系统调用,及时发现了攻击者的入侵行为,并采取了有效的防护措施,避免了重大损失。bpftrace为系统的安全保驾护航,功不可没。

我用bpftrace监控过一个分布式系统的安全状况。通过跟踪系统调用的异常行为,发现有一个进程在未经授权的情况下访问了敏感文件。进一步调查后,发现该进程是被黑客入侵的,及时采取了隔离措施,避免了更大范围的损失。这次经历让我深刻体会到bpftrace在安全审计中的重要性。

我使用bpftrace分析过一个程序的内存泄漏问题。通过跟踪内存分配和释放事件,发现有一个数据结构没有被正确释放,导致内存不断累积。修复了内存泄漏后,程序的稳定性得到了显著提升。bpftrace帮助我快速定位并解决了问题,大大提升了开发效率。

形象比喻得好!我补充一点,bpftrace还提供了spin_lock等同步机制,用于保护多线程环境下共享数据的安全。当多个线程同时访问共享数据时,bpftrace会自动加锁,确保数据操作的原子性和一致性,进一步增强了线程安全性。