一、講座內(nèi)容簡(jiǎn)要描述
本次講座內(nèi)容分為兩部分:
當(dāng)初學(xué)者接觸到Linux平臺(tái)的tracing系統(tǒng)時(shí),常常被各種詞語(yǔ)弄得暈頭轉(zhuǎn)向:比如 Kprobe,Tracepoint,Linux Auditing subsystem(auditd),systemtap,LTTng,perf,trace-cmd,eBPF,bpftrace,BCC等等。初學(xué)者往往會(huì)有以下疑問(wèn):這些專業(yè)詞語(yǔ)是什么意思?它們之間有什么關(guān)系?每種tracing技術(shù)的優(yōu)缺點(diǎn)是什么?應(yīng)該選擇哪種技術(shù)?為什么eBPF從中脫穎而出,近年來(lái)得到廣泛關(guān)注?
本次講座嘗試從統(tǒng)一的視角來(lái)梳理和對(duì)比這些技術(shù)的異同點(diǎn),并嘗試回答這些問(wèn)題。
2)eBPF開(kāi)發(fā)經(jīng)驗(yàn)分享
eBPF目前正在高速發(fā)展,很多坑和解決辦法缺乏官方文檔。本次講座主要介紹主講人在eBPF開(kāi)發(fā)實(shí)踐中經(jīng)常遇到的問(wèn)題,包括開(kāi)發(fā)框架的選擇,多內(nèi)核版本兼容性問(wèn)題,如何為低版本內(nèi)核生成BTF文件,eBPF驗(yàn)證機(jī)制與編譯器優(yōu)化機(jī)制的不一致問(wèn)題,eBPF在ARM架構(gòu)遇到的問(wèn)題等等。
二、Linux Tracing System淺析
對(duì)于Linux Tracing System尤其是目前最火的eBPF技術(shù)來(lái)說(shuō),主要是通過(guò)探針技術(shù),實(shí)現(xiàn)特定事件的追蹤和采樣,達(dá)到增強(qiáng)內(nèi)核行為可觀測(cè)性、優(yōu)化系統(tǒng)性能、動(dòng)態(tài)監(jiān)測(cè)網(wǎng)絡(luò)和加固系統(tǒng)安全的目的。如下圖所示,我將Linux Tracing System細(xì)分為三個(gè)維度,包括1)數(shù)據(jù)源(內(nèi)核態(tài)),負(fù)責(zé)提供數(shù)據(jù)地來(lái)源;2)Tracing框架(內(nèi)核態(tài)),負(fù)責(zé)對(duì)接數(shù)據(jù)源,采集解析發(fā)送數(shù)據(jù),并對(duì)用戶態(tài)提供接口;3)以及前端工具/庫(kù)(用戶態(tài)),對(duì)接Tracing內(nèi)核框架,直接與用戶交互,負(fù)責(zé)采集配置和數(shù)據(jù)分析。
下面將從這三個(gè)維度自下而上地對(duì)Linux Tracing System進(jìn)行梳理和分析。
1.1.數(shù)據(jù)源(內(nèi)核態(tài))介紹
如下圖所示,從數(shù)據(jù)提供方的角度來(lái)看,數(shù)據(jù)源可以分成硬件探針、軟件探針(又分為動(dòng)態(tài)探針以及靜態(tài)探針),也就是獲取底層數(shù)據(jù)源的方式和手段。顧名思義,硬件探針技術(shù)就是通過(guò)在硬件設(shè)備上(比如芯片)插入探針,捕獲硬件層次行為;而軟件探針技術(shù)則是通過(guò)軟件的方式插入探針,捕獲軟件層次的行為。這些探針技術(shù)負(fù)責(zé)提供數(shù)據(jù),上層的Tracing工具和框架則基于這些探針技術(shù)來(lái)采集數(shù)據(jù),并對(duì)數(shù)據(jù)進(jìn)一步整理、分析、和展現(xiàn)給用戶。
硬件探針
HPC: Hardware Performance Counter是CPU硬件提供的一種常見(jiàn)的數(shù)據(jù)源,如下圖所示,它能夠監(jiān)控CPU級(jí)別的事件,比如執(zhí)行的 指令數(shù),跳轉(zhuǎn)指令數(shù),Cache Miss等等,被廣泛應(yīng)用于性能調(diào)試(Vtune, Perf)、攻擊檢測(cè)等等
1)HPC事件列表
2)HPC數(shù)據(jù)案例
對(duì)于此類硬件數(shù)據(jù),我們通常使用用戶態(tài)工具perf來(lái)進(jìn)行采集,下圖展示了一個(gè)具體的案例。
LBR: Last Branch RecordCPU硬件提供的另一種特性,它能夠記錄每條分支(跳轉(zhuǎn))指令的源地址和目的地址?;贚BR硬件特性,可實(shí)現(xiàn)調(diào)用棧信息的記錄
在系統(tǒng)性能優(yōu)化領(lǐng)域以及調(diào)試程序時(shí)經(jīng)常使用的性能分析利器:火焰圖(Flame Graph)也可以基于LBR的數(shù)據(jù)生成,使用命令perf record -F 99 -a --call-graph lbr即可得到完整直觀的火焰圖數(shù)據(jù)。
火焰圖示例
軟件探針(靜態(tài)探針):
靜態(tài)內(nèi)核探針指的是在內(nèi)核運(yùn)行之前,在內(nèi)核源代碼或者二進(jìn)制中插入預(yù)先設(shè)置好的鉤子函數(shù),內(nèi)核運(yùn)行時(shí)觸發(fā)生效的探針?lè)桨浮?/p>
Tracepoint:Tracepoint是一種典型的靜態(tài)探針。它通過(guò)在內(nèi)核源代碼中插入預(yù)先定義的靜態(tài)鉤子函數(shù)來(lái)實(shí)現(xiàn)內(nèi)核行為的監(jiān)控。簡(jiǎn)單地來(lái)看,大家可以把Tracepoint的原理等同于調(diào)試程序時(shí)加入的printf函數(shù)。
下圖展示了2012年,內(nèi)核引入sched_process_execTracepoint時(shí)的commit,可以看到,首先用TRACE_EVENT宏定義了新增Tracepoint的名字和參數(shù)等信息,然后在內(nèi)核函數(shù)exec_binprm的源代碼中加入了鉤子函數(shù)trace_sched_process_exec。每當(dāng)程序執(zhí)行二進(jìn)制時(shí),都會(huì)觸發(fā)exec_binprm函數(shù),繼而觸發(fā)trace_sched_process_exec鉤子函數(shù)。Tracing工具和框架將自定義的函數(shù)掛載到該鉤子函數(shù)上,來(lái)采集程序執(zhí)行行為日志。
靜態(tài)探針的優(yōu)點(diǎn):
穩(wěn)定(內(nèi)核開(kāi)發(fā)者會(huì)負(fù)責(zé)維護(hù)該函數(shù)的穩(wěn)定性)
性能好
靜態(tài)探針的缺點(diǎn)
需要修改內(nèi)核代碼來(lái)添加新的靜態(tài)探針
內(nèi)核支持的靜態(tài)探針數(shù)量有限
軟件探針(動(dòng)態(tài)探針):
有了靜態(tài)探針,為什么還需要?jiǎng)討B(tài)探針呢?主要原因是靜態(tài)探針都是人工添加的,支持的數(shù)量有限,而動(dòng)態(tài)探針就是為了解決這個(gè)問(wèn)題,它能夠支持Hook幾乎所有的內(nèi)核函數(shù)。
Kprobes:Kprobe是一個(gè)典型的動(dòng)態(tài)探針,如下圖所示,在內(nèi)核運(yùn)行時(shí),Kprobe技術(shù)將需要監(jiān)控的內(nèi)核函數(shù)的指令動(dòng)態(tài)替換,使得該函數(shù)的控制流跳轉(zhuǎn)到用戶自定義的處理函數(shù)上。當(dāng)內(nèi)核執(zhí)行到該監(jiān)控函數(shù)時(shí),相應(yīng)的用戶自定義處理函數(shù)被執(zhí)行,然后繼續(xù)執(zhí)行正常的代碼路徑。
動(dòng)態(tài)探針的優(yōu)點(diǎn):
可以Hook幾乎所有的內(nèi)核函數(shù)
動(dòng)態(tài)探針的缺點(diǎn)
不穩(wěn)定(函數(shù)的變更、編譯器的優(yōu)化等都可能導(dǎo)致采集程序的失效)
性能相對(duì)較差
軟件探針(動(dòng)/靜態(tài)探針):
靜態(tài)探針性能好,但支持的數(shù)量有限,動(dòng)態(tài)探針支持的數(shù)量多,但不穩(wěn)定、性能相對(duì)較差,那么是否存在一種技術(shù),能同時(shí)兼顧靜態(tài)和動(dòng)態(tài)的優(yōu)勢(shì)呢?答案是動(dòng)靜態(tài)結(jié)合的探針?lè)桨浮?/p>
Function Hooks(Ftrace): Function Hooks是Ftrace引入的一種動(dòng)靜態(tài)結(jié)合的探針方案。如下圖所示,靜態(tài)指的是它通過(guò)gcc編譯器,在內(nèi)核編譯階段,在內(nèi)核函數(shù)的入口處插入了預(yù)留的特定指令,當(dāng)內(nèi)核運(yùn)行時(shí),它會(huì)將預(yù)留的特定指令替換為跳轉(zhuǎn)指令(callftrace_caller),使得內(nèi)核函數(shù)的控制流跳轉(zhuǎn)到用戶自定義函數(shù)上,達(dá)到數(shù)據(jù)監(jiān)控的目的。
Ftrace和Function Tracer
Function Hooks(Ftrace)的優(yōu)點(diǎn)
相比于Tracepoint和Kprobe,F(xiàn)unction Hooks最顯著的功能性特點(diǎn)是它能夠方便地監(jiān)控內(nèi)核函數(shù)的調(diào)用關(guān)系,如下圖所示,監(jiān)控了內(nèi)核函數(shù)exec_binprm的所有子函數(shù)調(diào)用關(guān)系。
上面分析完各種動(dòng)態(tài)和靜態(tài)探針的方案和優(yōu)缺點(diǎn)后,從開(kāi)發(fā)者代碼多功能可控的角度出發(fā),建議優(yōu)先使用靜態(tài)探針?lè)桨浮?/p>
1.2.Linux Tracing System 發(fā)展歷程
? 2004年4月,Linux Auditing subsystem(auditd)被引入內(nèi)核2.6.6-rc1
? 2005年4月,Kprobe被引入內(nèi)核2.6.11.7
? 2006年,LTTng發(fā)布(至今沒(méi)有合入內(nèi)核)
? 2008年10月 ,Kernel Tracepoint 被引入內(nèi)核(v2.6.28)。
? 2008年,F(xiàn)trace被引入內(nèi)核(包括compile time function hooks)。
? 2009年,perf被引入內(nèi)核
? 2009年,SystemTap發(fā)布(至今沒(méi)有合入內(nèi)核)
? 2014年,Alexei Starovoitov將eBPF引入內(nèi)核
1.3.Linux Tracing 框架方案對(duì)比
eBPF的優(yōu)勢(shì)對(duì)比:
穩(wěn)定:通過(guò)驗(yàn)證器,防止用戶編寫(xiě)的程序?qū)е聝?nèi)核崩潰
免安裝:eBPF內(nèi)置于linux內(nèi)核,無(wú)需安裝額外以來(lái)
內(nèi)核編程:支持開(kāi)發(fā)者插入自定義的代碼邏輯(包括數(shù)據(jù)采集、分析和過(guò)濾)到內(nèi)核中運(yùn)行
2.eBPF框架開(kāi)發(fā)分析
2.1 eBPF基礎(chǔ)架構(gòu)
eBPF程序分為兩部分: 用戶態(tài)和內(nèi)核態(tài)代碼。
eBPF內(nèi)核代碼:
這個(gè)代碼首先需要經(jīng)過(guò)編譯器(比如LLVM)編譯成eBPF字節(jié)碼,然后字節(jié)碼會(huì)被加載到內(nèi)核執(zhí)行。所以 這部分代碼理論上用什么語(yǔ)言編寫(xiě)都可以,只要編譯器支持將該語(yǔ)言編譯為eBPF字節(jié)碼即可。
目前絕大多數(shù)工具都是用的C語(yǔ)言來(lái)編寫(xiě)eBPF內(nèi)核代碼,包括BCC。
bpftrace提供了一種易用的腳本語(yǔ)言來(lái)幫助用戶快速高效的使用eBPF功能,其背后的原理還是利用LLVM 將腳本轉(zhuǎn)為eBPF字節(jié)碼。
eBPF用戶態(tài)代碼:
這部分代碼負(fù)責(zé)將eBPF內(nèi)核程序加載到內(nèi)核,與eBPF MAP交互,以及接收eBPF內(nèi)核程序發(fā)送出來(lái)的數(shù)據(jù)。這個(gè)功能的本質(zhì)上是通過(guò)Linux OS提供的syscall(bpf syscall + perf_event_open syscall)完成的,因此這 部分代碼你可以用任何語(yǔ)言實(shí)現(xiàn)。比如BCC使用python,libbpf使用c或者c++,TRACEE使用Go等等。
2.2 eBPF數(shù)據(jù)源
性能分析大師Brendan Gregg(Intel Fellow)總結(jié)的Linux BPF Tracing Tools上展示了豐富多彩的eBPF鉤子類型,這些鉤子類型提供了可以加載BPF程序的范圍。
fentry/fexit
Tracepoints
network devices (tc/xdp)
network routes
TCP congestion algorithms
sockets (data level)
kernel functions (kprobes)
userspace functions (uprobes)
system calls
2.3 eBPF框架的發(fā)展歷程
2014年9月 引入了bpf() syscall,將eBPF引入用戶態(tài)空間。
自帶迷你libbpf庫(kù),簡(jiǎn)單對(duì)bpf()進(jìn)行了封裝,功能是將eBPF字節(jié)碼加載到內(nèi)核。
2015年2月份 Kernel 3.19 引入bpf_load.c/h文件,對(duì)上述迷你libbpf庫(kù)再進(jìn)行封裝,功能是將eBPF elf二進(jìn)制文件加載到內(nèi)核(目前已過(guò)時(shí),不建議使用)。
2015年4月 BCC項(xiàng)目創(chuàng)建,提供了eBPF一站式編程。
1.創(chuàng)建之初,基于上述迷你libbpf庫(kù)來(lái)加載eBPF字節(jié)碼。
2. 提供了Python接口。
2015年11月 Kernel 4.3 引入標(biāo)準(zhǔn)庫(kù) libbpf
1. 該標(biāo)準(zhǔn)庫(kù)由Huawei 2012 OS內(nèi)核實(shí)驗(yàn)室的王楠提交。
2018年 為解決BCC的缺陷,CO-RE(Compile Once, Run Everywhere)的想法被提出并實(shí)現(xiàn),最后達(dá)成共識(shí):libbpf + BTF + CO-RE代表了eBPF的未來(lái),BCC底層實(shí)現(xiàn)逐步轉(zhuǎn)向libbpf。
2.4 eBPF可移植性痛點(diǎn)和解決方案
技術(shù)痛點(diǎn):
在內(nèi)核版本A上編譯的eBPF程序,無(wú)法直接在另外一個(gè)內(nèi)核版本B上運(yùn)行。造成可以執(zhí)行差的根本原因在于eBPF程序訪問(wèn)的內(nèi)核數(shù)據(jù)結(jié)構(gòu)(內(nèi)存空間)是不穩(wěn)定的,經(jīng)常隨內(nèi)核版本更迭而變化。
目前使用BCC的方案通過(guò)在部署機(jī)器上動(dòng)態(tài)編譯eBPF源代碼可以來(lái)解決移植性問(wèn)題。每一次eBPF程序運(yùn)行都需要進(jìn)行一次編譯,而且需要在部署機(jī)器上按照上百兆大小的依賴,如編譯器和頭文件Clang/LLVM + Linux headers等。同時(shí)在Clang/LLVM編譯過(guò)程中需要消耗大量的資源(CPU/內(nèi)存),對(duì)業(yè)務(wù)性能也會(huì)造成很大影響。
解決方案(CO-RE Compile Once,Run Everywhere):
1)BTF:將內(nèi)核數(shù)據(jù)結(jié)構(gòu)信息高效壓縮和存儲(chǔ)(相比于DWARF,可達(dá)到超過(guò)100倍的 壓縮比)
2)LLVM/Clang編譯器:編譯eBPF代碼的時(shí)候記錄下relocation相關(guān)的信息
3)Libbpf:基于BTF和編譯器提供的信息,動(dòng)態(tài)relocate數(shù)據(jù)結(jié)構(gòu)
其中BTF為重要組成部分,Linux Kernel 5.2及以上版本自帶BTF文件,低版本需要手動(dòng)移植。
通過(guò)分析內(nèi)核源碼,可以發(fā)現(xiàn)BTF文件的生成并不需要改動(dòng)內(nèi)核,只依賴:
帶有debug info的vmlinux image
pahole
LLVM
這意味著,我們可以自己為低版本內(nèi)核生產(chǎn)BTF文件,以此讓低內(nèi)核版本支持CORE。
為低版本內(nèi)核生成BTF文件
準(zhǔn)備工作:
·安裝pahole軟件(1.16+)
·https://git.kernel.org/pub/scm/devel/pahole/pahole.git
·安裝LLVM(11+)
·獲取目標(biāo)低版本內(nèi)核的vmlinux文件(帶有debug info),文件保存在{vmlinux_file_path}
·通過(guò)源下載
·比如對(duì)于CentOS,通過(guò)yum install kernel-debuginfo可以下載vmlinux
·源碼編譯內(nèi)核,獲取vmlinux
生成BTF:
·利用pahole在vmlinux文件中生成BTF信息,執(zhí)行以下命令:
·pahole -J {vmlinux_file_path}
·將BTF信息單獨(dú)輸出到新文件{BTF_file_path},執(zhí)行以下命令:
·llvm-objcopy --only-section=.BTF --set-section-flags .BTF=alloc,readonly --strip- all {vmlinux_file_path} {BTF_file_path}
·去除非必要的符號(hào)信息,降低BTF文件的大小,得到最終的BTF文件(大小約2~3MB):
·strip -x {BTF_file_path}
2.5 eBPF程序?qū)嵗治觯ㄒ粋€(gè)Print引發(fā)的慘案)
eBPF程序會(huì)被LLVM編譯為eBPF字節(jié)碼,eBPF字節(jié)碼需要通過(guò)eBPF Verifier的(靜態(tài))驗(yàn)證后,才能真正運(yùn)行。邊界檢查是eBPF Verifier的重點(diǎn)工作,目的是為了防止eBPF程序內(nèi)存越界訪問(wèn)。接下來(lái)通過(guò)在eBPF程序中簡(jiǎn)單的增加、刪減print打印信息觸發(fā)不同原因的幾種邊界檢查異常導(dǎo)致驗(yàn)證失敗的例子,進(jìn)一步講解深層的原理。
程序?qū)嶒?yàn)環(huán)境:
1)LLVM 11
2)Linux Kernel 5.8
3)Libbpf commit @9c44c8a
邊界檢查案例:
1)內(nèi)存越界:
SEC("kprobe/do_unlinkat")int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name){ // 獲取一個(gè)數(shù)組指針array(數(shù)組MAX_SIZE為16個(gè)字節(jié)) u32 key = 0; char *array = bpf_map_lookup_elem(&array_map, &key); if (array == NULL) return 0; // 獲取當(dāng)前運(yùn)行程序的CPU編號(hào)(當(dāng)前機(jī)器的CPU有16個(gè)核) unsigned int pos = bpf_get_smp_processor_id(); // 根據(jù)下表修改數(shù)組的值 array[pos] = 1; return 0;}
上述代碼編譯運(yùn)行后,提示Verifier失敗,然后使用objdump命令來(lái)看一下具體的字節(jié)碼,通過(guò)以下字節(jié)碼程序,可以看到Verifier失敗的原因在于第14行R6寄存器(變量pos)沒(méi)有進(jìn)行邊界檢查導(dǎo)致。
Root Cause:
當(dāng)eBPF Verifier走到第14行的時(shí)候嘗試去訪問(wèn)array數(shù)組,但是此時(shí)數(shù)組的下標(biāo)pos是來(lái)自bpf_get_smp_processor_id獲取到的unsigned int 類型的動(dòng)態(tài)變量,此時(shí)Verifier無(wú)法判斷變量的具體數(shù)值,所以會(huì)保守認(rèn)為可能會(huì)達(dá)到最大值,這樣的話就會(huì)超出array數(shù)組的范圍,造成內(nèi)存越界。
0000000000000000:;intBPF_KPROBE(do_unlinkat,intdfd,structfilename*name)0:r1=0; u32 key = 0;1: *(u32 *)(r10 - 4) = r12: r2 = r103: r2 += -4; char *array = bpf_map_lookup_elem(&array_map, &key); 4:r1 = 0 ll6: call 17: r6 = r0; if (array == NULL)8: if r6 == 0 goto +6 ; unsigned int pos = bpf_get_smp_processor_id();; 9:call 8; array[pos] = 1; 10:r0 <<= 3211: r0 >>= 3212: r6 += r013: r1 = 1; array[pos] = 1;14: *(u8 *)(r6 + 0) = r1
解決方案:
添加邊界檢查代碼
if (pos < MAX_SIZE)???if?r0?>15goto+3
2)Verifier驗(yàn)證機(jī)制和編譯器優(yōu)化機(jī)制不一致導(dǎo)致邊界檢查不通過(guò)
①使用錯(cuò)誤寄存器做邊界檢查:
SEC("kprobe/do_unlinkat") int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name){ // 獲取一個(gè)數(shù)組指針array(數(shù)組MAX_SIZE為16個(gè)字節(jié)) u32 key = 0; char *array = bpf_map_lookup_elem(&array_map, &key); if (array == NULL) return 0; // 獲取當(dāng)前運(yùn)行程序的CPU編號(hào)(當(dāng)前機(jī)器的CPU有16個(gè)核) unsigned int pos = bpf_get_smp_processor_id();; // 修改數(shù)值 if (pos < MAX_SIZE){ array[pos] = 1; pos += 1; } // debug代碼,輸出一些上下文信息 bpf_printk("debug %d %d %d ", bpf_get_current_pid_tgid() >> 32, bpf_get_current_pid_tgid(), array[1]); // 修改數(shù)值 if (pos < MAX_SIZE) array[pos] = 1; return 0;}
編譯這個(gè)代碼后Verifier驗(yàn)證通過(guò),可以正常運(yùn)行。但是此時(shí)如果把bpf_printk打印信息刪掉,竟然提示Verifier驗(yàn)證失敗,原因是R0寄存器(變量pos)沒(méi)有通過(guò)邊界檢查,但是明明已經(jīng)加了邊界檢查代碼,怎么還會(huì)出現(xiàn)問(wèn)題,這么神奇!
Root Cause:
由于編譯器的優(yōu)化策略,導(dǎo)致刪減bpf_printk后編譯生成的eBPF字節(jié)碼使用寄存器r1(表示pos變量)來(lái)進(jìn)行邊界檢查,但是卻用r0+1(同樣表示pos變量)來(lái)訪問(wèn)數(shù)組array。
相比之下,從eBPF verifier的角度來(lái)看,由于在編譯過(guò)程中,r1和r0+1的關(guān)聯(lián)性丟失了,導(dǎo)致eBPF verifier無(wú)法知道pos變量已經(jīng)通過(guò)了檢查,因此錯(cuò)誤的認(rèn)為pos變量沒(méi)有進(jìn)行邊界檢查,不允許程序運(yùn)行。
②寄存器溢出或重新加載后,狀態(tài)丟失:
SEC("kprobe/do_unlinkat") int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name){ //獲取一個(gè)數(shù)組指針array(數(shù)組MAX_SIZE為16個(gè)字節(jié)) u32 key = 0; char *array = bpf_map_lookup_elem(&array_map, &key); if (array == NULL) return 0; // 獲取當(dāng)前運(yùn)行程序的CPU編號(hào)(當(dāng)前機(jī)器的CPU有16個(gè)核) unsigned long pos = bpf_get_smp_processor_id();; // 修改數(shù)值 if (pos < MAX_SIZE){ for (unsigned long i = 0; i < MAX_SIZE; i++) bpf_printk("debug %d %d %d ", bpf_get_current_pid_tgid() >> 32, bpf_get_current_pid_tgid(), array[i]); array[pos] = 1; } return 0;}
在上述邊界檢查代碼中添加一段print調(diào)試打印信息后編譯驗(yàn)證又會(huì)出現(xiàn)Verifier失敗,通過(guò)排查發(fā)現(xiàn)不是已知的兩類問(wèn)題,依然使用objdump查看添加后的字節(jié)碼信息。
Root Cause:
加入bpf_printk后通過(guò)字節(jié)碼可以看到,代碼先使用R0(表示pos變量)進(jìn)行邊界檢查。由于當(dāng)前寄存器數(shù)量不足,編譯器決定將將R0臨時(shí)保存到棧上的空間(R10-16,在eBPF字節(jié)碼中,R10存儲(chǔ)存放著 eBPF ??臻g的棧幀指針的地址),這樣R0就可以空閑出來(lái),留給其他代碼使用,我們稱這種行為為寄存器溢出(register spill)。當(dāng)真正需要使用pos變量的時(shí)候,編譯器會(huì)從棧上(R10-16)將之前保存的內(nèi)容取出來(lái)賦給R1(也表示pos變量),然后使用R1對(duì)數(shù)組array進(jìn)行訪問(wèn)。但神奇的是,當(dāng)寄存器溢出發(fā)生時(shí),pos變量的狀態(tài)丟失了,eBPF忘記了該變量曾經(jīng)進(jìn)行了邊界檢查,導(dǎo)致程序無(wú)法通過(guò)驗(yàn)證。
解決方案:
在源碼中加入 &= 操作符,引導(dǎo)編譯器生成理想的eBPF字節(jié)碼
array[pos &= MAX_SIZE - 1] = 1;
如果上述方法失效,無(wú)法引導(dǎo)編譯器,那么針對(duì)出錯(cuò)的部分源代碼人工編寫(xiě)eBPF字節(jié)碼,替代編譯器生成的字節(jié)碼
#defineSTR(s)#s#defineXSTR(s)STR(s)#defineasm_variable_bound_check(variable)({ asmvolatile( "%[tmp]&="XSTR(MAX_SIZE-1)" " :[tmp]"+&r"(variable) );})asm_check(pos);array[pos] = 1;
3.總結(jié)
本文總結(jié)了從動(dòng)靜態(tài)探針的角度梳理分析Linux Tracing System以及實(shí)例解決eBPF程序中遇到的問(wèn)題。eBPF目前正在高速發(fā)展,很多坑和解決辦法缺乏官方文檔。本文在以下幾點(diǎn)上做了自己的分析和分享,希望對(duì)大家更清晰的認(rèn)識(shí)Linux Tracing System和eBPF有所幫助。
1.自下而上的方式分析動(dòng)靜態(tài)探針
2.各種場(chǎng)景下動(dòng)靜態(tài)探針的選擇
3.BPF開(kāi)發(fā)框架的選擇
4.多內(nèi)核版本兼容性問(wèn)題
5.如何為低版本內(nèi)核生成BTF文件
6.eBPF邊界檢查問(wèn)題分析
7.eBPF Verifier驗(yàn)證機(jī)制與編譯器優(yōu)化機(jī)制不一致問(wèn)題
原文標(biāo)題:Linux Tracing System淺析和eBPF開(kāi)發(fā)經(jīng)驗(yàn)分享
文章出處:【微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
-
cpu
+關(guān)注
關(guān)注
68文章
10878瀏覽量
212166 -
接口
+關(guān)注
關(guān)注
33文章
8639瀏覽量
151385 -
Linux
+關(guān)注
關(guān)注
87文章
11319瀏覽量
209830 -
硬件
+關(guān)注
關(guān)注
11文章
3345瀏覽量
66294
原文標(biāo)題:Linux Tracing System淺析和eBPF開(kāi)發(fā)經(jīng)驗(yàn)分享
文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論