为什么 kprobes 禁用抢占以及何时可以安全地重新启用它?
Why do kprobes disable preemption and when is it safe to reenable it?
根据docs,kprobes 禁用抢占:
Probe handlers are run with preemption disabled. Depending on the
architecture and optimization state, handlers may also run with
interrupts disabled (e.g., kretprobe handlers and optimized kprobe
handlers run without interrupt disabled on x86/x86-64).
从 commit 9a09f261a 我们可以清楚地看到优化的 kprobes 用于 运行 启用抢占。
为什么会这样?我将 kprobes 理解为一种在内核中的特定地址注入一些代码的方法,并且理解任何代码都应该没问题。
- 是什么让 kprobes 如此特殊以至于必须禁用抢占?
- 什么情况下可以重新启用抢占?
至少在 x86 上,Kprobes 的实现依赖于在 Kprobe 处理程序 运行.
时禁用抢占这一事实
当您将一个普通的(不是基于 Ftrace 的)Kprobe 放在一条指令上时,该指令的第一个字节将被 0xcc(int3,"software breakpoint")覆盖。如果内核试图执行该指令,则会发生陷阱并调用 kprobe_int3_handler()
(请参阅 do_int3() 的实现)。
要调用您的 Kprobe 处理程序,kprobe_int3_handler() 找到哪个 Kprobe 命中,将其保存为每个 cpu 变量 current_kprobe
并调用您的预处理程序。之后,它准备一切以单步执行原始指令。单步执行后,将调用您的 post-处理程序,然后执行一些清理工作。 current_kprobe
和其他一些 per-cpu 数据用于执行所有这些操作。仅在此之后启用抢占。
现在,假设预处理器启用了抢占,立即被抢占并在另一个 CPU 上恢复。如果 Kprobes 的实现尝试访问 current_kprobe
或其他 per-cpu 数据,内核可能会崩溃(如果 CPU 上没有 current_kprobe,则内核可能会崩溃(NULL pointer deref)那一刻)或更糟。
或者,被抢占的处理程序可以在同一个 CPU 上恢复,但另一个 Kprobe 可能会在它休眠时命中那里 - current_kprobe
,等等将被覆盖,并且很可能发生灾难。
在 Kprobe 处理程序中重新启用抢占可能会导致难以调试的内核崩溃和其他问题。
所以,简而言之,这是因为 Kprobes 是这样设计的,至少在 x86 上是这样。我不能多说它们在其他架构上的实现。
根据您要完成的目标,其他内核工具可能会有所帮助。
例如,如果您只需要 运行 您的代码在某些函数的开头,请查看 Ftrace。然后,您的代码将 运行 处于与您挂钩的函数相同的条件下。
综上所述,我的一个项目实际上需要使用 Kprobes,以便处理程序 运行 在相同条件下 w.r.t。抢占作为探测指令。您可以找到实现 here。然而,它必须克服重重困难才能在不破坏任何东西的情况下实现这一目标。到目前为止它一直工作正常,但它比我想要的更复杂,也有可移植性问题。
根据docs,kprobes 禁用抢占:
Probe handlers are run with preemption disabled. Depending on the architecture and optimization state, handlers may also run with interrupts disabled (e.g., kretprobe handlers and optimized kprobe handlers run without interrupt disabled on x86/x86-64).
从 commit 9a09f261a 我们可以清楚地看到优化的 kprobes 用于 运行 启用抢占。
为什么会这样?我将 kprobes 理解为一种在内核中的特定地址注入一些代码的方法,并且理解任何代码都应该没问题。
- 是什么让 kprobes 如此特殊以至于必须禁用抢占?
- 什么情况下可以重新启用抢占?
至少在 x86 上,Kprobes 的实现依赖于在 Kprobe 处理程序 运行.
时禁用抢占这一事实当您将一个普通的(不是基于 Ftrace 的)Kprobe 放在一条指令上时,该指令的第一个字节将被 0xcc(int3,"software breakpoint")覆盖。如果内核试图执行该指令,则会发生陷阱并调用 kprobe_int3_handler()
(请参阅 do_int3() 的实现)。
要调用您的 Kprobe 处理程序,kprobe_int3_handler() 找到哪个 Kprobe 命中,将其保存为每个 cpu 变量 current_kprobe
并调用您的预处理程序。之后,它准备一切以单步执行原始指令。单步执行后,将调用您的 post-处理程序,然后执行一些清理工作。 current_kprobe
和其他一些 per-cpu 数据用于执行所有这些操作。仅在此之后启用抢占。
现在,假设预处理器启用了抢占,立即被抢占并在另一个 CPU 上恢复。如果 Kprobes 的实现尝试访问 current_kprobe
或其他 per-cpu 数据,内核可能会崩溃(如果 CPU 上没有 current_kprobe,则内核可能会崩溃(NULL pointer deref)那一刻)或更糟。
或者,被抢占的处理程序可以在同一个 CPU 上恢复,但另一个 Kprobe 可能会在它休眠时命中那里 - current_kprobe
,等等将被覆盖,并且很可能发生灾难。
在 Kprobe 处理程序中重新启用抢占可能会导致难以调试的内核崩溃和其他问题。
所以,简而言之,这是因为 Kprobes 是这样设计的,至少在 x86 上是这样。我不能多说它们在其他架构上的实现。
根据您要完成的目标,其他内核工具可能会有所帮助。
例如,如果您只需要 运行 您的代码在某些函数的开头,请查看 Ftrace。然后,您的代码将 运行 处于与您挂钩的函数相同的条件下。
综上所述,我的一个项目实际上需要使用 Kprobes,以便处理程序 运行 在相同条件下 w.r.t。抢占作为探测指令。您可以找到实现 here。然而,它必须克服重重困难才能在不破坏任何东西的情况下实现这一目标。到目前为止它一直工作正常,但它比我想要的更复杂,也有可移植性问题。