kprobe函数入口时的汇编跳板执行流程与栈帧机制

文章目录

前言

本篇环境开启了CONFIG_FRAME_POINTER,这个选项在现代fedora38+/ubuntu24+已在应用层软件构建中普遍开启,有栈帧更有利于分析栈帧,帮助运维/性能分析,减少栈帧是则是以前为了提升一丢丢的性能(1-2%)的"弯路"/"奇淫巧技"(栈回溯信息通过信息埋点放到单独的一个elf节中)

看一下kprobe handle_mm_fault时发生了什么

linux_6.6-58-comment 内核代码注释
原文地址 eBPF/kprobe函数入口时的汇编跳板执行流程与栈帧机制.md

环境

需要kprobe handle_mm_fault函数,内核开启ftrace后会在函数开头插入call fentry函数,但是在内核启动时候ftrace模块会把这些地址替换成nop指令:

复制代码
-exec x/10i handle_mm_fault
   0xffffffff812e3880 <handle_mm_fault>:	endbr64
   0xffffffff812e3884 <handle_mm_fault+4>:	nop    DWORD PTR [rax+rax*1+0x0] # 这里 hock为止
   0xffffffff812e3889 <handle_mm_fault+9>:	push   rbp
   0xffffffff812e388a <handle_mm_fault+10>:	mov    rax,QWORD PTR gs:0x2ca80
   0xffffffff812e3893 <handle_mm_fault+19>:	mov    rbp,rsp

同事注意到,这个位置还没有建立栈帧(push rbp, mov rbp,rsp 两条指令)

Hock

创建跳板发生在/root/qemu/linux-6.6.58/arch/x86/kernel/ftrace.c: create_trampoline函数中,返回的就是一个跳板

复制代码
/root/qemu/linux-6.6.58/arch/x86/kernel/ftrace.c: 313

static unsigned long
create_trampoline(struct ftrace_ops *ops, unsigned int *tramp_size)

先看一下返回的跳板指令:

复制代码
-exec x/100i  ops->trampoline
   0xffffffffa0201000:	endbr64
   0xffffffffa0201004:	pushf
   0xffffffffa0201005:	push   rbp
   0xffffffffa0201006:	push   QWORD PTR [rsp+0x18]
   0xffffffffa020100a:	push   rbp
   0xffffffffa020100b:	mov    rbp,rsp
   0xffffffffa020100e:	push   QWORD PTR [rsp+0x20]
   0xffffffffa0201012:	push   rbp
   0xffffffffa0201013:	mov    rbp,rsp
   0xffffffffa0201016:	sub    rsp,0xa8
   0xffffffffa020101d:	mov    QWORD PTR [rsp+0x50],rax
   0xffffffffa0201022:	mov    QWORD PTR [rsp+0x58],rcx
   0xffffffffa0201027:	mov    QWORD PTR [rsp+0x60],rdx

跳板地址是0xffffffffa0201000

看一下跳板进入时刻的状态:

复制代码
-exec b *0xffffffffa0201000
Breakpoint 9 at 0xffffffffa0201000

-exec disassemble
Dump of assembler code for function handle_mm_fault:
   0xffffffff812e3880 <+0>:	endbr64
=> 0xffffffff812e3884 <+4>:	call   0xffffffffa0201000   # nop 替换为了call 跳板
   0xffffffff812e3889 <+9>:	push   rbp
   0xffffffff812e388a <+10>:	mov    rax,QWORD PTR gs:0x2ca80
   0xffffffff812e3893 <+19>:	mov    rbp,rsp

此时的sp值:

复制代码
-exec p $sp
$2 = (void *) 0xffffc90000013ed8

跟着执行看

注释中的寄存器值是当前行指令执行后的值

复制代码
   # 起始 $sp = 0xffffc90000013ed8, $bp = 0xffffc90000013f18, *$sp = 0xffffffff8107fbdc <do_user_addr_fault+364>

   0xffffffffa0201000:	endbr64                             # 这条指令为了安全 intel CVE 要求 call/jmp 的地址处必须是endbr指令 $sp = 0xffffc90000013ed0, $bp = 0xffffc90000013f18, *sp = 0xffffffff812e3889 (call 跳板时候的压入返回地址)
ftrace_regs_caller:
   0xffffffffa0201004:	pushf                               # 压栈flags, 如nmi中断位置在这里 $sp = 0xffffc90000013ec8, 后面保存寄存器时候会用到 *0xffffc90000013ec8 = 0x0000000000000306
save_mcount_regs:                                           # 从这里开始 保存21个寄存器 save_mcount_regs 8
#ifdef CONFIG_FRAME_POINTER
   0xffffffffa0201005:	push   rbp                          # 保存rbp 这次保存因为call fentry发生在kprobe的函数未建立栈帧时刻,先替kprobe的函数保存原来的栈帧 $bp = 0xffffc900004b3f18, $sp = 0xffffc90000013ec0, *$sp=$bp
   0xffffffffa0201006:	push   QWORD PTR [rsp+0x18]         # 保存call probe的返回地址 $sp = 0xffffc90000013eb8,  *$sp=0xffffffff8107fbdc   0xffffffffa020100a:	 push   rbp                          # 构建probe函数的栈帧 为了栈回溯时候能看到probe函数 $sp = 0xffffc90000013eb0
   0xffffffffa020100b:	mov    rbp,rsp                      # 创建栈帧 $bp = 0xffffc90000013eb0
   0xffffffffa020100e:	push   QWORD PTR [rsp+0x20]         # 保存当前跳板的函数的返回地址, 构建跳板的栈帧, 模拟在kprobe函数中call跳板 $sp = 0xffffc90000013ea8, *$sp=0xffffffff812e3889
   0xffffffffa0201012:	push   rbp                          # $sp = 0xffffc90000013ea0,  *$sp=0xffffc90000013eb0 
   0xffffffffa0201013:	mov    rbp,rsp                      # $bp = 0xffffc90000013ea0
#endif /* CONFIG_FRAME_POINTER */
   0xffffffffa0201016:	sub    rsp,0xa8                     # 保存寄存器预留大小 开始进行保存操作 $sp = 0xffffc90000013df8
   0xffffffffa020101d:	mov    QWORD PTR [rsp+0x50],rax
   0xffffffffa0201022:	mov    QWORD PTR [rsp+0x58],rcx
   0xffffffffa0201027:	mov    QWORD PTR [rsp+0x60],rdx
   0xffffffffa020102c:	mov    QWORD PTR [rsp+0x68],rsi
   0xffffffffa0201031:	mov    QWORD PTR [rsp+0x70],rdi
   0xffffffffa0201036:	mov    QWORD PTR [rsp+0x48],r8
   0xffffffffa020103b:	mov    QWORD PTR [rsp+0x40],r9
   0xffffffffa0201040:	mov    QWORD PTR [rsp+0x78],0x0     # ORIG_RAX syscall num/ret val/irq num*/ 本次不使用, arch_ftrace_set_direct_caller可能使用
#ifdef CONFIG_FRAME_POINTER
   0xffffffffa0201049:	mov    rdx,QWORD PTR [rsp+0xc8]     # $rdx = 0xffffc90000013f18, 进入时刻的rbp
#else
	                    movq   rdx, rbp
#endif
   0xffffffffa0201051:	mov    QWORD PTR [rsp+0x20],rdx     # 保存进入时刻的rbp
   0xffffffffa0201056:	mov    rsi,QWORD PTR [rsp+0xe0]     # rsp+0xe0是起始时刻的rsp, 里面存的是call kprobe的返回地址 $rsi = 0xffffffff8107fbdc
   0xffffffffa020105e:	mov    rdi,QWORD PTR [rsp+0xd8]     # rsp+0xd8是跳板进入时刻的rsp,里面存的是call 跳板的自动压栈下一ip地址,即跳板的返回地址 $rdi = 0xffffffff812e3889
   0xffffffffa0201066:	mov    QWORD PTR [rsp+0x80],rdi
   0xffffffffa020106e:	sub    rdi,0x5                      # 减去call跳板的call指令大小 $rdi = 0xffffffff812e3884
   .endm /* 结束宏 save_mcount_regs */
ftrace_regs_caller_op_ptr:
   0xffffffffa0201072:	cs nop WORD PTR [rax+rax*1+0x0]     # ftrace_regs_caller_op_ptr开始 空指令
   0xffffffffa020107c:	mov    rdx,QWORD PTR [rip+0xfc]     # movq function_trace_op(%rip), %rdx /* function_trace_op, 当前追踪的op指针位置 */
   0xffffffffa0201083:	mov    QWORD PTR [rsp],r15          # 开始保存寄存器
   0xffffffffa0201087:	mov    QWORD PTR [rsp+0x8],r14
   0xffffffffa020108c:	mov    QWORD PTR [rsp+0x10],r13
   0xffffffffa0201091:	mov    QWORD PTR [rsp+0x18],r12
   0xffffffffa0201096:	mov    QWORD PTR [rsp+0x30],r11
   0xffffffffa020109b:	mov    QWORD PTR [rsp+0x38],r10
   0xffffffffa02010a0:	mov    QWORD PTR [rsp+0x28],rbx
   0xffffffffa02010a5:	mov    rcx,QWORD PTR [rsp+0xd0]     # 再次保存ftrace_regs_caller时刻保存的flags寄存器 rcx = 0x306
   0xffffffffa02010ad:	mov    QWORD PTR [rsp+0x90],rcx     # movq %rcx, EFLAGS(%rsp)
   0xffffffffa02010b5:	mov    rcx,0x18                     # movq $__KERNEL_DS, %rcx
   0xffffffffa02010bc:	mov    QWORD PTR [rsp+0xa0],rcx     # movq %rcx, SS(%rsp)
   0xffffffffa02010c4:	mov    rcx,0x10                     # movq $__KERNEL_CS, %rcx
   0xffffffffa02010cb:	mov    QWORD PTR [rsp+0x88],rcx     # movq %rcx, CS(%rsp)
   0xffffffffa02010d3:	lea    rcx,[rsp+0xe0]               # 保存起始时刻的rsp, leaq MCOUNT_REG_SIZE+8*2(%rsp), %rcx, $rcx = 0xffffc90000013ed8
   0xffffffffa02010db:	mov    QWORD PTR [rsp+0x98],rcx     # 到这里截止,21个寄存器保存完整,movq %rcx, RSP(%rsp)
   0xffffffffa02010e3:	lea    rbp,[rsp+0x1]                # /* 内核栈回溯代码里有一个硬编码约定:如果 rbp 最低位是 1:表示 rbp 不是真的栈帧,而是一个指向 pt_regs 的编码指针 */
   0xffffffffa02010e8:	lea    rcx,[rsp]                    # 把当前栈顶地址(也就是指向保存好的所有寄存器结构体的指针),放入 % rcx 中,作为调用 C 语言 ftrace 处理函数的第 4 个参数
   0xffffffffa02010ec:	cs nop WORD PTR [rax+rax*1+0x0]
   0xffffffffa02010f6:	call   0xffffffff81075e70 <kprobe_ftrace_handler> # 跳转到具体功能
   0xffffffffa02010fb:	mov    rax,QWORD PTR [rsp+0x90]     # 往后是恢复寄存器 返回
   0xffffffffa0201103:	mov    QWORD PTR [rsp+0xd0],rax
   0xffffffffa020110b:	mov    rax,QWORD PTR [rsp+0x80]
   0xffffffffa0201113:	mov    QWORD PTR [rsp+0xd8],rax
   0xffffffffa020111b:	mov    r15,QWORD PTR [rsp]
   0xffffffffa020111f:	mov    r14,QWORD PTR [rsp+0x8]
   0xffffffffa0201124:	mov    r13,QWORD PTR [rsp+0x10]
   0xffffffffa0201129:	mov    r12,QWORD PTR [rsp+0x18]
   0xffffffffa020112e:	mov    r10,QWORD PTR [rsp+0x38]
   0xffffffffa0201133:	mov    rbx,QWORD PTR [rsp+0x28]
   0xffffffffa0201138:	mov    rax,QWORD PTR [rsp+0x78]
   0xffffffffa020113d:	mov    QWORD PTR [rsp+0xc8],rax
   0xffffffffa0201145:	test   rax,rax
   0xffffffffa0201148:	xchg   ax,ax                        # 无意义, create_trampoline替换了,不保存寄存器时为跳过恢复寄存器几条指令
   0xffffffffa020114a:	mov    rbp,QWORD PTR [rsp+0x20]
   0xffffffffa020114f:	mov    r9,QWORD PTR [rsp+0x40]
   0xffffffffa0201154:	mov    r8,QWORD PTR [rsp+0x48]
   0xffffffffa0201159:	mov    rdi,QWORD PTR [rsp+0x70]
   0xffffffffa020115e:	mov    rsi,QWORD PTR [rsp+0x68]
   0xffffffffa0201163:	mov    rdx,QWORD PTR [rsp+0x60]
   0xffffffffa0201168:	mov    rcx,QWORD PTR [rsp+0x58]
   0xffffffffa020116d:	mov    rax,QWORD PTR [rsp+0x50]
   0xffffffffa0201172:	add    rsp,0xd0                    # 一次性把跳板进入时刻的两层栈帧销毁
   0xffffffffa0201179:	popf                               # flags 寄存器恢复 
   0xffffffffa020117a:	ret
   0xffffffffa020117b:	int3                               # 防止ftrace异常gard

对应的ftrace_64.S注释:

复制代码
/root/qemu/linux-6.6.58/arch/x86/kernel/ftrace_64.S

/* Size of stack used to save mcount regs in save_mcount_regs 168(21个寄存器中断等场景需要保存) + 40(8 + 2 * 16(当前和父的rsp, rbp)) */
#define MCOUNT_REG_SIZE		(FRAME_SIZE + MCOUNT_FRAME_SIZE)

/*
 * @added: the amount of stack added before calling this
 *
 * After this is called, the following registers contain:
 *
 *  %rdi - holds the address that called the trampoline <- 返回的时候这三个寄存器保存了一些值
 *  %rsi - holds the parent function (traced function's return address)
 *  %rdx - holds the original %rbp
 */
.macro save_mcount_regs added=0 /* 这个宏会被跳板入口处调用 接受一个参数added表示已经压栈的大小 */

#ifdef CONFIG_FRAME_POINTER /* 使用栈帧方式,fentry时候还没有建立栈帧 需要恢复bp sp寄存器, push rax, call fentry,$sp=rax */
	/* Save the original rbp */
	pushq %rbp

	/* Save the parent pointer (skip orig rbp and our return address) */
	pushq \added+8*2(%rsp) /* 这和前一行都在保存寄存器, push   QWORD PTR [rsp+0x18] 2 * 8 即59行的rbp上面的call probe的函数push的返回地址, 后面保存状态时候会用到,130行还会用added一次性销毁这两次创建的栈帧 */
	pushq %rbp
	movq %rsp, %rbp /* 构建probe函数的栈帧 为了栈回溯时候能看到probe函数 */
	/* Save the return address (now skip orig rbp, rbp and parent) */
	pushq \added+8*3(%rsp) /* 构建跳板的栈帧 */
	pushq %rbp
	movq %rsp, %rbp
#endif /* CONFIG_FRAME_POINTER */

	/*
	 * We add enough stack to save all regs.
	 */
	subq $(FRAME_SIZE), %rsp /* sub    rsp,0xa8 21个寄存器空间 */
	movq %rax, RAX(%rsp)
	movq %rcx, RCX(%rsp)
	movq %rdx, RDX(%rsp)
	movq %rsi, RSI(%rsp)
	movq %rdi, RDI(%rsp)
	movq %r8, R8(%rsp)
	movq %r9, R9(%rsp)
	movq $0, ORIG_RAX(%rsp) /* 到这里保存了8个寄存器 可能是syscall num/ret val/irq num*/
	/*
	 * Save the original RBP. Even though the mcount ABI does not
	 * require this, it helps out callers.
	 */
#ifdef CONFIG_FRAME_POINTER
	movq MCOUNT_REG_SIZE-8(%rsp), %rdx /* 保存进入时刻的rbp, mov    rdx,QWORD PTR [rsp+0xc8] */
#else
	movq %rbp, %rdx
#endif
	movq %rdx, RBP(%rsp) # 保存最开始进入跳板时刻的rbp

	/* Copy the parent address into %rsi (second parameter) */
	movq MCOUNT_REG_SIZE+8+\added(%rsp), %rsi /* rsp+0xe0是起始时刻的rsp, 里面存的是call kprobe的返回地址 mov    rsi,QWORD PTR [rsp+0xe0] */

	 /* Move RIP to its proper location */
	movq MCOUNT_REG_SIZE+\added(%rsp), %rdi /* rsp+0xd8是跳板进入时刻的rsp,里面存的是call 跳板的自动压栈下一ip地址,即跳板的返回地址, rdi,QWORD PTR [rsp+0xd8] */
	movq %rdi, RIP(%rsp) /* 保存原来的rip值,同同时也是跳板的返回地址 QWORD PTR [rsp+0x80],rdi */

	/*
	 * Now %rdi (the first parameter) has the return address of
	 * where ftrace_call returns. But the callbacks expect the
	 * address of the call itself.
	 */
	subq $MCOUNT_INSN_SIZE, %rdi /* 减去call跳板本身命令的大小 callback 需要call跳板的地址 */
	.endm /* 到这里保存了10个寄存器 结束宏 save_mcount_regs */

.macro restore_mcount_regs save=0

	/* ftrace_regs_caller or frame pointers require this */
	movq RBP(%rsp), %rbp

	movq R9(%rsp), %r9
	movq R8(%rsp), %r8
	movq RDI(%rsp), %rdi
	movq RSI(%rsp), %rsi
	movq RDX(%rsp), %rdx
	movq RCX(%rsp), %rcx
	movq RAX(%rsp), %rax

	addq $MCOUNT_REG_SIZE-\save, %rsp /* 一次性把跳板进入时刻的两层栈帧销毁 */

	.endm

SYM_FUNC_START(ftrace_regs_caller) /* 这里开始是保存reg的跳板start */
	/* Save the current flags before any operations that can change them */
	pushfq /* 压栈 flags, 如nmi中断位置在这里 223行要保存 */

	/* added 8 bytes to save flags */
	save_mcount_regs 8 /* 本文件开头的函数 先压栈8个寄存器 */
	/* save_mcount_regs fills in first two parameters */

	CALL_DEPTH_ACCOUNT /* 和一个安全漏洞有关 默认没启用 */

SYM_INNER_LABEL(ftrace_regs_caller_op_ptr, SYM_L_GLOBAL)
	ANNOTATE_NOENDBR /* 空指令 */
	/* Load the ftrace_ops into the 3rd parameter */
	movq function_trace_op(%rip), %rdx /* function_trace_op, 当前追踪的op指针位置,mov    rdx,QWORD PTR [rip+0xfc] */

	/* Save the rest of pt_regs */
	movq %r15, R15(%rsp)
	movq %r14, R14(%rsp)
	movq %r13, R13(%rsp)
	movq %r12, R12(%rsp)
	movq %r11, R11(%rsp)
	movq %r10, R10(%rsp)
	movq %rbx, RBX(%rsp)
	/* Copy saved flags */
	movq MCOUNT_REG_SIZE(%rsp), %rcx /* 再次保存201行时刻保存的flags寄存器, mov    rcx,QWORD PTR [rsp+0xd0] */
	movq %rcx, EFLAGS(%rsp)
	/* Kernel segments */
	movq $__KERNEL_DS, %rcx
	movq %rcx, SS(%rsp)
	movq $__KERNEL_CS, %rcx
	movq %rcx, CS(%rsp)
	/* Stack - skipping return address and flags */
	leaq MCOUNT_REG_SIZE+8*2(%rsp), %rcx /* 保存起始时刻的rsp, lea    rcx,[rsp+0xe0]*/
	movq %rcx, RSP(%rsp) /* 到这里保存21个寄存器完成 */

	ENCODE_FRAME_POINTER /* 内核栈回溯代码里有一个硬编码约定:如果 rbp 最低位是 1:表示 rbp 不是真的栈帧,而是一个指向 pt_regs 的编码指针 */

	/* regs go into 4th parameter */
	leaq (%rsp), %rcx   /* 把当前栈顶地址(也就是指向保存好的所有寄存器结构体的指针),放入 % rcx 中,作为调用 C 语言 ftrace 处理函数的第 4 个参数 */

	/* Account for the function call below */
	CALL_DEPTH_ACCOUNT  /* 条件, 为 Intel Skylake 系列 CPU 做 Retbleed 漏洞的调用深度跟踪 */

SYM_INNER_LABEL(ftrace_regs_call, SYM_L_GLOBAL)
	ANNOTATE_NOENDBR    /* nothing */
	call ftrace_stub    /* 跳转到具体功能 */

	/* Copy flags back to SS, to restore them */
	movq EFLAGS(%rsp), %rax
	movq %rax, MCOUNT_REG_SIZE(%rsp)

	/* Handlers can change the RIP */
	movq RIP(%rsp), %rax
	movq %rax, MCOUNT_REG_SIZE+8(%rsp)

	/* restore the rest of pt_regs */
	movq R15(%rsp), %r15
	movq R14(%rsp), %r14
	movq R13(%rsp), %r13
	movq R12(%rsp), %r12
	movq R10(%rsp), %r10
	movq RBX(%rsp), %rbx

	movq ORIG_RAX(%rsp), %rax
	movq %rax, MCOUNT_REG_SIZE-8(%rsp)

	/*
	 * If ORIG_RAX is anything but zero, make this a call to that.
	 * See arch_ftrace_set_direct_caller().
	 */
	testq	%rax, %rax
SYM_INNER_LABEL(ftrace_regs_caller_jmp, SYM_L_GLOBAL)
	ANNOTATE_NOENDBR
	jnz	1f /* 往前跳一行,即跳过恢复寄存器 需要保存寄存器时create_trampoline会替换这个指令为无意义的指令 */

	restore_mcount_regs
	/* Restore flags */
	popfq

	/*
	 * The trampoline will add the return.
	 */
SYM_INNER_LABEL(ftrace_regs_caller_end, SYM_L_GLOBAL) /* 跳板结束位置 */
	ANNOTATE_NOENDBR
	RET

总结

函数handle_mm_fault被kprobe挂钩后,入口nop被替换为call指令跳转到ftrace跳板。跳板先pushfq保存标志,再手动构建两层栈帧保证栈回溯正常,随后sub rsp, 0xa8保存21个寄存器构成完整pt_regs结构,并设置好C函数调用参数。接着调用kprobe_ftrace_handler执行探针逻辑。返回后从栈中恢复所有寄存器,通过add rsp, 0xd0一次性销毁整个栈帧,最后popfq恢复标志位并ret返回原函数。

相关推荐
HLC++17 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室18 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy21 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕1 天前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫1 天前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生1 天前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.111 天前
linux基础IO
linux·运维·服务器
BelongPanda1 天前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯1 天前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_777998341 天前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器