本文作者為團隊小伙伴阿松,在Linux文件監控領域實戰經驗豐富。本次引入eBPF在文件監控上應用,提升文件變更的關聯進程信息等。在實現過程中,分享了eBPF kbproe時,被插樁函數超多參數獲取的解決方案。
本文內容,如非特殊說明,均基于 4.18 內核,x86-64 CPU 架構。
插樁的程序類型選擇
說起 eBPF 大家都不陌生,就內核而言,hook 會盡可能選在 tracepoint,如果沒有 tracepoint,會考慮使用 kprobe。
tracepoint 的范圍有限,而內核函數又太多,基于各種需求場景,kprobe 的出場機會較多;但需要注意的,并不是所有的內核函數都可以選作 hook 點,inline 函數無法被 hook,static 函數也有可能被優化掉;如果想知道究竟有哪些函數可以選做 hook 點,在 Linux 機器上,可以通過less /proc/kallsyms查看。
使用 eBPF 時,內核代碼 kprobe 的書寫范例如下:
?
SEC("kprobe/vfs_write") int?kprobe_vfs_write(struct?pt_regs?*regs) { ????struct?file?*file ????file?=?(struct?file?*)PT_REGS_PARM1(regs); ????//?... }
?
其中 pt_regs 的結構體如下:
?
struct?pt_regs?{ /* ?*?C?ABI?says?these?regs?are?callee-preserved.?They?aren't?saved?on?kernel?entry ?*?unless?syscall?needs?a?complete,?fully?filled?"struct?pt_regs". ?*/ ?unsigned?long?r15; ?unsigned?long?r14; ?unsigned?long?r13; ?unsigned?long?r12; ?unsigned?long?bp; ?unsigned?long?bx; /*?These?regs?are?callee-clobbered.?Always?saved?on?kernel?entry.?*/ ?unsigned?long?r11; ?unsigned?long?r10; ?unsigned?long?r9; ?unsigned?long?r8; ?unsigned?long?ax; ?unsigned?long?cx; ?unsigned?long?dx; ?unsigned?long?si; ?unsigned?long?di; /* ?*?On?syscall?entry,?this?is?syscall#.?On?CPU?exception,?this?is?error?code. ?*?On?hw?interrupt,?it's?IRQ?number: ?*/ ?unsigned?long?orig_ax; /*?Return?frame?for?iretq?*/ ?unsigned?long?ip; ?unsigned?long?cs; ?unsigned?long?flags; ?unsigned?long?sp; ?unsigned?long?ss; /*?top?of?stack?page?*/ };
?
通常來說,我們要獲取的參數,均可通過諸如 PT_REGS_PARM1 這樣的宏來拿到,宏定義如下:
?
#define?PT_REGS_PARM1(x)?((x)->di) #define?PT_REGS_PARM2(x)?((x)->si) #define?PT_REGS_PARM3(x)?((x)->dx) #define?PT_REGS_PARM4(x)?((x)->cx) #define?PT_REGS_PARM5(x)?((x)->r8)
?
可以看到,上述的宏只能獲取 5 個參數;但是在最近的一個項目中,就遇到了如何獲取超過 5 個參數的難題,這也是本文的由來,如果你也有類似的困惑,本文也許是為你準備的。
如何獲取插樁函數中第 6 個參數
上述的 5 個宏已經可以覆蓋大多數的獲取小于 5 個參數的需求,不知道大家有沒有想過,使用 eBPF 時如果獲取的參數個數大于 5 個怎么辦呢?
如下的內核函數__get_user_pages(幸運的是,該 static 函數并未被優化掉):
?
static?long?__get_user_pages(struct?task_struct?*tsk,?struct?mm_struct?*mm, ??unsigned?long?start,?unsigned?long?nr_pages, ??unsigned?int?gup_flags,?struct?page?**pages, ??struct?vm_area_struct?**vmas,?int?*nonblocking)
?
在希望對這個函數進行 hook 的時候犯了難,該函數總共有 8 個參數,如果想拿到最后 3 個參數,該如何操作呢?
且看 BCC 是如何操作的。
BCC 代碼中明確表明:只支持寄存器參數。那什么是寄存器參數呢?其實就是內核函數調用約定中的前 6 個參數要通過寄存器傳遞,只支持這前六個寄存器參數。
?
constexpr?int?MAX_CALLING_CONV_REGS?=?6; const?char?*calling_conv_regs_x86[]?=?{ ??"di",?"si",?"dx",?"cx",?"r8",?"r9" }; bool?BTypeVisitor::VisitFunctionDecl(FunctionDecl?*D)?{ ????if?(D->param_size()?>?MAX_CALLING_CONV_REGS?+?1)?{ ??????error(GET_BEGINLOC(D->getParamDecl(MAX_CALLING_CONV_REGS?+?1)), ????????????"too?many?arguments,?bcc?only?supports?in-register?parameters"); ??????return?false; ????} }
?
BCC 中使用如下的代碼對用戶寫的BPF text進行rewrite,覆蓋的參數剛好是前 6 個參數,分別保存于di, si, dx, cx, r8, r9寄存器:
?
const?char?*calling_conv_regs_x86[]?=?{ ??"di",?"si",?"dx",?"cx",?"r8",?"r9" }; void?BTypeVisitor::genParamDirectAssign(FunctionDecl?*D,?string&?preamble, ????????????????????????????????????????const?char?**calling_conv_regs)?{ ??for?(size_t?idx?=?0;?idx?=?1)?{ ??????//?Move?the?args?into?a?preamble?section?where?the?same?params?are ??????//?declared?and?initialized?from?pt_regs. ??????//?Todo:?this?init?should?be?done?only?when?the?program?requests?it. ??????string?text?=?rewriter_.getRewrittenText(expansionRange(arg->getSourceRange())); ??????arg->addAttr(UnavailableAttr::CreateImplicit(C,?"ptregs")); ??????size_t?d?=?idx?-?1; ??????const?char?*reg?=?calling_conv_regs[d]; ??????preamble?+=?"?"?+?text?+?"?=?("?+?arg->getType().getAsString()?+?")"?+ ??????????????????fn_args_[0]->getName().str()?+?"->"?+?string(reg)?+?";"; ????} ??} }
?
看到這里,大家應該明白,之所以能使用 BCC 提供的如此簡便的 python 接口(內核函數前面加上前綴 kprobe__,第一個參數永遠是struct pt_regs *,然后需要使用幾個內核參數就填寫幾個)來做一些監控工作,是因為 BCC 在幕后做了大量的 rewirte 工作,respect!
?
int?kprobe__tcp_v4_connect(struct?pt_regs?*ctx,?struct?sock?*sk)?{ ????[...] }
?
之前總是由于 eBPF 給的限制(按照 eBPF 的 calling convention,只有 5 個參數可以傳遞),以為更多的參數是無法獲取的。實際上可以回憶下,實際上按照 amd64 的調用約定,最多是可以通過寄存器傳遞 6 個參數的。
這么看下來,獲取第 6 個參數的方案其實也是很簡單,手動添加如下的宏即可:
?
#define?PT_REGS_PARM6(x)?((x)->r9)
?
插樁函數超過 6 個參數怎么辦
amd64 的調用約定同樣規定了,超過 6 個的參數,都會在棧上傳遞,具體可以參考regs_get_kernel_argument
那么如果參數超過 6 個,處理方案呼之欲出:從棧上獲取。
regs_get_kernel_argument該函數在新版本的內核中才有,實現如下:
?
static?inline?unsigned?long?regs_get_kernel_argument(struct?pt_regs?*regs, ???????????unsigned?int?n) { ?static?const?unsigned?int?argument_offs[]?=?{ #ifdef?__i386__ ??offsetof(struct?pt_regs,?ax), ??offsetof(struct?pt_regs,?dx), ??offsetof(struct?pt_regs,?cx), #define?NR_REG_ARGUMENTS?3 #else ??offsetof(struct?pt_regs,?di), ??offsetof(struct?pt_regs,?si), ??offsetof(struct?pt_regs,?dx), ??offsetof(struct?pt_regs,?cx), ??offsetof(struct?pt_regs,?r8), ??offsetof(struct?pt_regs,?r9), #define?NR_REG_ARGUMENTS?6 #endif ?}; ?if?(n?>=?NR_REG_ARGUMENTS)?{ ??n?-=?NR_REG_ARGUMENTS?-?1; ??return?regs_get_kernel_stack_nth(regs,?n); ?}?else ??return?regs_get_register(regs,?argument_offs[n]); }
?
從上述的代碼可以看到,常用的前 6 個參數,確實是在寄存器中獲取,分別是di, si, dx, cx, r8, r9,這也印證了我們之前的想法,且和 BCC 中的行為是一致的。
從regs_get_kernel_argument中也可以看到,從第 7 個參數開始,便開始從棧上獲取了,關鍵函數為:regs_get_kernel_stack_nth,這個函數在 4.18 內核中也有,如下:
?
static?inline?unsigned?long?regs_get_kernel_stack_nth(struct?pt_regs?*regs,?unsigned?int?n) { ?unsigned?long?*addr?=?(unsigned?long?*)kernel_stack_pointer(regs); ?addr?+=?n; ?if?(regs_within_kernel_stack(regs,?(unsigned?long)addr)) ??return?*addr; ?else ??return?0; } //?等價于bpf提供的幫助宏?#define?PT_REGS_SP(x)?((x)->sp) static?inline?unsigned?long?kernel_stack_pointer(struct?pt_regs?*regs) { ?return?regs->sp; }
?
regs_get_kernel_stack_nth是標準的棧上操作獲取,只不過內核提供了一些地址合法性的檢查,不考慮這些的話,在 eBPF 中其實可以一步到位;使用如下函數,便能返回棧上的第 n 個參數(從 1 開始)。
?
static?__always_inline?unsigned?long?regs_get_kernel_stack_nth(struct?pt_regs?*regs, ????????????unsigned?int?n) { ?unsigned?long?*addr; ??unsigned?long?val; ?addr?=?(unsigned?long?*)PT_REGS_SP(x)?+?n; ?if?(addr)?{ ?????bpf_probe_read(&val,?sizeof(val),?addr); ?????return?val; ?} ?return?0; }
?
捎帶提一句,在 amd64 中,eBPF calling ABI 使用了 R1-R5 來傳遞參數,且做了如下的寄存器映射約定,方便 jit 轉換為 native code,提高效率。
?
R0?–?rax??????return?value?from?function R1?–?rdi??????1st?argument R2?–?rsi??????2nd?argument R3?–?rdx??????3rd?argument R4?–?rcx??????4th?argument R5?–?r8???????5th?argument R6?–?rbx??????callee?saved R7?-?r13??????callee?saved R8?-?r14??????callee?saved R9?-?r15??????callee?saved R10?–?rbp?????frame?pointer
?
而 R0 - R10,是 bpf 虛擬機的內部的特殊標識符(函數調用等地方使用),如果 jit 可用,bpf code 會被翻譯為native code。
Linux Amd64 調用約定
demo 驗證
那 Amd64 的 ABI 是如何操作的呢?可以使用如下的代碼進行驗證:
?
#?cat?myfunc.c int?utilfunc(int?a,?int?b,?int?c) { ????int?xx?=?a?+?2; ????int?yy?=?b?+?3; ????int?zz?=?c?+?4; ????int?sum?=?xx?+?yy?+?zz; ????return?xx?*?yy?*?zz?+?sum; } int?myfunc(int?a,?int?b,?int?c,?int?d, ????????????int?e,?int?f,?int?g,?int?h) { ????int?xx?=?(a?+?b)?*?c?*?d?*?e?*?(f?+?(g?*?h)); ????int?zz?=?utilfunc(xx,?2,?xx?%?2); ????return?zz?+?20; } int?main()?{ ????myfunc(1,?2,?3,?4,?5,?6,?7,?8); ????return?0; }
?
gcc -c -g myfunc.c進行編譯匯編得到 myfunc.o
eBPF 字節碼反匯編
objdump -S myfunc.o反匯編,查看調用約定是不是和我們從教科書上看到的一致
先看 main 函數,可以簡單地得出如下結論:
超過 6 個參數的函數調用,需要用到棧傳遞
前 6 個參數,分別使用 di、si、dx、cx、r8、r9
使用棧傳遞的參數,是從右向左壓棧,此例中先壓入 8,再壓入 7
?
00000000000000c4?: int?main()?{ ??c4:?f3?0f?1e?fa???????????endbr64 ??c8:?55????????????????????push???%rbp ??c9:?48?89?e5??????????????mov????%rsp,%rbp ????myfunc(1,?2,?3,?4,?5,?6,?7,?8); ??cc:?6a?08?????????????????push???$0x8????#棧上傳遞參數 ??ce:?6a?07?????????????????push???$0x7????#棧上傳遞參數 ??d0:?41?b9?06?00?00?00?????mov????$0x6,%r9d?#如下是寄存器傳遞參數 ??d6:?41?b8?05?00?00?00?????mov????$0x5,%r8d ??dc:?b9?04?00?00?00????????mov????$0x4,%ecx ??e1:?ba?03?00?00?00????????mov????$0x3,%edx ??e6:?be?02?00?00?00????????mov????$0x2,%esi ??eb:?bf?01?00?00?00????????mov????$0x1,%edi?#第1個參數,寄存器傳遞 ??f0:?e8?00?00?00?00????????call???f5? ??f5:?48?83?c4?10???????????add????$0x10,%rsp ????return?0; ??f9:?b8?00?00?00?00????????mov????$0x0,%eax } ??fe:?c9????????????????????leave ??ff:?c3????????????????????ret
?
用戶空間程序調用
再看被 main 調用的 myfunc 函數的反匯編:
和 main 函數的調用參數排列一致,參數1-6是寄存器傳遞,參數7-8是棧上傳遞
?
int?myfunc(int?a,?int?b,?int?c,?int?d, ????????????int?e,?int?f,?int?g,?int?h) { ??50:?f3?0f?1e?fa???????????endbr64 ??54:?55????????????????????push???%rbp ??55:?48?89?e5??????????????mov????%rsp,%rbp ??58:?48?83?ec?28???????????sub????$0x28,%rsp ??5c:?89?7d?ec??????????????mov????%edi,-0x14(%rbp)?#第1個參數,從edi中復制到棧上 ??5f:?89?75?e8??????????????mov????%esi,-0x18(%rbp) ??62:?89?55?e4??????????????mov????%edx,-0x1c(%rbp) ??65:?89?4d?e0??????????????mov????%ecx,-0x20(%rbp) ??68:?44?89?45?dc???????????mov????%r8d,-0x24(%rbp) ??6c:?44?89?4d?d8???????????mov????%r9d,-0x28(%rbp)?#第6個參數 ????int?xx?=?(a?+?b)?*?c?*?d?*?e?*?(f?+?(g?*?h)); ??70:?8b?55?ec??????????????mov????-0x14(%rbp),%edx ??73:?8b?45?e8??????????????mov????-0x18(%rbp),%eax ??76:?01?d0?????????????????add????%edx,%eax??????#?a+b ??78:?0f?af?45?e4???????????imul???-0x1c(%rbp),%eax?#(a+b)?*?c ??7c:?0f?af?45?e0???????????imul???-0x20(%rbp),%eax?#(a+b)?*?c?*?d ??80:?0f?af?45?dc???????????imul???-0x24(%rbp),%eax?#(a+b)?*?c?*?d?*?e ??84:?89?c2?????????????????mov????%eax,%edx ??86:?8b?45?10??????????????mov????0x10(%rbp),%eax??#棧上第1個參數?g ??89:?0f?af?45?18???????????imul???0x18(%rbp),%eax??#?g*h ??8d:?89?c1?????????????????mov????%eax,%ecx ??8f:?8b?45?d8??????????????mov????-0x28(%rbp),%eax?#?參數f ??92:?01?c8?????????????????add????%ecx,%eax????????#?(g*h)?+?f ??94:?0f?af?c2??????????????imul???%edx,%eax????????#?((g*h)?+?f)?*?(a+b)?*?c?*?d?*?e ??97:?89?45?f8??????????????mov????%eax,-0x8(%rbp)
?
寄存器堆棧狀態
main 函數調用 myfunc,做完 prolog 操作后,棧和寄存器的狀態如下:
main-myfunc
實戰 kprobe 獲取 6 個以上參數
說了那么多,到底是不是符合預期呢?嘗試使用 BCC 驗證下,為了方便驗證,換了一個比較容易從用戶態驗證的 hook 點:inotify_handle_event
如果在 BCC 中使用了超過 6 個的參數,則會報錯,比如函數 kprobe__inotify_handle_event 的原型如下:
?
int?kprobe__inotify_handle_event(struct?pt_regs?*ctx,?struct?fsnotify_group?*group, ?????????????????????????struct?inode?*inode, ?????????????????????????u32?mask,?const?void?*data,?int?data_type, ?????????????????????????const?unsigned?char?*file_name,?u32?cookie, ?????????????????????????struct?fsnotify_iter_info?*iter_info)
?
當在 BCC 中做超過 6 個參數的獲取時,得到如下錯誤:
?
error:?too?many?arguments,?bcc?only?supports?in-register?parameters
?
如果只使用前 6 個寄存器的參數,如下代碼即可:
?
#!/usr/bin/python from?bcc?import?BPF #?load?BPF?program b?=?BPF(text=""" #include?int?kprobe__inotify_handle_event(struct?pt_regs?*ctx,?struct?fsnotify_group?*group, ?????????????????????????struct?inode?*inode, ?????????????????????????u32?mask,?const?void?*data,?int?data_type, ?????????????????????????const?unsigned?char?*file_name) { ??char?comm[128]; ??int?pid?=?bpf_get_current_pid_tgid()?>>?32; ??bpf_get_current_comm(comm,?sizeof(comm)); ??bpf_trace_printk("pid?is:%d,?comm?is:?%s\n",?pid,?comm); ??bpf_trace_printk("file?is:?%s\n",?file_name); ??return?0; } """) b.trace_print()
?
但是我們可以使用如下的方式,拿到剩下的參數(以 cookie 為例):
?
??unsigned?long?cookie; ??bpf_probe_read(&cookie,?8,?(unsigned?long*)PT_REGS_SP(ctx)?+?1);
?
完整代碼如下:
?
#!/usr/bin/python from?bcc?import?BPF #?load?BPF?program b?=?BPF(text=""" #include?int?kprobe__inotify_handle_event(struct?pt_regs?*ctx,?struct?fsnotify_group?*group, ?????????????????????????struct?inode?*inode, ?????????????????????????u32?mask,?const?void?*data,?int?data_type, ?????????????????????????const?unsigned?char?*file_name) { ??char?comm[128]; ??unsigned?long?cookie; ??int?pid?=?bpf_get_current_pid_tgid()?>>?32; ??bpf_probe_read(&cookie,?8,?(unsigned?long*)PT_REGS_SP(ctx)?+?1); ??bpf_get_current_comm(comm,?sizeof(comm)); ??bpf_trace_printk("pid?is:%d,?comm?is:?%s\n",?pid,?comm); ??bpf_trace_printk("cookie?is?%d,?file?is:?%s\n",?cookie,?file_name); ??return?0; } """) b.trace_print()
?
shell 1 運行 BCC 代碼
?
./get-stack-arg.py
?
shell 2 使用 inotify-tools 驗證
?
[root@rmed?~]#?inotifywait?-m?./
?
shell 3 做如下的操作
?
[root@rmed?~]#?mv?testFileA?testFileB
?
shell 1 如下輸出
shell 1 output
shell 2 如下輸出
shell 2 output
為了保持嚴謹性,可以使用https://man7.org/linux/man-pages/man7/inotify.7.html[1] 中的代碼進行驗證,
主要是做了如下改動,增加對IN_MOVED_FROM | IN_MOVED_TO的監控:
?
diff?--git?a/inotify.c?b/inotify.c index?08fa55a..7116a9a?100644 ---?a/inotify.c +++?b/inotify.c @@?-61,6?+61,10?@@ ????????????????????if?(event->mask?&?IN_CLOSE_WRITE) ????????????????????????printf("IN_CLOSE_WRITE:?"); +???????????????????if?(event->mask?&?IN_MOVED_FROM) +???????????????????????printf("IN_MOVED_FROM:?"); +???????????????????if?(event->mask?&?IN_MOVED_TO) +???????????????????????printf("IN_MOVED_TO:?"); ????????????????????/*?Print?the?name?of?the?watched?directory.?*/ ????????????????????for?(int?i?=?1;?i?len) ????????????????????????printf("%s",?event->name); +???????????????????if?(event->cookie) +???????????????????????printf("cookie:?%d",?event->cookie); ????????????????????/*?Print?type?of?filesystem?object.?*/ ????????????????????if?(event->mask?&?IN_ISDIR) @@?-123,7?+129,7?@@ ????????????for?(i?=?1;?i??
同樣的,使用 BCC 和自己編譯的 inotify 工具驗證。
BCC 輸出:
ebpf-bcc-inotify
inotify 輸出:
ebpf-inotify
輸出符合預期,剩下的第 8 個參數,大家可自行修改代碼驗證。
祝大家玩得開心。
審核編輯:湯梓紅
?
評論