BPFLBR 源碼分析bpflbr[1] 是一個(gè)增強(qiáng)型 eBPF 工具,它利用 Intel CPU 的 Last Branch Record (LBR) 特性來(lái)追蹤 BPF 程序和內(nèi)核函數(shù)的執(zhí)行細(xì)節(jié)。它可以顯示函數(shù)調(diào)用棧、反匯編 BPF 程序的 JIT 代碼、追蹤內(nèi)核函數(shù),并能夠通過(guò) fentry/fexit 鉤子在函數(shù)入口或出口處進(jìn)行跟蹤。該工具對(duì)于理解 BPF 程序的執(zhí)行流程和內(nèi)核函數(shù)調(diào)用關(guān)系特別有幫助,同時(shí)還支持將內(nèi)核地址轉(zhuǎn)換為源代碼行號(hào)信息,方便開(kāi)發(fā)者進(jìn)行調(diào)試和性能分析。 核心流程內(nèi)核態(tài)具體實(shí)現(xiàn)前置校驗(yàn)- 1. Ring Buffer 支持
- · 檢測(cè)
BPF_MAP_TYPE_RINGBUF 是否存在
- 2. 分支追蹤支持
- · 檢測(cè)
bpf_get_branch_snapshot() 是否存在 - · 用于獲取 LBR(Last Branch Record)數(shù)據(jù)
- 3. 函數(shù)上下文獲取
- ·
bpf_get_func_ret() : 獲取函數(shù)返回值 - ·
bpf_get_func_ip() : 獲取函數(shù)指令指針
- 4. 堆棧追蹤支持
- · 檢測(cè)
bpf_get_stackid() 是否存在 - · 僅在啟用
--output-stack 選項(xiàng)時(shí)需要
檢測(cè)機(jī)制 - 1. 通過(guò) fentry 掛載到
__x64_sys_nanosleep 系統(tǒng)調(diào)用 - 2. 使用
bpf_core_enum_value_exists 檢查各個(gè)特性是否存在 - 3. 將結(jié)果存儲(chǔ)在
.bss 段中供用戶(hù)態(tài)程序讀取 - 4. 用戶(hù)態(tài)程序會(huì)驗(yàn)證所有必需特性,如果缺少任何必需特性則報(bào)錯(cuò)退出
內(nèi)核符號(hào)信息讀取基本符號(hào)信息從 /proc/kallsyms 讀取基本符號(hào)信息: - 1. 基本數(shù)據(jù)結(jié)構(gòu):
// 單個(gè)符號(hào)條目的結(jié)構(gòu) type KsymEntry struct { addr uint64 // 符號(hào)地址 name string // 符號(hào)名稱(chēng) mod string // 所屬模塊 trace bool // 是否可追蹤(T類(lèi)型) }
// 整體符號(hào)表結(jié)構(gòu) type Kallsyms struct { a2s map[uint64]*KsymEntry // 地址到符號(hào)的映射 n2s map[string]*KsymEntry // 名稱(chēng)到符號(hào)的映射 addrs []uint64 // 已排序的地址列表(用于二分查找) stext uint64 // _stext 符號(hào)地址 sysBPF uint64 // __x64_sys_bpf 符號(hào)地址 }
func NewKallsyms() (*Kallsyms, error) { // 1. 打開(kāi) /proc/kallsyms 文件 fd, err := os.Open(kallsymsFilepath) // '/proc/kallsyms' // 2. 逐行掃描處理 scanner := bufio.NewScanner(fd) for scanner.Scan() { // 3. 解析每行,格式為: '地址 類(lèi)型 符號(hào)名 [模塊名]' fields := strings.Fields(line) // 4. 只處理 t/T 類(lèi)型的符號(hào) if fields[1] == 't' || fields[1] == 'T' { // 解析地址(16進(jìn)制) entry.addr, err = strconv.ParseUint(fields[0], 16, 64) // 獲取符號(hào)名 entry.name = strings.TrimSpace(fields[2]) // 獲取可選的模塊名 if len(fields) >= 4 { entry.mod = strings.Trim(fields[3], '[]') } // 標(biāo)記是否為 T 類(lèi)型(可追蹤) entry.trace = fields[1] == 'T' // 5. 保存到映射表 ks.a2s[entry.addr] = &entry ks.n2s[entry.name] = &entry } } // 6. 構(gòu)建已排序的地址列表 ks.addrs = maps.Keys(ks.a2s) slices.Sort(ks.addrs) }
func (ks *Kallsyms) find(kaddr uintptr) (*KsymEntry, bool) { addr := uint64(kaddr) // 1. 檢查地址范圍 if addr < ks.addrs[0] || addr > ks.addrs[len(ks.addrs)-1] { return nil, false } // 2. 二分查找最接近的符號(hào) total := len(ks.addrs) i, j := 0, total for i < j { h := int(uint(i+j) >> 1) if ks.addrs[h] <= addr { if h+1 < total && ks.addrs[h+1] > addr { return ks.a2s[ks.addrs[h]], true } i = h + 1 } else { j = h } } return ks.a2s[ks.addrs[i-1]], true }
主要特點(diǎn): - 1. 只關(guān)注 t/T 類(lèi)型的符號(hào)(text 段的函數(shù)符號(hào))
- 2. 維護(hù)了雙向映射:地址到符號(hào)和符號(hào)到地址
- 3. 使用二分查找優(yōu)化符號(hào)查找性能
- 4. 特別關(guān)注兩個(gè)重要符號(hào):
- ·
__x64_sys_bpf :BPF 系統(tǒng)調(diào)用入口
獲取 vmlinux 調(diào)試信息- 1. 嘗試查找并加載 vmlinux 調(diào)試信息:
// 在常見(jiàn)位置查找 vmlinux 文件 func FindVmlinux() (string, error) { // 常見(jiàn)位置包括: locations := []string{ '/boot/vmlinux-%s', '/lib/modules/%s/vmlinux-%s', '/lib/modules/%s/build/vmlinux', '/usr/lib/debug/boot/vmlinux-%s', // ... } // ... }
- · 計(jì)算 KASLR (Kernel Address Space Layout Randomization) 偏移量
- · 創(chuàng)建 addr2line 對(duì)象用于符號(hào)解析
if err == nil { // 讀取 .text 段地址 textAddr, err := bpflbr.ReadTextAddrFromVmlinux(vmlinux) // 計(jì)算 KASLR 偏移量 kaslrOffset := textAddr - kallsyms.Stext() // 創(chuàng)建 addr2line 用于符號(hào)解析 addr2line, err = bpflbr.NewAddr2Line(vmlinux, kaslrOffset, kallsyms.SysBPF()) }
主要特點(diǎn): - · 優(yōu)先使用 /proc/kallsyms 獲取基本符號(hào)信息
- · 如果有 vmlinux 調(diào)試文件則可以獲取更詳細(xì)的調(diào)試信息
- · 考慮了 KASLR 帶來(lái)的地址隨機(jī)化影響
- · 支持 addr2line 功能進(jìn)行更精確的符號(hào)解析
addr2line 原理主要功能 addr2line是一個(gè)用于將程序地址轉(zhuǎn)換為源代碼文件名和行號(hào)的工具。這個(gè)實(shí)現(xiàn)主要用于解析ELF(Executable and Linkable Format)文件中的調(diào)試信息。 核心組件 - 1. 數(shù)據(jù)結(jié)構(gòu)
type Addr2LineEntry struct { Address uint64 // 可執(zhí)行文件中的地址或可重定位對(duì)象中段的偏移量 SoPath string // 需要轉(zhuǎn)換地址的庫(kù)文件路徑 Func string // 該地址對(duì)應(yīng)的函數(shù)名 File string // 源代碼文件名 Line uint // 源代碼行號(hào) Inline bool // 是否是內(nèi)聯(lián)函數(shù) }
初始化過(guò)程 (New函數(shù)) func New(soPath string) (*Addr2Line, error) { // 1. 打開(kāi)ELF文件 // 2. 提取符號(hào)表信息 // 3. 獲取DWARF調(diào)試信息 // 4. 構(gòu)建地址映射 }
地址查找過(guò)程 (Get函數(shù)) func (a2l *Addr2Line) Get(address uint64, doDemangle bool) (*Addr2LineEntry, error) { // 1. 使用二分查找定位地址 // 2. 解析函數(shù)名(可選擇是否進(jìn)行名稱(chēng)重整) // 3. 處理內(nèi)聯(lián)函數(shù)情況 // 4. 獲取源代碼位置信息 }
Get 函數(shù)用于根據(jù)給定的地址返回對(duì)應(yīng)的源代碼位置信息。
func (a2l *Addr2Line) Get(address uint64, doDemangle bool) (*Addr2LineEntry, error) { // 使用二分查找找到小于給定地址的最大地址 idx := sort.Search(len(a2l.addrs), func(idx int) bool { if a2l.addrs[idx] > address { return true } return false }) - 1
if idx < 0 { return nil, fmt.Errorf('index is invalid\n') }
addr := a2l.addrs[idx] var functionName string if doDemangle { // 處理C++名稱(chēng)重整 name := a2l.symbols[addr].Name // 處理動(dòng)態(tài)鏈接器前綴 if strings.HasPrefix(name, '__dl__Z') { name = name[5:] } // 嘗試解析重整后的名稱(chēng) functionName, err = demangle.ToString(name) if errors.Is(err, demangle.ErrNotMangledName) { functionName = a2l.symbols[addr].Name } } else { functionName = a2l.symbols[addr].Name }
- 3. 內(nèi)聯(lián)函數(shù)檢測(cè)
dwarfEntry := a2l.dwarf.Reader() e, err := dwarfEntry.SeekPC(address) if err != nil { return nil, err }
// 遍歷DWARF條目查找內(nèi)聯(lián)函數(shù) for { entry, err := dwarfEntry.Next() if err != nil { continue }
if entry == nil || entry.Tag == dwarf.TagCompileUnit { break }
// 檢查地址范圍 rng, err := a2l.dwarf.Ranges(entry) if err != nil { continue }
// 檢查是否是內(nèi)聯(lián)函數(shù) if len(rng) == 1 { if rng[0][0] <= address && rng[0][1] > address && entry.Tag == dwarf.TagInlinedSubroutine { isInline = true break } } }
lr, err := a2l.dwarf.LineReader(e) if err != nil { return nil, err }
if isInline { // 內(nèi)聯(lián)函數(shù)的特殊處理 err := lr.SeekPC(address, &line) if err != nil { return nil, err }
// 檢查前一個(gè)條目的行號(hào) err = lr.SeekPC(uint64(line.Address-1), &line) if err != nil { return nil, err }
// 如果前一個(gè)條目行號(hào)為0,檢查下一個(gè)條目 if line.Line == 0 { err := lr.SeekPC(address, &line) if err != nil { return nil, err }
var line2 dwarf.LineEntry lr.Next(&line2) if line2.Line != 0 { line = line2 } } } else { // 非內(nèi)聯(lián)函數(shù)直接獲取行號(hào) err = lr.SeekPC(address, &line) if err != nil { return nil, err } }
var entry Addr2LineEntry entry.SoPath = a2l.soPath entry.Address = address entry.Func = functionName entry.File = line.File.Name entry.Line = uint(line.Line) entry.Inline = isInline return &entry, nil
關(guān)鍵技術(shù)點(diǎn)
- · 處理特殊前綴(如動(dòng)態(tài)鏈接器前綴)
- 3. 內(nèi)聯(lián)函數(shù)處理
- · 通過(guò)DWARF信息識(shí)別內(nèi)聯(lián)函數(shù)
獲取目標(biāo)程序源碼信息JitedLineInfos (JIT后的行信息)jitedLineInfos, _ := pinfo.JitedLineInfos()
- · 從 Linux 5.0 版本開(kāi)始支持
- · 對(duì)于調(diào)試和性能分析非常重要
lineInfos 切片按執(zhí)行順序存儲(chǔ)了主函數(shù)和所有子函數(shù)中每個(gè)代碼行對(duì)應(yīng)的 JIT 編譯后的內(nèi)核地址。
具體來(lái)說(shuō): - 1. 存儲(chǔ)內(nèi)容:
- · 主函數(shù)的代碼行對(duì)應(yīng)的內(nèi)核地址
- · 所有子函數(shù)的代碼行對(duì)應(yīng)的內(nèi)核地址
- 2. 存儲(chǔ)順序:
- · 從程序入口開(kāi)始,依次記錄每個(gè)代碼行的地址
- · 當(dāng)遇到子函數(shù)時(shí),也會(huì)記錄子函數(shù)中的代碼行地址
例如,假設(shè)有這樣的代碼: int subprogram(int x) { // 地址: 0xffffffffc0123450 return x + 1; // 地址: 0xffffffffc0123460 }
int main() { // 地址: 0xffffffffc0123468 int a = 1; // 地址: 0xffffffffc0123470 return subprogram(a); // 地址: 0xffffffffc0123480 }
對(duì)應(yīng)的 lineInfos 可能是: lineInfos = []uint64{ 0xffffffffc0123450, // subprogram 函數(shù)聲明 0xffffffffc0123460, // subprogram return 語(yǔ)句 0xffffffffc0123468, // main 函數(shù)聲明 0xffffffffc0123470, // main 中的賦值語(yǔ)句 0xffffffffc0123480, // main 中的 return 語(yǔ)句 }
這種設(shè)計(jì)使得調(diào)試器可以在程序執(zhí)行過(guò)程中準(zhǔn)確追蹤代碼的執(zhí)行位置。 這些信息主要用于: - · 性能調(diào)優(yōu): 通過(guò)查看 JIT 編譯后的指令質(zhì)量
- · 調(diào)試: 通過(guò)符號(hào)地址和行號(hào)定位問(wèn)題
- · 分析: 理解程序的內(nèi)存布局和結(jié)構(gòu)
- · 監(jiān)控: 追蹤程序在內(nèi)核中的執(zhí)行
JitedInsns (JIT編譯后的機(jī)器碼)jitedInsns, _ := pinfo.JitedInsns()
- · 這是 BPF 程序被 JIT(即時(shí)編譯)后生成的本地機(jī)器指令
- · 是實(shí)際在 CPU 上執(zhí)行的二進(jìn)制代碼
- · 從 Linux 4.13 版本開(kāi)始支持獲取
- · 這些指令已經(jīng)被優(yōu)化并轉(zhuǎn)換為特定 CPU 架構(gòu)(如 x86、ARM)的原生指令
jitedInsns 是一個(gè) []uint8 切片,存儲(chǔ)的是 JIT 編譯后的機(jī)器碼指令,每個(gè)元素都是一個(gè)字節(jié)值(如 15, 31, 68 等)。
// jitedInsns 存儲(chǔ)的是 JIT 編譯后的機(jī)器碼指令字節(jié) jitedInsns = []uint8{ 15, // 第一個(gè)字節(jié) 31, // 第二個(gè)字節(jié) 68, // 第三個(gè)字節(jié) 0, // 第四個(gè)字節(jié) 0, // 第五個(gè)字節(jié) 102, // 第六個(gè)字節(jié) // ... 更多機(jī)器碼字節(jié) }
這些字節(jié)組成了完整的機(jī)器碼指令序列。每條機(jī)器指令可能由一個(gè)或多個(gè)字節(jié)組成,需要通過(guò)反匯編工具(如代碼中使用的 gapstone)來(lái)解析這些原始字節(jié),得到可讀的匯編指令。 這也是為什么代碼中使用: inst, err := engine.Disasm(fnInsns, kaddr, 1) // 使用 gapstone 反匯編一條指令
來(lái)解析這些原始字節(jié)序列。 JitedKsyms (內(nèi)核符號(hào)地址)jitedKsyms, _ := pinfo.JitedKsymAddrs()
- · 返回 BPF 程序在內(nèi)核中的符號(hào)地址
- · 這些地址對(duì)應(yīng) /proc/kallsyms 中的符號(hào)
- · 從 Linux 4.18 版本開(kāi)始支持
- · 用于在內(nèi)核空間定位和追蹤 BPF 程序
ksyms 存儲(chǔ)的是每個(gè)函數(shù)在內(nèi)核中的符號(hào)地址。例如:
ksyms = []uintptr{ 0xffffffffc0123450, // 主函數(shù)的符號(hào)地址 0xffffffffc0123500, // 子函數(shù)1的符號(hào)地址 0xffffffffc0123580, // 子函數(shù)2的符號(hào)地址 // ... }
假設(shè)有這樣一個(gè) eBPF 程序: // 子函數(shù)1 static int __always_inline helper_func1(void *ctx) { return 1; }
// 子函數(shù)2 static int __always_inline helper_func2(void *ctx) { return 2; }
// 主函數(shù) SEC('kprobe/sys_execve') int main_prog(void *ctx) { helper_func1(ctx); return helper_func2(ctx); }
對(duì)應(yīng)的 ksyms 可能是: ksyms = []uintptr{ 0xffffffffc0123450, // main_prog 的符號(hào)地址 0xffffffffc0123500, // helper_func1 的符號(hào)地址 0xffffffffc0123580, // helper_func2 的符號(hào)地址 }
關(guān)鍵特點(diǎn): - 1. 每個(gè)函數(shù)只有一個(gè)符號(hào)地址(函數(shù)入口地址)
- 2. 這些地址可以在
/proc/kallsyms 中找到對(duì)應(yīng)的符號(hào)名 - 3. 主要用于調(diào)試和跟蹤程序執(zhí)行
與 lineInfos 的區(qū)別: - ·
ksyms 是函數(shù)級(jí)別的,每個(gè)函數(shù)一個(gè)地址 - ·
lineInfos 是指令級(jí)別的,記錄了函數(shù)內(nèi)部各個(gè)指令塊的地址
JitedFuncLens (函數(shù)指令長(zhǎng)度)jitedFuncLens, _ := pinfo.JitedFuncLens()
- · 返回 JIT 編譯后每個(gè)函數(shù)的指令長(zhǎng)度
- · 可以用來(lái)分析程序中各個(gè)函數(shù)的大小
- · 從 Linux 4.18 版本開(kāi)始支持
- · 幫助理解程序結(jié)構(gòu)和內(nèi)存布局
funcLens 是函數(shù)級(jí)別的切片,它記錄了每個(gè)函數(shù)的指令長(zhǎng)度。
從代碼注釋可以看到: type programJitedInfo struct { // funcLens holds the insns length of each function. // // Available from 4.18. funcLens []uint32 // 每個(gè)函數(shù)的指令長(zhǎng)度 numFuncLens uint32 // 函數(shù)數(shù)量 }
舉個(gè)例子,假設(shè)有這樣的程序: // 函數(shù)1: JIT后占用20字節(jié)的指令 int subprogram1() { return 1; }
// 函數(shù)2: JIT后占用30字節(jié)的指令 int subprogram2() { int a = 1; return a + 2; }
// 主函數(shù): JIT后占用50字節(jié)的指令 int main() { subprogram1(); return subprogram2(); }
對(duì)應(yīng)的 funcLens 可能是: funcLens = []uint32{ 50, // main 函數(shù)的指令長(zhǎng)度 20, // subprogram1 的指令長(zhǎng)度 30 // subprogram2 的指令長(zhǎng)度 }
所以 funcLens 是函數(shù)級(jí)別的,記錄了每個(gè)函數(shù) JIT 編譯后的指令長(zhǎng)度 JIT 信息校驗(yàn)newBPFProgInfo 中的校驗(yàn)策略主要確保各種信息的數(shù)量匹配和一致性。讓我總結(jié)這些校驗(yàn):
// 1. 函數(shù)信息數(shù)量校驗(yàn) if len(funcInfos) != len(jitedFuncLens) { return nil, fmt.Errorf('func info number %d != jited func lens number %d', len(funcInfos), len(jitedFuncLens)) }
確保:每個(gè)函數(shù)都有對(duì)應(yīng)的長(zhǎng)度信息 // 2. 內(nèi)核符號(hào)地址數(shù)量校驗(yàn) if len(jitedKsyms) != len(jitedFuncLens) { return nil, fmt.Errorf('jited ksyms number %d != jited func lens number %d', len(jitedKsyms), len(jitedFuncLens)) }
確保:每個(gè)函數(shù)都有對(duì)應(yīng)的內(nèi)核符號(hào)地址 // 3. 行信息數(shù)量校驗(yàn) if len(jitedLineInfos) != len(lines) { return nil, fmt.Errorf('line info number %d != jited line info number %d', len(lines), len(jitedLineInfos)) }
確保:每個(gè) JIT 后的行信息地址都有對(duì)應(yīng)的源碼行信息 總結(jié)校驗(yàn)策略: - 1. 以
jitedFuncLens (函數(shù)長(zhǎng)度數(shù)組)作為基準(zhǔn) - 2. 確保函數(shù)相關(guān)信息數(shù)量一致:
- · 函數(shù)符號(hào)地址 (jitedKsyms)
- 3. 確保行信息數(shù)量一致:
- · JIT 后的行信息地址 (jitedLineInfos)
源碼信息轉(zhuǎn)換newBPFProgInfo 函數(shù)的處理流程:
// 獲取程序的各種信息 pinfo, err := prog.Info() funcInfos, err := pinfo.FuncInfos() // 獲取函數(shù)信息 lines, _ := pinfo.LineInfos() // 獲取行信息 jitedInsns, _ := pinfo.JitedInsns() // 獲取JIT后的指令 jitedKsyms, _ := pinfo.JitedKsymAddrs() // 獲取每個(gè)函數(shù)的內(nèi)核符號(hào)地址 jitedFuncLens, _ := pinfo.JitedFuncLens() // 獲取每個(gè)函數(shù)的長(zhǎng)度 jitedLineInfos, _ := pinfo.JitedLineInfos() // 獲取JIT后的行信息地址
// 建立 JIT地址 -> 源碼行信息 的映射 jited2li := make(map[uint64]btf.LineOffset, len(jitedLineInfos)) for i, kaddr := range jitedLineInfos { jited2li[kaddr] = lines[i] }
- 3. 對(duì)每個(gè)函數(shù)進(jìn)行處理:
for i, funcLen := range jitedFuncLens { // 獲取函數(shù)基本信息 ksym := uint64(jitedKsyms[i]) // 函數(shù)的內(nèi)核符號(hào)地址 fnInsns := insns[:funcLen] // 函數(shù)的指令序列 // 設(shè)置函數(shù)的地址范圍 info.kaddrRange.start = jitedKsyms[i] info.kaddrRange.end = info.kaddrRange.start + uintptr(funcLen) info.funcName = strings.TrimSpace(funcInfos[i].Func.Name)
// 遍歷函數(shù)的每條指令 for len(fnInsns) > 0 { // 計(jì)算出當(dāng)前指令的地址 kaddr := ksym + pc // 如果這個(gè)地址有對(duì)應(yīng)的行信息,記錄下來(lái) if li, ok := jited2li[kaddr]; ok { info.jitedLineInfo = append(info.jitedLineInfo, uintptr(kaddr)) info.lineInfos = append(info.lineInfos, li) } // 計(jì)算出當(dāng)前指令長(zhǎng)度 inst, err := engine.Disasm(fnInsns, kaddr, 1) // 計(jì)算出下一個(gè)指令開(kāi)始的偏移量 pc += uint64(inst[0].Size) // 從當(dāng)前指令塊中刪除當(dāng)前指令 fnInsns = fnInsns[instSize:] } }
最終得到的信息包括: - 1. 每個(gè)函數(shù)的地址范圍(start, end)
- 3. 函數(shù)內(nèi)每個(gè)關(guān)鍵位置的地址及其對(duì)應(yīng)的源碼行信息
這樣就建立了:內(nèi)核地址 -> 源碼位置 的完整映射關(guān)系,可以用于調(diào)試和跟蹤。 源碼信息轉(zhuǎn)換示例- 1. 假設(shè)有這樣一個(gè) BPF 程序:
// test.bpf.c SEC('kprobe/sys_execve') int main_prog(void *ctx) { helper_func1(ctx); // 行號(hào)3 return helper_func2(ctx); // 行號(hào)4 }
static int helper_func1(void *ctx) { return 1; // 行號(hào)8 }
static int helper_func2(void *ctx) { int a = 1; // 行號(hào)12 return a + 2; // 行號(hào)13 }
// 函數(shù)信息 funcInfos = []btf.FuncInfo{ {Name: 'main_prog'}, {Name: 'helper_func1'}, {Name: 'helper_func2'}, }
// 函數(shù)符號(hào)地址 jitedKsyms = []uintptr{ 0xffffffffc0123450, // main_prog 0xffffffffc0123500, // helper_func1 0xffffffffc0123580, // helper_func2 }
// 函數(shù)長(zhǎng)度 jitedFuncLens = []uint32{ 48, // main_prog 長(zhǎng)度 32, // helper_func1 長(zhǎng)度 40, // helper_func2 長(zhǎng)度 }
// JIT后的行信息地址 jitedLineInfos = []uint64{ 0xffffffffc0123450, // main_prog 開(kāi)始 0xffffffffc0123460, // helper_func1 調(diào)用 0xffffffffc0123470, // helper_func2 調(diào)用 0xffffffffc0123500, // helper_func1 開(kāi)始 0xffffffffc0123510, // return 1 0xffffffffc0123580, // helper_func2 開(kāi)始 0xffffffffc0123590, // int a = 1 0xffffffffc0123598, // return a + 2 }
// jitedInsns 存儲(chǔ)的是實(shí)際的機(jī)器碼指令字節(jié) jitedInsns = []uint8{ 15, // push rbp 31, 68, 0, // mov rbp, rsp 0, 102, 144, // sub rsp, 0x10 85, 72, // 其他指令 // ... 更多機(jī)器碼指令字節(jié) }
// lineInfos 存儲(chǔ)的是源碼行信息 lineInfos = []btf.LineOffset{ { Offset: 0, Line: btf.Line{ fileName: './test.bpf.c', line: 'int main_prog(void *ctx)', lineNumber: 690, lineColumn: 0, }, }, // ... 更多行信息 }
- 3. 轉(zhuǎn)換后的
bpfProgAddrLineInfo 數(shù)組:
progs = []*bpfProgAddrLineInfo{ // main_prog 的信息 { kaddrRange: { start: 0xffffffffc0123450, end: 0xffffffffc0123450 + 48, }, funcName: 'main_prog', jitedLineInfo: []uintptr{ 0xffffffffc0123450, // 函數(shù)開(kāi)始 0xffffffffc0123460, // helper_func1 調(diào)用 0xffffffffc0123470, // helper_func2 調(diào)用 }, lineInfos: []btf.LineOffset{ { Offset: 0, Line: btf.Line{ fileName: './test.bpf.c', line: 'int main_prog(void *ctx)', lineNumber: 20, lineColumn: 0, }, }, }, }
掛載跟蹤程序link.AttachTracing 的功能和使用方法:
功能說(shuō)明link.AttachTracing 是用于將 eBPF 跟蹤程序附加到內(nèi)核模塊中定義的 BPF hook 點(diǎn)的方法。主要支持以下類(lèi)型的跟蹤:
- 3. fmod_ret(函數(shù)返回值修改)
- 4. BTF 支持的原始跟蹤點(diǎn)(tp_btf)
使用方法func AttachTracing(opts TracingOptions) (Link, error)
參數(shù)說(shuō)明:type TracingOptions struct { // 必須是 Tracing 類(lèi)型的程序,支持以下 attach 類(lèi)型: // - AttachTraceFEntry // - AttachTraceFExit // - AttachModifyReturn // - AttachTraceRawTp Program *ebpf.Program // 附加類(lèi)型(可選) AttachType ebpf.AttachType // 可以通過(guò) bpf_get_attach_cookie() 在 eBPF 程序中獲取的任意值 Cookie uint64 }
使用示例// 1. 基本使用 link, err := link.AttachTracing(link.TracingOptions{ Program: bpfProgram, AttachType: ebpf.AttachTraceFExit, }) if err != nil { return fmt.Errorf('failed to attach tracing: %w', err) } defer link.Close()
// 2. 帶 Cookie 的使用 link, err := link.AttachTracing(link.TracingOptions{ Program: bpfProgram, AttachType: ebpf.AttachTraceFExit, Cookie: 12345, })
注意事項(xiàng)
if t := opts.Program.Type(); t != ebpf.Tracing { return nil, fmt.Errorf('invalid program type %s, expected Tracing', t) }
switch opts.AttachType { case ebpf.AttachTraceFEntry, ebpf.AttachTraceFExit, ebpf.AttachModifyReturn, ebpf.AttachTraceRawTp, ebpf.AttachNone: default: return nil, fmt.Errorf('invalid attach type: %s', opts.AttachType.String()) }
- · 使用后需要調(diào)用
Close() 方法釋放資源
實(shí)際應(yīng)用場(chǎng)景bpf_tracing.go 中的 traceProg 方法就是一個(gè)很好的使用示例:
func (t *bpfTracing) traceProg(spec *ebpf.CollectionSpec, reusedMaps map[string]*ebpf.Map, info bpfTracingInfo) error { // 獲取跟蹤程序 eBPF prog 對(duì)象 progSpec := spec.Programs[tracingFuncName] // 設(shè)置目標(biāo) hook 的 eBPF prog 對(duì)象 progSpec.AttachTarget = info.prog // 設(shè)置目標(biāo) hook eBPF prog 對(duì)象的函數(shù)名稱(chēng) progSpec.AttachTo = info.funcName progSpec.AttachType = ebpf.AttachTraceFExit l, err := link.AttachTracing(link.TracingOptions{ Program: prog, AttachType: ebpf.AttachTraceFExit, }) if err != nil { _ = prog.Close() return fmt.Errorf('failed to attach tracing: %w', err) } // ... 后續(xù)處理代碼 ... }
用戶(hù)態(tài)解析內(nèi)核態(tài) LBR 事件結(jié)構(gòu)type Event struct { Entries [32]LbrEntry // LBR(Last Branch Record)條目數(shù)組 NrBytes int64 // 有效數(shù)據(jù)的字節(jié)數(shù) Retval int64 // 函數(shù)返回值 FuncIP uintptr // 函數(shù)的指令指針地址 CPU uint32 // 執(zhí)行 CPU 編號(hào) Pid uint32 // 進(jìn)程 ID Comm [16]byte // 進(jìn)程名稱(chēng) StackID int64 // 函數(shù)調(diào)用棧 ID }
type LbrEntry struct { From uintptr // 分支指令的源地址 To uintptr // 分支指令的目標(biāo)地址 Flags uint64 // 分支標(biāo)志 }
字段說(shuō)明 - 1. Entries [32]LbrEntry
- · 每個(gè)條目包含分支的源地址和目標(biāo)地址
- 2. NrBytes int64
- · 表示實(shí)際記錄的有效數(shù)據(jù)字節(jié)數(shù)
- · 用于計(jì)算有效的 LBR 條目數(shù):
nrEntries := event.NrBytes / int64(8*3) - · 如果為負(fù)值且等于
-ENOENT ,表示 LBR 不被支持
- 3. Retval int64
- · 在
TracingModeEntry 模式下不使用
- 4. FuncIP uintptr
- · 用于定位函數(shù)位置和獲取函數(shù)信息
- 5. CPU uint32
- · 記錄事件發(fā)生時(shí)的 CPU 編號(hào)
- 6. Pid uint32
- · 記錄產(chǎn)生事件的進(jìn)程 ID
- 7. Comm [16]byte
- · 固定長(zhǎng)度為 16 字節(jié)的字符數(shù)組
- 8. StackID int64
- · 函數(shù)調(diào)用棧的唯一標(biāo)識(shí)符
- · 用于關(guān)聯(lián)函數(shù)調(diào)用棧信息
計(jì)算 LBR 記錄數(shù)量在代碼中 nrEntries := event.NrBytes / int64(8*3) 這行實(shí)際上是在計(jì)算 LBR (Last Branch Record) 條目的數(shù)量。 這里除以 24 (8*3) 是因?yàn)? - 1. 每個(gè)
LbrEntry 結(jié)構(gòu)體包含 3 個(gè)字段:
type LbrEntry struct { From uintptr // 8 字節(jié) To uintptr // 8 字節(jié) Flags uint64 // 8 字節(jié) }
- 2. 每個(gè)字段都是 8 字節(jié)長(zhǎng)度:
- 3. 所以一個(gè)完整的
LbrEntry 占用 8*3 = 24 字節(jié)
因此,當(dāng)我們有一個(gè)字節(jié)數(shù)組(event.NrBytes )包含多個(gè) LBR 條目時(shí),需要除以 24 來(lái)得到實(shí)際的條目數(shù)量。 例如: - · 如果
event.NrBytes = 48 ,那么 nrEntries = 48/24 = 2 ,表示有 2 個(gè) LBR 條目 - · 如果
event.NrBytes = 72 ,那么 nrEntries = 72/24 = 3 ,表示有 3 個(gè) LBR 條目
清洗 LBR 切片entries := event.Entries[:nrEntries]
這行代碼是在對(duì) event.Entries 數(shù)組進(jìn)行切片操作,獲取前 nrEntries 個(gè)有效的 LBR 條目。 - 1. 首先看
Event 結(jié)構(gòu)體中的定義:
type Event struct { Entries [32]LbrEntry // 固定大小為32的數(shù)組 // ... 其他字段 }
- 2.
entries := event.Entries[:nrEntries] 這個(gè)切片操作:- ·
event.Entries 是一個(gè)固定大小為 32 的數(shù)組 - ·
:nrEntries 表示從索引 0 到 nrEntries-1 的切片 - · 只獲取實(shí)際包含有效數(shù)據(jù)的部分
例如: - · 如果
nrEntries = 5 ,那么 entries 將只包含前 5 個(gè) LBR 條目 - · 雖然
Entries 數(shù)組大小是 32,但可能只有一部分包含實(shí)際的分支記錄數(shù)據(jù)
這樣做的目的是: - · 只關(guān)注實(shí)際記錄的分支信息
- · 提高處理效率,因?yàn)椴恍枰闅v整個(gè) 32 個(gè)條目的數(shù)組
解析 LBR 記錄解析 eBPF prog 源碼信息 func (b *bpfProgAddrLineInfo) get(addr uintptr) (*bpfProgLineInfo, bool) { // 1. 首先檢查地址是否在當(dāng)前 eBPF 程序的地址范圍內(nèi) if addr < b.kaddrRange.start || addr >= b.kaddrRange.end { return nil, false }
// 2. 在已排序的 jitedLineInfo 數(shù)組中二分查找地址 idx, ok := slices.BinarySearch(b.jitedLineInfo, addr) if !ok { // 如果沒(méi)找到精確匹配,使用前一個(gè)地址的行信息 idx-- }
// 3. 處理源文件路徑 fileName := b.lineInfos[idx].Line.FileName() if fileName != '' && fileName[0] == '.' { fileName = strings.TrimLeft(fileName, './') }
// 4. 構(gòu)造返回結(jié)果 var line bpfProgLineInfo line.funcName = b.funcName line.ksymAddr = b.kaddrRange.start line.fileName = fileName line.fileLine = b.lineInfos[idx].Line.LineNumber() return &line, true }
這個(gè)方法的主要功能是:根據(jù) JIT 編譯后的指令地址,查找對(duì)應(yīng)的源代碼位置信息。 關(guān)鍵數(shù)據(jù)結(jié)構(gòu): type bpfProgAddrLineInfo struct { kaddrRange bpfProgKaddrRange // 程序的地址范圍 funcName string // 函數(shù)名 jitedLineInfo []uintptr // JIT 后的指令地址,已排序 lineInfos []btf.LineOffset // 與 jitedLineInfo 一一對(duì)應(yīng)的行信息 }
工作流程: - 1. 地址范圍檢查:
- · 確保要查找的地址在當(dāng)前 eBPF 程序的地址范圍內(nèi)
- 2. 二分查找:
- · 如果找不到精確匹配,使用小于該地址的最近一個(gè)行信息
- · 這是因?yàn)橐恍性创a可能對(duì)應(yīng)多條機(jī)器指令
- 3. 源文件路徑處理:
- · 移除開(kāi)頭的 './' 使路徑更規(guī)范
解析內(nèi)核函數(shù)源碼信息 func getLineInfo(addr uintptr, progs *bpfProgs, a2l *Addr2Line, ksyms *Kallsyms) *branchEndpoint { // ... eBPF 程序解析部分省略 ...
// 1. 創(chuàng)建返回結(jié)構(gòu) var ep branchEndpoint ep.addr = addr defer ep.updateInfo()
// 2. 通過(guò) kallsyms 獲取基本符號(hào)信息 if ksym, ok := ksyms.find(addr); ok { ep.funcName = ksym.name ep.offset = addr - uintptr(ksym.addr) }
// 3. 使用 addr2line 獲取詳細(xì)源碼信息 if a2l == nil { return &ep }
li, err := a2l.get(addr) if err != nil { return &ep }
// 4. 處理源文件路徑和行號(hào)信息 fileName := li.File if strings.HasPrefix(fileName, a2l.buildDir) { fileName = fileName[len(a2l.buildDir):] }
ep.funcName = li.Func ep.fileName = fileName ep.fileLine = uint32(li.Line) ep.fromVmlinux = true return &ep }
解析內(nèi)核函數(shù)信息的過(guò)程分為以下幾個(gè)步驟: - 1. 通過(guò) kallsyms 獲取基本信息
- ·
kallsyms 是內(nèi)核符號(hào)表,包含所有內(nèi)核函數(shù)的地址映射
- 2. 使用 addr2line 工具獲取詳細(xì)信息
- ·
addr2line 是一個(gè)將地址轉(zhuǎn)換為源碼位置的工具 - · 需要內(nèi)核帶調(diào)試信息(vmlinux)
輸出示例: // 只有 kallsyms 信息時(shí) do_sys_open+0x58
// 有完整調(diào)試信息時(shí) do_sys_open+0x58 ; fs/open.c:1234
引用鏈接[1] bpflbr: https://github.com/Asphaltt/bpflbr
|