至少在 x86 上,Kprobes 的实现依赖于在 Kprobe 处理程序运行时禁用抢占这一事实。
当您在一条指令上放置一个普通的(不是基于 Ftrace 的)Kprobe 时,该指令的第一个字节将被 0xcc(int3,“软件断点”)覆盖。如果内核尝试执行该指令,则会发生陷阱并被kprobe_int3_handler()
调用(参见do_int3()的实现)。
要调用您的 Kprobe 处理程序,kprobe_int3_handler()会找到哪个 Kprobe 命中,将其保存为 percpu 变量current_kprobe
并调用您的预处理程序。之后,它准备一切以单步执行原始指令。单步执行后,调用您的后处理程序,然后执行一些清理。current_kprobe
和一些其他的每 cpu 数据用于完成所有这些。只有在那之后才启用抢占。
现在,假设预处理程序启用了抢占,立即被抢占并在不同的 CPU 上恢复。如果 Kprobes 的实现试图访问current_kprobe
或其他 per-cpu 数据,内核可能会崩溃(如果此时该 CPU 上没有 current_kprobe,则 NULL 指针 deref)或更糟。
或者,被抢占的处理程序可以在同一个 CPU 上恢复,但另一个 Kprobe 可能会在它休眠时到达那里 -current_kprobe
等将被覆盖,并且很可能发生灾难。
在 Kprobe 处理程序中重新启用抢占可能会导致难以调试的内核崩溃和其他问题。
所以,简而言之,这是因为 Kprobes 是这样设计的,至少在 x86 上是这样。关于它们在其他架构上的实现,我不能说太多。
根据您要完成的工作,其他内核工具可能会有所帮助。
例如,如果您只需要在某些函数的开头运行代码,请查看Ftrace。然后,您的代码将在与您挂钩的函数相同的条件下运行。
话虽如此,我的一个项目实际上需要使用 Kprobes,以便处理程序在与被探测指令相同的条件下运行。你可以在这里找到实现。然而,它必须在不破坏任何东西的情况下跳过箍来实现这一目标。到目前为止它工作正常,但它比我想要的更复杂,也有可移植性问题。