介绍

由于需要在macOS下实现Intel PT的缘故,我用空余时间开发了一款比较稳定的基于DTrace的Hook框架,用来监控线程的创建和线程上下文切换。整个实现只用加载一个KEXT,核心代码仅寥寥几行。这里介绍了其实现的原理和方法。由于和公司业务相关,详细代码不会公开,只分享大体实现思路。

DTrace内部实现

反汇编一个函数

objdump --disassemble-functions=_thread_start -x86-asm-syntax=intel /System/Library/Kernels/kernel

FBT Hook - 简介

FBT全称为Function Boundary Tracing,FBT Hook只需替换一个字节即可实现Hook 函数的Entry/Return,简介如下

ffffff8000360b50 _thread_start:
ffffff8000360b50: 55                   	push	rbp
ffffff8000360b51: 48 89 e5             	mov	rbp, rsp ;; 第一字节patched为0xf0
ffffff8000360b54: 53                   	push	rbx
ffffff8000360b55: 50                   	push	rax
ffffff8000360b56: 48 89 fb             	mov	rbx, rdi
ffffff8000360b59: 31 f6                	xor	esi, esi
ffffff8000360b5b: e8 80 3e fe ff       	call	-115072 <_clear_wait>
ffffff8000360b60: 80 8b 73 03 00 00 02 	or	byte ptr [rbx + 883], 2
ffffff8000360b67: 48 83 c4 08          	add	rsp, 8
ffffff8000360b6b: 5b                   	pop	rbx
ffffff8000360b6c: 5d                   	pop	rbp ;; 或者leave, 第一字节patch为0xf0
ffffff8000360b6d: c3                   	ret
ffffff8000360b6e: 66 90                	nop

SDT Hook - Explained

找到一个SDT(Statically Defined Tracing)定义的探针过程如下

$ nm -S /System/Library/Kernels/kernel  | grep dtrace_probe | grep sched
ffffff8000c06158 0000000000000000 D __dtrace_probe$43767___sched____wakeup
ffffff8000c06160 0000000000000000 D __dtrace_probe$44715___sched____iwakeup
ffffff8000c06168 0000000000000000 D __dtrace_probe$45853___sched____sleep
ffffff8000c06170 0000000000000000 D __dtrace_probe$462491___sched____off__cpu
ffffff8000c06178 0000000000000000 D __dtrace_probe$472531___sched____on__cpu
ffffff8000c06180 0000000000000000 D __dtrace_probe$482330___sched____off__cpu
ffffff8000c06188 0000000000000000 D __dtrace_probe$492350___sched____on__cpu
ffffff8000c06190 0000000000000000 D __dtrace_probe$503149___sched____on__cpu

这些探针指向了三个连续的nop指令,其中DTrace SDT会把第一个字节Patch为0xf0

ffffff80003450b0 _thread_continue:
ffffff80003450b0: 55                   	push	rbp
ffffff80003450b1: 48 89 e5             	mov	rbp, rsp
ffffff80003450b4: 41 57                	push	r15
ffffff80003450b6: 41 56                	push	r14
ffffff80003450b8: 41 55                	push	r13
ffffff80003450ba: 41 54                	push	r12
ffffff80003450bc: 53                   	push	rbx
ffffff80003450bd: 50                   	push	rax
ffffff80003450be: 49 89 fc             	mov	r12, rdi
ffffff80003450c1: 65 48 8b 1c 25 08 00 00 00   	mov	rbx, qword ptr gs:[8]
ffffff80003450ca: 90                   	nop ;; 如你所见,3字节的nop,将被patch为0xf0
ffffff80003450cb: 90                   	nop
ffffff80003450cc: 90                   	nop
ffffff80003450cd: 4c 8d 6b 70          	lea	r13, [rbx + 112]
ffffff80003450d1: 4c 8b 73 70          	mov	r14, qword ptr [rbx + 112]
ffffff80003450d5: 4c 8b 7b 78          	mov	r15, qword ptr [rbx + 120]
ffffff80003450d9: 83 3d 20 26 98 00 00 	cmp	dword ptr [rip + 9971232], 0
ffffff80003450e0: 74 0d                	je	13 <_thread_continue+0x3f>
ffffff80003450e2: 48 89 df             	mov	rdi, rbx
ffffff80003450e5: 4c 89 f6             	mov	rsi, r14
ffffff80003450e8: 31 d2                	xor	edx, edx
ffffff80003450ea: e8 01 8e 0c 00       	call	822785 <_kperf_on_cpu_internal>
ffffff80003450ef: 4c 89 e7             	mov	rdi, r12
ffffff80003450f2: 48 89 de             	mov	rsi, rbx
ffffff80003450f5: e8 46 e7 ff ff       	call	-6330 <_thread_dispatch>

异常指令处理

0xf0 lock 异常指令被执行后, kernel_trap 就会被调用,最终的调用链如下

kernel_trap; // kernel trap handler
=> perfCallback tempDTraceTrapHook; //a function pointer
     => fbt_perfCallback; //points to me
          => dtrace_invop; //call next chain
               dtrace_invop_hdlr; //a linked list

dtrace_invop_hdlr链表可以通过如下方式来增加和删除。

void dtrace_invop_add(int (*func)(uintptr_t, uintptr_t *, uintptr_t));
void dtrace_invop_remove(int (*func)(uintptr_t, uintptr_t *, uintptr_t));

实现

核心代码

int my_handler(uintptr_t addr, uintptr_t *saved_state, uintptr_t eax){
    /*...more...*/
    return DTRACE_INVOP_NOP; //let dtrace emulate it
    /*...more...*/
}

/*...more...*/

    //make dtrace hook work
    dtrace_invop_add(my_handler);

    OSCompareAndSwap64(*(UInt64*)tempDTraceTrapHook, (UInt64)fbt_perfCallback , (UInt64*)tempDTraceTrapHook);
    if(*tempDTraceTrapHook != fbt_perfCallback){
        IOLog("%s : failed to enable dhook, tempDTraceTrapHook already occupied\n",__func__);
        return false;
    }
    uint8_t patchval = 0xf0;
    ml_nofault_copy((vm_offset_t)&patchval, patchaddr ,1);
/*...more...*/

剩余部分以及如何监控线程创建和上下文切换,请自行参考XNU内核中DTrace部分,核心的probe分别是sched::cpu::onsched::cpu::off以及proc::lwp::create