KProbes is an in-kernel dynamic instrumentation mechanism that allows developers to gather information about kernel operation without recompiling or rebooting. This implementation is inspired by Linux kernel KProbes but adapted for the ARM Cortex-M architecture.
KProbes is currently implemented for ARMv7-M using hardware breakpoints provided by the Flash Patch and Breakpoint Unit (FPB). The FPB is part of the ARMv7-M Debug Architecture and provides hardware comparators that can trigger debug exceptions on instruction fetch.
When a kprobe is registered at a specific address:
- One of the FPB comparators is programmed with the target address
- On every instruction fetch, the FPB compares the program counter against registered comparators
- When a match occurs, the FPB generates a DebugMon exception
- The Debug Fault Status Register (DFSR) bit-BKPT is set to indicate a breakpoint hit
The kprobe handler execution follows this sequence:
- DebugMon exception triggers at the probed address
- All registered
pre_handlerfunctions for that address are called - The kernel sets bit-MON_STEP in Debug Exception and Monitor Control Register
- Execution returns to the probed instruction for single-step execution
- Another DebugMon exception triggers after the instruction executes
- The kernel checks bit-HALTED in DFSR to confirm single-step completion
- All registered
post_handlerfunctions are called - Normal execution resumes
If a DebugMon exception occurs without sufficient exception priority to preempt current execution, priority escalation causes a Hard Fault. This commonly happens when the cpsid instruction has disabled interrupts. Avoid placing kprobes in interrupt-disabled sections.
#include <kprobes.h>
int kprobe_register(struct kprobe *kp);
int kprobe_unregister(struct kprobe *kp);Both functions return 0 on success or a negative value on error.
struct kprobe {
void *addr; /* Address to probe */
kprobe_pre_handler_t pre_handler; /* Called before probed instruction */
kprobe_post_handler_t post_handler; /* Called after probed instruction */
};typedef int (*kprobe_pre_handler_t)(struct kprobe *kp,
uint32_t *kp_stack,
uint32_t *kp_regs);
typedef int (*kprobe_post_handler_t)(struct kprobe *kp,
uint32_t *kp_stack,
uint32_t *kp_regs);kp: Pointer to the registered kprobe structurekp_stack: Pointer to the interrupted stack (Main_SP or Process_SP), containing registers r0-r3, r12, lr, pc, psrkp_regs: Pointer to saved registers r4-r11
Use the provided register offset macros:
/* Access r0 from exception frame */
uint32_t r0_value = kp_stack[REG_R0];
/* Access r4 from saved registers */
uint32_t r4_value = kp_regs[KP_REG_R4];Handlers can read and modify register values through these pointers, allowing inspection and alteration of program state at the probe point.
Register a kprobe at the ktimer_handler function:
#include <kprobes.h>
int my_pre_handler(struct kprobe *kp, uint32_t *stack, uint32_t *regs)
{
/* Log entry to ktimer_handler */
dbg_printf(DL_KDB, "ktimer_handler called, r0=%x\n", stack[REG_R0]);
return 0;
}
struct kprobe ktimer_probe = {
.addr = ktimer_handler,
.pre_handler = my_pre_handler,
.post_handler = NULL,
};
void setup_probes(void)
{
kprobe_register(&ktimer_probe);
}- Limited number of simultaneous probes (constrained by FPB comparator count)
- Cannot probe code running with interrupts disabled
- ARM Cortex-M specific implementation
- Single-stepping overhead affects timing-sensitive code