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返回原函数。

相关推荐
桌面运维家2 小时前
VHD/VHDX 数据守护:BAT位图校验与修复
linux·服务器·网络
pupudawang2 小时前
Linux下安装Nginx服务及systemctl方式管理nginx详情
linux·运维·nginx
零K沁雪2 小时前
Linux 内核遍历宏介绍
linux·内核
淼淼爱喝水3 小时前
openEuler 下 Ansible 基础命令详解与实操演示2
linux·运维·windows
拾贰_C3 小时前
【Ubuntu | install | 安装软件】 Ubuntu软件安装多种方式以及卸载
linux·运维·ubuntu
·醉挽清风·4 小时前
学习笔记—Linux—信号阻塞&信号捕捉
linux·笔记·学习
杨云龙UP4 小时前
Linux生产环境下Oracle RMAN 备份、核查、清理与验证常用命令整理_20260330
linux·运维·服务器·数据库·oracle
A.A呐4 小时前
【Linux第二十二章】https
linux·https
齐齐大魔王5 小时前
linux-线程编程
java·linux·服务器