找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2777

积分

0

好友

368

主题
发表于 昨天 03:09 | 查看: 1| 回复: 0

想了解如何利用eBPF技术精确跟踪Shell脚本及解释型语言的执行路径,同时解决因内核行为导致的监控盲点吗?从 execve 到子进程追踪,这些方法将帮助你全面掌握脚本运行的每一个细节。

本文译自 Tracking Shell Scripts (and Python, Perl, etc) with eBPF is Hard[1]

在Linux世界里,如果你想跟踪机器上的活动,eBPF几乎总是那个标准答案。这一点在进程跟踪上尤其明显:简单地挂载到 execve 系统调用,你就能获得大量信息。

不过,如果你需要更精确的数据,比如被启动的二进制文件的确切路径,就需要深入到类似 bprm_check_security 这样的BPF-LSM钩子,它能让你获取更详尽的进程上下文。

我们团队在项目中正是这样实践的,并且一度运行得完美无缺……直到它遇到了一个棘手的问题。

程序员吐槽解释型脚本与eBPF追踪的漫画对话

解释型脚本带来的困扰

当我们运行一个常规的可执行文件时,监控日志的输出清晰明了(例如,我们记录了进程访问某些受保护文件的事件):

exec => "/usr/bin/accessor"
read file => "/tmp/foo/bar"

然而,当我们运行一个Shell脚本时,输出却变成了这样:

exec => ""
read file => "/tmp/foo/bar"

那个关键的Shell脚本路径去哪了?

问题的根源在于内核执行带有 #!(Shebang)的文件的特殊方式,这包括了Shell脚本以及Python、Perl、Ruby等所有解释型语言脚本。

内核的“两次”execve把戏

当执行一个解释型脚本时,内核悄悄增加了一个额外步骤。首次调用 execve() 会触发我们的eBPF钩子。此时,我们捕获到的数据结构看起来是这样的:

process_id => 1
executable_file => "/path/to/script.sh"
interpreter_file => ""

但是,内核会读取文件的前两个字节。一旦它识别出 #!,Linux内核会紧接着发起第二次 execve() 调用!

内核看到了shebang行,这条指令告诉它:不要直接执行这个文件;应该将其作为参数传递给一个解释器程序,由解释器来实际执行代码。关键在于,脚本文件本身作为一个可执行实体,在第二次调用时“消失”了。

我们的eBPF钩子自然也会在第二次 execve() 时被触发,这正是问题的起点:

process_id => 1
executable_file => ""
interpreter_file => "/usr/bin/bash"

第二次调用针对的是同一个进程ID,但参数已经完全改变。由于没有“可执行文件”了,进程中唯一活动的实体变成了解释器(如 /bin/bash)。因此,当我们的eBPF代码将这些新数据写入存储(一个eBPF映射)时,它会覆盖掉第一次调用时存储的数据。

所以,逻辑过程如下:

  • 第一次 execve() 之后,映射中的数据是:
    process1 : exec { path: "/path/to/script.sh", interpreter: "" }
  • 第二次 execve() 之后,数据被覆盖为:
    process1 : exec { path: "", interpreter: "/usr/bin/bash" }

解决方案其实很直接:我们需要在写入数据前增加一个检查。如果发现该进程ID对应的“路径”字段已经被记录过(即非空),就不再对其进行覆写。这样,我们就能成功保留住脚本文件的原始路径信息。对于 Bash 或其他解释器的深入监控,可以在 运维/DevOps/SRE 实践中找到更多思路。

追踪子进程:另一座待翻越的山

虽然上述方法让我们成功捕获了脚本的路径,但这并没有解决所有问题。对于Shell脚本,我们还需要注意另一个特性。

你想过吗?大多数在Shell脚本中执行的操作,其实是通过调用其他独立的可执行文件来完成的。比如,当你想在脚本里读取一个文件内容时,通常会使用 cat 这个命令(即 /bin/cat 这个可执行文件)。

cat 会作为一个全新的子进程被启动。因此,如果我们监控一个打开了 file.txt 的Shell脚本,我们期望的输出是:

exec => "/path/to/script.sh"
read file => "file.txt"

可实际我们得到的却是:

exec => "/usr/bin/cat"
read file => "file.txt"

这是因为Shell脚本本身从未直接执行文件操作;它将任务“委派”给了自己创建的子进程。这就导致了监控视角的割裂:父进程(脚本)和子进程(如 cat)在监控日志中表现为两个独立且不直接关联的实体。

因此,如果我们想要精确地归因行为——例如,知道是哪个脚本最终导致了某个文件被读取——仅仅跟踪单一的 execve 调用是不够的。我们还需要跟踪进程的派生关系,将整个进程树(父脚本及其所有子进程)的行为进行关联和分析。这涉及到更复杂的 eBPF 程序逻辑,需要挂钩 forkclone 等系统调用来构建和维护进程间的父子关系图谱。

总结

使用 eBPF 跟踪解释型脚本,初看简单,实则暗藏两层挑战:

  1. Shebang导致的数据覆盖:内核的双重 execve 调用会覆盖我们第一次记录的脚本路径。解决方法是在写入 eBPF 映射时,检查并保留非空的路径字段。
  2. 子进程的行为归因:脚本的真实行为往往隐藏在它调用的子进程中。要获得完整的视图,必须追踪进程树,将子进程的行为正确地关联回其父脚本。

克服这些挑战,你的 eBPF 监控工具才能真正做到明察秋毫,无论是编译型二进制文件还是灵活的脚本,都逃不过你的法眼。想了解更多底层系统调用和内核机制的细节,可以关注相关的技术讨论。

引用链接

[1] Tracking Shell Scripts (and Python, Perl, etc) with eBPF is Hard: https://substack.bomfather.dev/p/tracking-shell-scripts-and-python




上一篇:前端架构设计:资深级系统设计思维与2026最佳实践详解
下一篇:从命令式循环到声明式查询:深入解析LINQ的延迟执行核心原理
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-6 04:57 , Processed in 0.347196 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表