Linux内核调试技术kprobe使用与实现Word格式.docx
《Linux内核调试技术kprobe使用与实现Word格式.docx》由会员分享,可在线阅读,更多相关《Linux内核调试技术kprobe使用与实现Word格式.docx(85页珍藏版)》请在冰豆网上搜索。
![Linux内核调试技术kprobe使用与实现Word格式.docx](https://file1.bdocx.com/fileroot1/2023-2/3/2a624b4e-be9d-416c-93d6-442ae4dc6681/2a624b4e-be9d-416c-93d6-442ae4dc66811.gif)
6、在kprobes的注册和注销过程中不会使用mutex锁和动态的申请内存;
7、kprobes回调函数的运行期间是关闭内核抢占的,同时也可能在关闭中断的情况下执行,具体要视CPU架构而定。
因此不论在何种情况下,在回调函数中不要调用会放弃CPU的函数(如信号量、mutex锁等);
8、kretprobe通过替换返回地址为预定义的trampoline的地址来实现,因此栈回溯和gcc内嵌函数__builtin_return_address()调用将返回trampoline的地址而不是真正的被探测函数的返回地址;
9、如果一个函数的调用此处和返回次数不相等,则在类似这样的函数上注册kretprobe将可能不会达到预期的效果,例如do_exit()函数会存在问题,而do_execve()函数和do_fork()函数不会;
10、如果当在进入和退出一个函数时,CPU运行在非当前任务所有的栈上,那么往该函数上注册kretprobe可能会导致不可预料的后果,因此,kprobes不支持在X86_64的结构下为__switch_to()函数注册kretprobe,将直接返回-EINVAL。
二、kprobe原理
下面来介绍一下kprobe是如何工作的。
具体流程见下图:
1、当用户注册一个探测点后,kprobe首先备份被探测点的对应指令,然后将原始指令的入口点替换为断点指令,该指令是CPU架构相关的,如i386和x86_64是int3,arm是设置一个未定义指令(目前的x86_64架构支持一种跳转优化方案JumpOptimization,内核需开启CONFIG_OPTPROBES选项,该种方案使用跳转指令来代替断点指令);
2、当CPU流程执行到探测点的断点指令时,就触发了一个trap,在trap处理流程中会保存当前CPU的寄存器信息并调用对应的trap处理函数,该处理函数会设置kprobe的调用状态并调用用户注册的pre_handler回调函数,kprobe会向该函数传递注册的structkprobe结构地址以及保存的CPU寄存器信息;
3、随后kprobe单步执行前面所拷贝的被探测指令,具体执行方式各个架构不尽相同,arm会在异常处理流程中使用模拟函数执行,而x86_64架构则会设置单步调试flag并回到异常触发前的流程中执行;
4、在单步执行完成后,kprobe执行用户注册的post_handler回调函数;
5、最后,执行流程回到被探测指令之后的正常流程继续执行。
三、kprobe使用实例
在分析kprobe的实现之前先来看一下如何利用kprobe对函数进行探测,以便于让我们对kprobre所完成功能有一个比较清晰的认识。
目前,使用kprobe可以通过两种方式,第一种是开发人员自行编写内核模块,向内核注册探测点,探测函数可根据需要自行定制,使用灵活方便;
第二种方式是使用kprobesonftrace,这种方式是kprobe和ftrace结合使用,即可以通过kprobe来优化ftrace来跟踪函数的调用。
下面来分别介绍:
1、编写kprobe探测模块
内核提供了一个structkprobe结构体以及一系列的内核API函数接口,用户可以通过这些接口自行实现探测回调函数并实现structkprobe结构,然后将它注册到内核的kprobes子系统中来达到探测的目的。
同时在内核的samples/kprobes目录下有一个例程kprobe_example.c描述了kprobe模块最简单的编写方式,开发者可以以此为模板编写自己的探测模块。
1.1、kprobe结构体与API介绍
structkprobe结构体定义如下:
[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片
structkprobe{
structhlist_nodehlist;
/*listofkprobesformulti-handlersupport*/
structlist_headlist;
/*countthenumberoftimesthisprobewastemporarilydisarmed*/
unsignedlongnmissed;
/*locationoftheprobepoint*/
kprobe_opcode_t*addr;
/*Allowusertoindicatesymbolnameoftheprobepoint*/
constchar*symbol_name;
/*Offsetintothesymbol*/
unsignedintoffset;
/*Calledbeforeaddrisexecuted.*/
kprobe_pre_handler_tpre_handler;
/*Calledafteraddrisexecuted,unless...*/
kprobe_post_handler_tpost_handler;
/*
*...calledifexecutingaddrcausesafault(eg.pagefault).
*Return1ifithandledfault,otherwisekernelwillseeit.
*/
kprobe_fault_handler_tfault_handler;
*...calledifbreakpointtrapoccursinprobehandler.
*Return1ifithandledbreak,otherwisekernelwillseeit.
kprobe_break_handler_tbreak_handler;
/*Savedopcode(whichhasbeenreplacedwithbreakpoint)*/
kprobe_opcode_topcode;
/*copyoftheoriginalinstruction*/
structarch_specific_insnainsn;
*Indicatesvariousstatusflags.
*Protectedbykprobe_mutexafterthiskprobeisregistered.
u32flags;
};
其中各个字段的含义如下:
structhlist_nodehlist:
被用于kprobe全局hash,索引值为被探测点的地址;
structlist_headlist:
用于链接同一被探测点的不同探测kprobe;
kprobe_opcode_t*addr:
被探测点的地址;
constchar*symbol_name:
被探测函数的名字;
unsignedintoffset:
被探测点在函数内部的偏移,用于探测函数内部的指令,如果该值为0表示函数的入口;
kprobe_pre_handler_tpre_handler:
在被探测点指令执行之前调用的回调函数;
kprobe_post_handler_tpost_handler:
在被探测指令执行之后调用的回调函数;
kprobe_fault_handler_tfault_handler:
在执行pre_handler、post_handler或单步执行被探测指令时出现内存异常则会调用该回调函数;
kprobe_break_handler_tbreak_handler:
在执行某一kprobe过程中触发了断点指令后会调用该函数,用于实现jprobe;
kprobe_opcode_topcode:
保存的被探测点原始指令;
structarch_specific_insnainsn:
被复制的被探测点的原始指令,用于单步执行,架构强相关(可能包含指令模拟函数);
u32flags:
状态标记。
涉及的API函数接口如下:
intregister_kprobe(structkprobe*kp)//向内核注册kprobe探测点
voidunregister_kprobe(structkprobe*kp)//卸载kprobe探测点
intregister_kprobes(structkprobe**kps,intnum)//注册探测函数向量,包含多个探测点
voidunregister_kprobes(structkprobe**kps,intnum)//卸载探测函数向量,包含多个探测点
intdisable_kprobe(structkprobe*kp)//临时暂停指定探测点的探测
intenable_kprobe(structkprobe*kp)//恢复指定探测点的探测
1.2、用例kprobe_example.c分析与演示
该用例函数非常简单,它实现了内核函数do_fork的探测,该函数会在fork系统调用或者内核kernel_thread函数创建进程时被调用,触发也十分的频繁。
下面来分析一下用例代码:
/*Foreachprobeyouneedtoallocateakprobestructure*/
staticstructkprobekp={
.symbol_name="
do_fork"
staticint__initkprobe_init(void)
{
intret;
kp.pre_handler=handler_pre;
kp.post_handler=handler_post;
kp.fault_handler=handler_fault;
ret=register_kprobe(&
kp);
if(ret<
0){
printk(KERN_INFO"
register_kprobefailed,returned%d\n"
ret);
returnret;
}
Plantedkprobeat%p\n"
kp.addr);
return0;
}
staticvoid__exitkprobe_exit(void)
unregister_kprobe(&
kprobeat%punregistered\n"
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("
GPL"
);
程序中定义了一个structkprobe结构实例kp并初始化其中的symbol_name字段为“do_fork”,表明它将要探测do_fork函数。
在模块的初始化函数中,注册了
pre_handler、post_handler和fault_handler这3个回调函数分别为handler_pre、handler_post和handler_fault,最后调用register_kprobe注册。
在模块的卸载函数中调用unregister_kprobe函数卸载kp探测点。
staticinthandler_pre(structkprobe*p,structpt_regs*regs)
#ifdefCONFIG_X86
pre_handler:
p->
addr=0x%p,ip=%lx,"
"
flags=0x%lx\n"
addr,regs->
ip,regs->
flags);
#endif
#ifdefCONFIG_PPC
addr=0x%p,nip=0x%lx,"
msr=0x%lx\n"
nip,regs->
msr);
#ifdefCONFIG_MIPS
addr=0x%p,epc=0x%lx,"
status=0x%lx\n"
cp0_epc,regs->
cp0_status);
#ifdefCONFIG_TILEGX
addr=0x%p,pc=0x%lx,"
ex1=0x%lx\n"
pc,regs->
ex1);
/*Adump_stack()herewillgiveastackbacktrace*/
handler_pre回调函数的第一个入参是注册的structkprobe探测实例,第二个参数是保存的触发断点前的寄存器状态,它在do_fork函数被调用之前被调用,该函数仅仅是打印了被探测点的地址,保存的个别寄存器参数。
由于受CPU架构影响,这里对不同的架构进行了宏区分(虽然没有实现arm架构的,但是支持的,可以自行添加);
/*kprobepost_handler:
calledaftertheprobedinstructionisexecuted*/
staticvoidhandler_post(structkprobe*p,structpt_regs*regs,
unsignedlongflags)
post_handler:
addr=0x%p,flags=0x%lx\n"
addr=0x%p,msr=0x%lx\n"
addr=0x%p,status=0x%lx\n"
addr=0x%p,ex1=0x%lx\n"
handler_post回调函数的前两个入参同handler_pre,第三个参数目前尚未使用,全部为0;
该函数在do_fork函数调用之后被调用,这里打印的内容同handler_pre类似。
/*
*fault_handler:
thisiscalledifanexceptionisgeneratedforany
*instructionwithinthepre-orpost-handler,orwhenKprobes
*single-stepstheprobedinstruction.
staticinthandler_fault(structkprobe*p,structpt_regs*regs,inttrapnr)
fault_handler:
addr=0x%p,trap#%dn"
addr,trapnr);
/*Return0becausewedon'
thandlethefault.*/
handler_fault回调函数会在执行handler_pre、handler_post或单步执行do_fork时出现错误时调用,这里第三个参数时具体发生错误的trapnumber,与架构相关,例如i386的pagefault为14。
下面将它编译成模块在我的x86(CentOS3.10)环境下进行演示,首先确保架构和内核已经支持kprobes,开启以下选项(一般都是默认开启的):
Symbol:
KPROBES[=y]
Type:
boolean
Prompt:
Kprobes
Location:
(3)->
Generalsetup
Definedatarch/Kconfig:
37
Dependson:
MODULES[=y]&
&
HAVE_KPROBES[=y]
Selects:
KALLSYMS[=y]
174
Selectedby:
X86[=y]
然后使用以下Makefile单独编译kprobe_example.ko模块:
obj-m:
=kprobe_example.o
CROSS_COMPILE='
'
KDIR:
=/lib/modules/$(shelluname-r)/build
all:
make-C$(KDIR)M=$(PWD)modules
clean:
rm-f*.ko*.o*.mod.o*.mod.c.*.cmd*.symversmodul*
加载到内核中后,随便在终端上敲一个命令,可以看到dmesg中打印如下信息:
<
6>
addr=0xc0439cc0,ip=c0439cc1,flags=0x246
addr=0xc0439cc0,flags=0x246
addr=0xc0439cc0,ip=c0439cc1,flags=