一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

eBPF 項(xiàng)目源碼分析-BPFLBR

 江海博覽 2024-12-11

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. 1. Ring Buffer 支持
    • · 檢測(cè) BPF_MAP_TYPE_RINGBUF 是否存在
    • · 用于高效的數(shù)據(jù)傳輸
  2. 2. 分支追蹤支持
    • · 檢測(cè) bpf_get_branch_snapshot() 是否存在
    • · 用于獲取 LBR(Last Branch Record)數(shù)據(jù)
  3. 3. 函數(shù)上下文獲取
    • · bpf_get_func_ret(): 獲取函數(shù)返回值
    • · bpf_get_func_ip(): 獲取函數(shù)指令指針
  4. 4. 堆棧追蹤支持
    • · 檢測(cè) bpf_get_stackid() 是否存在
    • · 僅在啟用 --output-stack 選項(xiàng)時(shí)需要

檢測(cè)機(jī)制

  1. 1. 通過(guò) fentry 掛載到 __x64_sys_nanosleep 系統(tǒng)調(diào)用
  2. 2. 使用 bpf_core_enum_value_exists 檢查各個(gè)特性是否存在
  3. 3. 將結(jié)果存儲(chǔ)在 .bss 段中供用戶(hù)態(tài)程序讀取
  4. 4. 用戶(hù)態(tài)程序會(huì)驗(yàn)證所有必需特性,如果缺少任何必需特性則報(bào)錯(cuò)退出

內(nèi)核符號(hào)信息

讀取基本符號(hào)信息

/proc/kallsyms 讀取基本符號(hào)信息:

  1. 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)地址
}
  1. 2. 讀取和解析過(guò)程:
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)
}
  1. 3. 符號(hào)查找方法:
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. 1. 只關(guān)注 t/T 類(lèi)型的符號(hào)(text 段的函數(shù)符號(hào))
  2. 2. 維護(hù)了雙向映射:地址到符號(hào)和符號(hào)到地址
  3. 3. 使用二分查找優(yōu)化符號(hào)查找性能
  4. 4. 特別關(guān)注兩個(gè)重要符號(hào):
    • · _stext:內(nèi)核代碼段起始地址
    • · __x64_sys_bpf:BPF 系統(tǒng)調(diào)用入口
  5. 5. 支持模塊信息的記錄

獲取 vmlinux 調(diào)試信息

  1. 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',
       // ...
   }
   // ...
}
  1. 2. 如果找到 vmlinux 文件,則:
  • · 讀取 .text 段地址
  • · 計(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. 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ù)
}
  1. 2. 主要實(shí)現(xiàn)步驟

初始化過(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)的源代碼位置信息。

  1. 1. 地址定位
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')
   }
  1. 2. 函數(shù)名處理
    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
   }
  1. 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
           }
       }
   }
  1. 4. 行號(hào)信息獲取
    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
       }
   }
  1. 5. 結(jié)果返回
   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)

  1. 1. 二分查找優(yōu)化
  • · 使用 sort.Search 快速定位地址
  • · 查找小于目標(biāo)地址的最大地址
  1. 2. 名稱(chēng)重整處理
  • · 支持C++符號(hào)的解析
  • · 處理特殊前綴(如動(dòng)態(tài)鏈接器前綴)
  1. 3. 內(nèi)聯(lián)函數(shù)處理
  • · 通過(guò)DWARF信息識(shí)別內(nèi)聯(lián)函數(shù)
  • · 特殊的行號(hào)查找邏輯
  • · 處理行號(hào)為0的邊界情況
  1. 4. DWARF調(diào)試信息使用
  • · 使用LineReader獲取源碼位置
  • · 處理地址范圍和編譯單元信息

獲取目標(biāo)程序源碼信息

JitedLineInfos (JIT后的行信息)

jitedLineInfos, _ := pinfo.JitedLineInfos()
  • · 提供 JIT 編譯后代碼的行號(hào)信息
  • · 幫助將機(jī)器碼映射回源代碼位置
  • · 從 Linux 5.0 版本開(kāi)始支持
  • · 對(duì)于調(diào)試和性能分析非常重要

lineInfos 切片按執(zhí)行順序存儲(chǔ)了主函數(shù)和所有子函數(shù)中每個(gè)代碼行對(duì)應(yīng)的 JIT 編譯后的內(nèi)核地址。

具體來(lái)說(shuō):

  1. 1. 存儲(chǔ)內(nèi)容:
    • · 主函數(shù)的代碼行對(duì)應(yīng)的內(nèi)核地址
    • · 所有子函數(shù)的代碼行對(duì)應(yīng)的內(nèi)核地址
  2. 2. 存儲(chǔ)順序:
    • · 按照程序的執(zhí)行順序存儲(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. 1. 每個(gè)函數(shù)只有一個(gè)符號(hào)地址(函數(shù)入口地址)
  2. 2. 這些地址可以在 /proc/kallsyms 中找到對(duì)應(yīng)的符號(hào)名
  3. 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. 1. 以 jitedFuncLens(函數(shù)長(zhǎng)度數(shù)組)作為基準(zhǔn)
  2. 2. 確保函數(shù)相關(guān)信息數(shù)量一致:
    • · 函數(shù)信息 (funcInfos)
    • · 函數(shù)符號(hào)地址 (jitedKsyms)
  3. 3. 確保行信息數(shù)量一致:
    • · JIT 后的行信息地址 (jitedLineInfos)
    • · 源碼行信息 (lines)

源碼信息轉(zhuǎn)換

newBPFProgInfo 函數(shù)的處理流程:

  1. 1. 首先獲取程序的基本信息:
// 獲取程序的各種信息
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后的行信息地址
  1. 2. 建立行信息映射:
// 建立 JIT地址 -> 源碼行信息 的映射
jited2li := make(map[uint64]btf.LineOffset, len(jitedLineInfos))
for i, kaddr := range jitedLineInfos {
   jited2li[kaddr] = lines[i]
}
  1. 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. 1. 每個(gè)函數(shù)的地址范圍(start, end)
  2. 2. 函數(shù)名
  3. 3. 函數(shù)內(nèi)每個(gè)關(guān)鍵位置的地址及其對(duì)應(yīng)的源碼行信息

這樣就建立了:內(nèi)核地址 -> 源碼位置 的完整映射關(guān)系,可以用于調(diào)試和跟蹤。

源碼信息轉(zhuǎn)換示例

  1. 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
}
  1. 2. 從程序中獲取的原始信息:
// 函數(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,
       },
   },
   // ... 更多行信息
}
  1. 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)型的跟蹤:

  1. 1. fentry(函數(shù)入口跟蹤)
  2. 2. fexit(函數(shù)退出跟蹤)
  3. 3. fmod_ret(函數(shù)返回值修改)
  4. 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)

  1. 1. 程序類(lèi)型要求
if t := opts.Program.Type(); t != ebpf.Tracing {
   return nil, fmt.Errorf('invalid program type %s, expected Tracing', t)
}
  1. 2. 支持的附加類(lèi)型
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())
}
  1. 3. 資源管理
  • · 使用后需要調(diào)用 Close() 方法釋放資源
  • · 建議使用 defer 確保資源被正確釋放

實(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. 1. Entries [32]LbrEntry
    • · 存儲(chǔ)最近的分支記錄
    • · 固定大小為 32 個(gè)條目
    • · 每個(gè)條目包含分支的源地址和目標(biāo)地址
  2. 2. NrBytes int64
    • · 表示實(shí)際記錄的有效數(shù)據(jù)字節(jié)數(shù)
    • · 用于計(jì)算有效的 LBR 條目數(shù):nrEntries := event.NrBytes / int64(8*3)
    • · 如果為負(fù)值且等于 -ENOENT,表示 LBR 不被支持
  3. 3. Retval int64
    • · 存儲(chǔ)被跟蹤函數(shù)的返回值
    • · 在 TracingModeEntry 模式下不使用
  4. 4. FuncIP uintptr
    • · 被跟蹤函數(shù)的指令指針
    • · 用于定位函數(shù)位置和獲取函數(shù)信息
  5. 5. CPU uint32
    • · 記錄事件發(fā)生時(shí)的 CPU 編號(hào)
    • · 用于多核系統(tǒng)中的事件追蹤
  6. 6. Pid uint32
    • · 記錄產(chǎn)生事件的進(jìn)程 ID
    • · 用于進(jìn)程級(jí)別的追蹤
  7. 7. Comm [16]byte
    • · 記錄進(jìn)程的命令名稱(chēng)
    • · 固定長(zhǎng)度為 16 字節(jié)的字符數(shù)組
  8. 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. 1. 每個(gè) LbrEntry 結(jié)構(gòu)體包含 3 個(gè)字段:
type LbrEntry struct {
   From  uintptr  // 8 字節(jié)
   To    uintptr  // 8 字節(jié)
   Flags uint64   // 8 字節(jié)
}
  1. 2. 每個(gè)字段都是 8 字節(jié)長(zhǎng)度:
    • · From: 8 字節(jié)
    • · To: 8 字節(jié)
    • · Flags: 8 字節(jié)
  2. 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. 1. 首先看 Event 結(jié)構(gòu)體中的定義:
type Event struct {
   Entries [32]LbrEntry  // 固定大小為32的數(shù)組
   // ... 其他字段
}
  1. 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ù)

這樣做的目的是:

  • · 避免處理空的或無(wú)效的條目
  • · 只關(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. 1. 地址范圍檢查
    • · 確保要查找的地址在當(dāng)前 eBPF 程序的地址范圍內(nèi)
    • · 如果不在范圍內(nèi),返回 false
  2. 2. 二分查找
    • · 在 jitedLineInfo 中查找地址
    • · 如果找不到精確匹配,使用小于該地址的最近一個(gè)行信息
    • · 這是因?yàn)橐恍性创a可能對(duì)應(yīng)多條機(jī)器指令
  3. 3. 源文件路徑處理
    • · 獲取源文件名
    • · 移除開(kāi)頭的 './' 使路徑更規(guī)范
  4. 4. 返回信息
    • · 函數(shù)名
    • · 程序起始地址
    • · 源文件名
    • · 行號(hào)

解析內(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. 1. 通過(guò) kallsyms 獲取基本信息
    • · kallsyms 是內(nèi)核符號(hào)表,包含所有內(nèi)核函數(shù)的地址映射
    • · 可以獲?。?/span>
      • · 函數(shù)名
      • · 函數(shù)起始地址
      • · 計(jì)算出相對(duì)偏移量
  2. 2. 使用 addr2line 工具獲取詳細(xì)信息
    • · addr2line 是一個(gè)將地址轉(zhuǎn)換為源碼位置的工具
    • · 需要內(nèi)核帶調(diào)試信息(vmlinux)
    • · 可以獲?。?/span>
      • · 精確的源文件路徑
      • · 具體的行號(hào)
      • · 規(guī)范化的函數(shù)名
  3. 3. 源文件路徑處理
    • · 移除構(gòu)建目錄前綴
    • · 使路徑更簡(jiǎn)潔易讀

輸出示例:

// 只有 kallsyms 信息時(shí)
do_sys_open+0x58

// 有完整調(diào)試信息時(shí)
do_sys_open+0x58 ; fs/open.c:1234

引用鏈接

[1] bpflbr: https://github.com/Asphaltt/bpflbr

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶(hù)發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶(hù) 評(píng)論公約

    類(lèi)似文章 更多

    国产精品香蕉在线的人| 国产传媒中文字幕东京热| 有坂深雪中文字幕亚洲中文| 国产又粗又猛又爽又黄的文字| 国产水滴盗摄一区二区| 亚洲一区二区三在线播放| 国产精品日本女优在线观看| 欧美夫妻性生活一区二区| 国产在线不卡中文字幕| 制服丝袜美腿美女一区二区| 中文字幕乱子论一区二区三区| 草草草草在线观看视频| 色哟哟在线免费一区二区三区| 加勒比东京热拍拍一区二区| 无套内射美女视频免费在线观看| 厕所偷拍一区二区三区视频| 久热香蕉精品视频在线播放| 天海翼高清二区三区在线| 日本免费一级黄色录像| 精品欧美日韩一区二区三区 | 国产精品一区日韩欧美| 色婷婷日本视频在线观看| 欧美日韩亚洲精品在线观看| 国产欧美一区二区三区精品视| 久久99国产精品果冻传媒| 国产av精品高清一区二区三区| 久久国产精品热爱视频| 亚洲精品一区二区三区免| 精品女同一区二区三区| 91日韩在线视频观看| 国产一区二区不卡在线视频| 欧美日韩黑人免费观看| 天堂网中文字幕在线观看| 日本加勒比在线播放一区| 国产内射一级一片内射高清| 欧洲精品一区二区三区四区| 欧美日韩在线观看自拍| 黄片免费观看一区二区| 色老汉在线视频免费亚欧| 欧美午夜一区二区福利视频| 国产成人精品国产成人亚洲|