文章目录
前言
本篇环境开启了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返回原函数。