diff --git a/helpers/kernel_symbols.go b/helpers/kernel_symbols.go index 8929d3d8..c1a1baa5 100644 --- a/helpers/kernel_symbols.go +++ b/helpers/kernel_symbols.go @@ -3,32 +3,17 @@ package helpers import ( "bufio" "fmt" - "math" "os" - "sort" "strconv" "strings" + "sync" ) -/* - * The helpers in this file gives the ability to query kernel symbols. - * - * The KernelSymbolTable interface should query a map of all the kernel symbols with a key which is the kernel object owner and the name with under-case between them. - * As such the query keys looks like [objectOwner_objectname{SymbolData}, objectOwner_objectname{SymbolData}, etc...] - * The key schema is due to kernel symbols being able to have the same name or address which prevents being able to key the map with only one of them. - */ - const ( kallsymsPath = "/proc/kallsyms" + chanBuffer = 112800 ) -type KernelSymbolTable interface { - TextSegmentContains(addr uint64) (bool, error) - GetSymbolByName(owner string, name string) (*KernelSymbol, error) - GetSymbolByAddr(addr uint64) (*KernelSymbol, error) - Refresh() error -} - type KernelSymbol struct { Name string Type string @@ -36,307 +21,297 @@ type KernelSymbol struct { Owner string } -// errors +func symNotFoundErr(v interface{}) error { + return fmt.Errorf("symbol not found: %v", v) +} + +// +// Interface implementation +// -func SymbolNotFound(owner, name string) error { - return fmt.Errorf("symbol not found: %s_%s", owner, name) +type name struct { + name string } -func SymbolNotFoundAtAddress(addr uint64) error { - return fmt.Errorf("symbol not found at address: 0x%x", addr) + +type addr struct { + addr uint64 } -// general +type nameAndOwner struct { + name string + owner string +} -func symbolKey(owner, name string) string { - return owner + "_" + name +type addrAndOwner struct { + addr uint64 + owner string } -// parses ksymbol line file file. returns in order the symbols "type", "name", "order" -func parseSymbolLine(line []string) (string, string, string) { - symbolType := strings.Clone(line[1]) - symbolName := strings.Clone(line[2]) +type KernelSymbolTable struct { + symbols map[name][]*KernelSymbol + addrs map[addr][]*KernelSymbol + symByName map[nameAndOwner][]*KernelSymbol + symByAddr map[addrAndOwner][]*KernelSymbol + textSegStart uint64 + textSegEnd uint64 + updateLock *sync.RWMutex + updateWg *sync.WaitGroup +} - symbolOwner := "system" - if len(line) > 3 { - // When a symbol is contained in a kernel module, it will be specified - // within square brackets, otherwise it's part of the system - symbolOwner = strings.Clone(line[3]) - symbolOwner = strings.TrimPrefix(symbolOwner, "[") - symbolOwner = strings.TrimSuffix(symbolOwner, "]") +func NewKernelSymbolTable() (*KernelSymbolTable, error) { + k := KernelSymbolTable{ + updateLock: &sync.RWMutex{}, } - return symbolType, symbolName, symbolOwner + return &k, k.Refresh() } -// fullKernelSymbolTable +// +// Getters (return a copy of the symbol for thread safety). +// + +// TextSegmentContains returns true if the given address is in the kernel text segment. +func (k *KernelSymbolTable) TextSegmentContains(addr uint64) (bool, error) { + k.updateLock.RLock() + defer k.updateLock.RUnlock() -type fullKernelSymbolTable struct { - symbolMap map[string]*KernelSymbol - symbolAddrMap map[uint64]*KernelSymbol - textSegStart uint64 - textSegEnd uint64 + return addr >= k.textSegStart && addr < k.textSegEnd, nil } -/* NewKernelSymbolsMap initiates a kernel symbol map by parsing the /proc/kallsyms file. - * Each line contains the symbol's address, segment type, name, module owner (which can be empty in case the symbol is owned by the system). - * If memory is a concern, using this constructor can allocate up to ~130mb. - * Note: the key of the map is the symbol owner and the symbol name (with undercase between them) - */ -func NewKernelSymbolsMap() (KernelSymbolTable, error) { - k := fullKernelSymbolTable{} - err := k.Refresh() +// GetSymbolByName returns all the symbols with the given name. +func (k *KernelSymbolTable) GetSymbolByName(n string) ([]KernelSymbol, error) { + k.updateLock.RLock() + defer k.updateLock.RUnlock() - return &k, err -} + symbols, exist := k.symbols[name{n}] + if !exist { + return []KernelSymbol{}, symNotFoundErr(n) + } -// TextSegmentContains checks if a given address is in the kernel text segment -// by comparing it to the kernel text segment address boundaries -func (k *fullKernelSymbolTable) TextSegmentContains(addr uint64) (bool, error) { - return addr >= k.textSegStart && addr < k.textSegEnd, nil + return copySliceOfPointersToSliceOfStructs(symbols), nil } -// GetSymbolByName returns a symbol by a given name and owner -func (k *fullKernelSymbolTable) GetSymbolByName(owner string, name string) (*KernelSymbol, error) { - symbol, exist := k.symbolMap[symbolKey(owner, name)] - if exist { - return symbol, nil +// GetSymbolByAddr returns all the symbols with the given address. +func (k *KernelSymbolTable) GetSymbolByAddr(a uint64) ([]KernelSymbol, error) { + k.updateLock.RLock() + defer k.updateLock.RUnlock() + + symbols, exist := k.addrs[addr{a}] + if !exist { + return []KernelSymbol{}, symNotFoundErr(a) } - return nil, SymbolNotFound(owner, name) + + return copySliceOfPointersToSliceOfStructs(symbols), nil } -// GetSymbolByAddr returns a symbol by a given address -func (k *fullKernelSymbolTable) GetSymbolByAddr(addr uint64) (*KernelSymbol, error) { - symbol, exist := k.symbolAddrMap[addr] - if exist { - return symbol, nil +// GetSymbolByOwnerAndName returns all the symbols with the given owner and name. +func (k *KernelSymbolTable) GetSymbolByOwnerAndName(o, n string) ([]KernelSymbol, error) { + k.updateLock.RLock() + defer k.updateLock.RUnlock() + + symbols, exist := k.symByName[nameAndOwner{n, o}] + if !exist { + return []KernelSymbol{}, symNotFoundErr(nameAndOwner{n, o}) } - return nil, SymbolNotFoundAtAddress(addr) + + return copySliceOfPointersToSliceOfStructs(symbols), nil } -func (k *fullKernelSymbolTable) Refresh() error { - k.symbolMap = make(map[string]*KernelSymbol) - k.symbolAddrMap = make(map[uint64]*KernelSymbol) - file, err := os.Open(kallsymsPath) - if err != nil { - return fmt.Errorf("could not open /proc/kallsyms: %w", err) +// GetSymbolByOwnerAndAddr returns all the symbols with the given owner and address. +func (k *KernelSymbolTable) GetSymbolByOwnerAndAddr(o string, a uint64) ([]KernelSymbol, error) { + k.updateLock.RLock() + defer k.updateLock.RUnlock() + + symbols, exist := k.symByAddr[addrAndOwner{a, o}] + if !exist { + return []KernelSymbol{}, symNotFoundErr(addrAndOwner{a, o}) } - defer file.Close() - scanner := bufio.NewScanner(file) - scanner.Split(bufio.ScanLines) - for scanner.Scan() { - line := strings.Fields(scanner.Text()) - // if the line is less than 3 words, we can't parse it (one or more - // fields missing) - if len(line) < 3 { - continue - } - symbolAddr, err := strconv.ParseUint(line[0], 16, 64) - if err != nil { - continue - } - symbolType, symbolName, symbolOwner := parseSymbolLine(line) + return copySliceOfPointersToSliceOfStructs(symbols), nil +} - symbolKey := symbolOwner + "_" + symbolName - symbol := &KernelSymbol{symbolName, symbolType, symbolAddr, symbolOwner} - k.symbolMap[symbolKey] = symbol - k.symbolAddrMap[symbolAddr] = symbol - } - stext, err := k.GetSymbolByName("system", "_stext") - if err != nil { - return err - } - k.textSegStart = stext.Address - etext, err := k.GetSymbolByName("system", "_etext") +// Concurrency logic for updating the maps: faster than a single goroutine that +// updates all maps OR multiple goroutines with fine grained locking (turned out +// to be slower than a single goroutine). +// +// Simple file parsing (no processing) takes ~0.200 seconds on a 4-core machine. +// If buffer is increased to 4MB, it might take ~0.150 seconds. A simple +// mono-threaded implementation that parses + processes the lines takes ~0.700 +// seconds. This approach takes ~0.350 seconds (2x speedup). +// +// NOTE: The procfs file reading cannot be paralelized because procfs does not +// implement mmap (if it was, the reading could be done in parallel chunks and +// processed in different goroutines). + +// Refresh refreshes the KernelSymbolTable, reading the symbols from /proc/kallsyms. +func (k *KernelSymbolTable) Refresh() error { + k.updateLock.Lock() + defer k.updateLock.Unlock() + + // re-initialize the maps to include all new symbols. + k.symbols = make(map[name][]*KernelSymbol) + k.addrs = make(map[addr][]*KernelSymbol) + k.symByName = make(map[nameAndOwner][]*KernelSymbol) + k.symByAddr = make(map[addrAndOwner][]*KernelSymbol) + + // Create the channels for the map update goroutines. + symbolChan := make(chan *KernelSymbol, chanBuffer) + addrChan := make(chan *KernelSymbol, chanBuffer) + symByNameChan := make(chan *KernelSymbol, chanBuffer) + symByAddrChan := make(chan *KernelSymbol, chanBuffer) + + k.updateWg = &sync.WaitGroup{} + k.updateWg.Add(4) + + // Start map update goroutines. + go k.updateSymbolMap(symbolChan) + go k.updateAddrsMap(addrChan) + go k.updateSymByNameMap(symByNameChan) + go k.updateSymByAddrMap(symByAddrChan) + + // Send kallsyms lines to the map update goroutines. + err := k.processLines([]chan *KernelSymbol{ + symbolChan, + addrChan, + symByNameChan, + symByAddrChan, + }) if err != nil { return err } - k.textSegStart = etext.Address - return nil -} -// lazyKernelSymbols -type lazyKernelSymbols struct { - fileContent []string - symbolMap map[string]*KernelSymbol - symbolAddrMap map[uint64]*KernelSymbol - textSegStart uint64 - textSegEnd uint64 -} + // Finally, wait for the map update goroutines to finish. + k.updateWg.Wait() -// NewLazyKernelSymbolsMap will return a lazy implementation of the KernelSymbolTable -// The lazy implementation keeps a copy of the /proc/kallsyms file content and queries that -// copy on demand, instead of preparsing it. -// It keeps caches of previously found results. -func NewLazyKernelSymbolsMap() (KernelSymbolTable, error) { - k := &lazyKernelSymbols{} - err := k.Refresh() - return k, err + // Get the kernel text segment addresses. + return k.getTextSegmentAddresses() } -func (k *lazyKernelSymbols) TextSegmentContains(addr uint64) (bool, error) { - // query the segments if not queried yet - if k.textSegEnd < k.textSegStart { - stext, err := k.GetSymbolByName("system", "_stext") - if err != nil { - return false, err - } - k.textSegStart = stext.Address - etext, err := k.GetSymbolByName("system", "_etext") - if err != nil { - return false, err - } - k.textSegEnd = etext.Address +// +// Private methods. +// + +// getTextSegmentAddresses gets the start and end addresses of the kernel text segment. +func (k *KernelSymbolTable) getTextSegmentAddresses() error { + stext, exist1 := k.symByName[nameAndOwner{"_stext", "system"}] + etext, exist2 := k.symByName[nameAndOwner{"_etext", "system"}] + + if !exist1 || !exist2 { + return fmt.Errorf("kernel text segment symbol(s) not found") } - return addr >= k.textSegStart && addr < k.textSegEnd, nil + + k.textSegStart = stext[0].Address + k.textSegEnd = etext[0].Address + + return nil } -func (k *lazyKernelSymbols) GetSymbolByName(owner string, name string) (*KernelSymbol, error) { - key := symbolKey(owner, name) - symbol, exist := k.symbolMap[key] - if exist { - return symbol, nil +// +// Concurrency logic for updating the maps +// + +// processLines process lines from kallsyms and sends them to map update goroutines. +func (k *KernelSymbolTable) processLines(chans []chan *KernelSymbol) error { + file, err := os.Open(kallsymsPath) + if err != nil { + return err } - for _, line := range k.fileContent { - line := strings.Fields(line) - // if the line is less than 3 words, we can't parse it (one or more fields missing) - // if the searched owner isn't system and the line counter is less than 4 words, the line is irrelevant - if len(line) < 3 || (owner != "system" && len(line) < 4) { + defer file.Close() + + // Send all lines to all channels. + scanner := bufio.NewScanner(file) + for scanner.Scan() { + fields := strings.Fields(scanner.Text()) + if len(fields) < 3 { continue } - symbolAddr, err := strconv.ParseUint(line[0], 16, 64) - if err != nil { - continue + if sym := parseLine(fields); sym != nil { + for _, ch := range chans { + ch <- sym + } } + } - symbolType, symbolName, symbolOwner := parseSymbolLine(line) - - if name == symbolName && owner == symbolOwner { - symbolKey := symbolKey(symbolOwner, symbolName) - symbol := &KernelSymbol{symbolName, symbolType, symbolAddr, symbolOwner} - k.symbolMap[symbolKey] = symbol - k.symbolAddrMap[symbolAddr] = symbol - return symbol, nil - } + // Close all channels. + for _, ch := range chans { + close(ch) } - return nil, SymbolNotFound(owner, name) + + return nil } -func (k *lazyKernelSymbols) GetSymbolByAddr(addr uint64) (*KernelSymbol, error) { - symbol, exist := k.symbolAddrMap[addr] - if exist { - return symbol, nil +// updateSymbolMap updates the symbols map arrived from the symbolChan. +func (k *KernelSymbolTable) updateSymbolMap(symbolChan chan *KernelSymbol) { + defer k.updateWg.Done() + + for sym := range symbolChan { + key := name{sym.Name} + k.symbols[key] = append(k.symbols[key], sym) } +} - var ( - symbolAddr uint64 - err error - ) - - fileLen := len(k.fileContent) - found := false - // kallsyms are almost sorted by address, start search with binary search - i := sort.Search(fileLen, func(i int) bool { - line := strings.Fields(k.fileContent[i]) - if len(line) < 3 { - return false - } - symbolAddr, err = strconv.ParseUint(line[0], 16, 64) - if err != nil { - return false - } - if symbolAddr == addr { - found = true +// updateAddrsMap updates the addrs map arrived from the addrChan. +func (k *KernelSymbolTable) updateAddrsMap(addrChan chan *KernelSymbol) { + defer k.updateWg.Done() - symbolType, symbolName, symbolOwner := parseSymbolLine(line) + for sym := range addrChan { + key := addr{sym.Address} + k.addrs[key] = append(k.addrs[key], sym) + } +} - symbolKey := symbolKey(symbolOwner, symbolName) - symbol = &KernelSymbol{symbolName, symbolType, symbolAddr, symbolOwner} - k.symbolMap[symbolKey] = symbol - k.symbolAddrMap[symbolAddr] = symbol - return true - } - return symbolAddr > addr - }) +// updateSymByNameMap updates the symByName map arrived from the symByNameChan. +func (k *KernelSymbolTable) updateSymByNameMap(symByNameChan chan *KernelSymbol) { + defer k.updateWg.Done() - if i < len(k.fileContent) && found { - return symbol, nil + for sym := range symByNameChan { + key := nameAndOwner{sym.Name, sym.Owner} + k.symByName[key] = append(k.symByName[key], sym) } +} - // symbols may be out of order near the end of the ksymbols, search linearly in reverse - for i := fileLen - 1; i > 0; i-- { - line := strings.Fields(k.fileContent[i]) - if len(line) < 3 { - continue - } - symbolAddr, err = strconv.ParseUint(line[0], 16, 64) - if err != nil { - continue - } - if symbolAddr == addr { - symbolType := strings.Clone(line[1]) - symbolName := strings.Clone(line[2]) - - symbolOwner := "system" - if len(line) > 3 { - // When a symbol is contained in a kernel module, it will be specified - // within square brackets, otherwise it's part of the system - symbolOwner = strings.Clone(line[3]) - symbolOwner = strings.TrimPrefix(symbolOwner, "[") - symbolOwner = strings.TrimSuffix(symbolOwner, "]") - } +// updateSymByAddrMap updates the symByAddr map arrived from the symByAddrChan. +func (k *KernelSymbolTable) updateSymByAddrMap(symByAddrChan chan *KernelSymbol) { + defer k.updateWg.Done() - symbolKey := symbolKey(symbolOwner, symbolName) - symbol := &KernelSymbol{symbolName, symbolType, symbolAddr, symbolOwner} - k.symbolMap[symbolKey] = symbol - k.symbolAddrMap[symbolAddr] = symbol - return symbol, nil - } + for sym := range symByAddrChan { + key := addrAndOwner{sym.Address, sym.Owner} + k.symByAddr[key] = append(k.symByAddr[key], sym) } - return nil, SymbolNotFoundAtAddress(addr) } -func (k *lazyKernelSymbols) Refresh() error { - file, err := os.ReadFile(kallsymsPath) +// +// Support functions. +// + +// parseLine parses a line from /proc/kallsyms and returns a KernelSymbol. +func parseLine(line []string) *KernelSymbol { + symbolAddr, err := strconv.ParseUint(line[0], 16, 64) if err != nil { - return fmt.Errorf("could not open /proc/kallsyms: %w", err) + return nil } - fileLines := strings.Split(string(file), "\n") - k.fileContent = fileLines - k.symbolMap = make(map[string]*KernelSymbol) - k.symbolAddrMap = make(map[uint64]*KernelSymbol) - k.textSegStart = math.MaxUint64 - k.textSegEnd = 0 - return nil -} + symbolType := line[1] + symbolName := line[2] -// kept for benchmarking purpose -func (k *lazyKernelSymbols) getSymbolByAddrNotBinary(addr uint64) (*KernelSymbol, error) { - symbol, exist := k.symbolAddrMap[addr] - if exist { - return symbol, nil + symbolOwner := "system" + if len(line) > 3 { + line[3] = strings.TrimPrefix(line[3], "[") + line[3] = strings.TrimSuffix(line[3], "]") + symbolOwner = line[3] } - for _, line := range k.fileContent { - line := strings.Fields(line) - // if the line is less than 3 words, we can't parse it (one or more fields missing) - if len(line) < 3 { - continue - } - symbolAddr, err := strconv.ParseUint(line[0], 16, 64) - if err != nil { - continue - } - if symbolAddr != addr { - continue - } - symbolType, symbolName, symbolOwner := parseSymbolLine(line) + return &KernelSymbol{ + Name: symbolName, + Type: symbolType, + Address: symbolAddr, + Owner: symbolOwner, + } +} - symbolKey := symbolKey(symbolOwner, symbolName) - symbol := &KernelSymbol{symbolName, symbolType, symbolAddr, symbolOwner} - k.symbolMap[symbolKey] = symbol - k.symbolAddrMap[symbolAddr] = symbol - return symbol, nil +func copySliceOfPointersToSliceOfStructs(s []*KernelSymbol) []KernelSymbol { + ret := make([]KernelSymbol, 0, len(s)) + for _, v := range s { + ret = append(ret, *v) } - return nil, SymbolNotFoundAtAddress(addr) + return ret } diff --git a/helpers/kernel_symbols_test.go b/helpers/kernel_symbols_test.go index d3a8288b..4f8e29fd 100644 --- a/helpers/kernel_symbols_test.go +++ b/helpers/kernel_symbols_test.go @@ -1,70 +1,45 @@ package helpers import ( - "math/rand" + "reflect" "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -// run tests as such -// alias mysudo='sudo -E env "PATH=$PATH"' -// cd helpers -// mysudo go test -func TestKernelSymbols(t *testing.T) { - const ( - owner = "system" - symbol = "do_sys_openat2" - ) - syms, err := NewKernelSymbolsMap() - require.NoError(t, err) - lSyms, err := NewLazyKernelSymbolsMap() - require.NoError(t, err) - res, err1 := syms.GetSymbolByName(owner, symbol) - res2, err2 := lSyms.GetSymbolByName(owner, symbol) - require.NoError(t, err1) - require.NoError(t, err2) - assert.Equal(t, res, res2) - addr := res.Address - res, err1 = syms.GetSymbolByAddr(addr) - res2, err2 = lSyms.GetSymbolByAddr(addr) - require.NoError(t, err1) - require.NoError(t, err2) - assert.Equal(t, res, res2) +// TestParseLine tests the parseLine function. +func TestParseLine(t *testing.T) { + testCases := []struct { + line []string + expected *KernelSymbol + }{ + {[]string{"00000000", "t", "my_symbol", "[my_owner]"}, &KernelSymbol{Name: "my_symbol", Type: "t", Address: 0, Owner: "my_owner"}}, + // Add more test cases as needed. + } + + for _, tc := range testCases { + result := parseLine(tc.line) + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("parseLine(%v) = %v; want %v", tc.line, result, tc.expected) + } + } } -// run benchmarks as such -// alias mysudo='sudo -E env "PATH=$PATH"' -// cd helpers -// mysudo go test -bench=. +func TestRefresh(t *testing.T) { + kst, err := NewKernelSymbolTable() + if err != nil { + t.Fatalf("NewKernelSymbolTable() failed: %v", err) + } -const ( - max = 812435456 - start = 0xffffffff90000000 - end = 0xffffffffc06cc738 -) + // Test well-known symbols like _stext and _etext. + symbolsToTest := []string{"_stext", "_etext"} -func BenchmarkLazySymByAddr(b *testing.B) { - rand.Seed(time.Now().UnixNano()) - seed := uint64(rand.Intn(max)) - addr := seed + uint64(start) - syms, err := NewLazyKernelSymbolsMap() - require.NoError(b, err) - for i := 0; i < b.N; i++ { - syms.GetSymbolByAddr(addr) + for _, symbol := range symbolsToTest { + if syms, err := kst.GetSymbolByName(symbol); err != nil || len(syms) == 0 { + t.Errorf("Expected to find symbol %s, but it was not found", symbol) + } } -} -func BenchmarkLazySymByAddrNotBinary(b *testing.B) { - rand.Seed(time.Now().UnixNano()) - seed := uint64(rand.Intn(max)) - addr := seed + uint64(start) - syms, err := NewLazyKernelSymbolsMap() - lSyms := syms.(*lazyKernelSymbols) - require.NoError(b, err) - for i := 0; i < b.N; i++ { - lSyms.getSymbolByAddrNotBinary(addr) + // Text the text swegment contains function. + if _, err := kst.TextSegmentContains(0); err != nil { + t.Errorf("TextSegmentContains failed: %v", err) } } diff --git a/libbpfgo.c b/libbpfgo.c index 67cd88eb..e7dd5e1c 100644 --- a/libbpfgo.c +++ b/libbpfgo.c @@ -300,6 +300,30 @@ void cgo_bpf_tc_hook_free(struct bpf_tc_hook *hook) free(hook); } +struct bpf_kprobe_opts *cgo_bpf_kprobe_opts_new(__u64 bpf_cookie, + size_t offset, + bool retprobe, + int attach_mode) +{ + struct bpf_kprobe_opts *opts; + opts = calloc(1, sizeof(*opts)); + if (!opts) + return NULL; + + opts->sz = sizeof(*opts); + opts->bpf_cookie = bpf_cookie; + opts->offset = offset; + opts->retprobe = retprobe; + opts->attach_mode = attach_mode; + + return opts; +} + +void cgo_bpf_kprobe_opts_free(struct bpf_kprobe_opts *opts) +{ + free(opts); +} + // // struct getters // diff --git a/libbpfgo.h b/libbpfgo.h index b6feba26..44e22b2e 100644 --- a/libbpfgo.h +++ b/libbpfgo.h @@ -71,6 +71,12 @@ void cgo_bpf_tc_opts_free(struct bpf_tc_opts *opts); struct bpf_tc_hook *cgo_bpf_tc_hook_new(); void cgo_bpf_tc_hook_free(struct bpf_tc_hook *hook); +struct bpf_kprobe_opts *cgo_bpf_kprobe_opts_new(__u64 bpf_cookie, + size_t offset, + bool retprobe, + int attach_mode); +void cgo_bpf_kprobe_opts_free(struct bpf_kprobe_opts *opts); + // // struct getters // diff --git a/prog.go b/prog.go index 9d78eb06..3a0bf5d0 100644 --- a/prog.go +++ b/prog.go @@ -370,41 +370,101 @@ func (p *BPFProg) AttachPerfEvent(fd int) (*BPFLink, error) { return bpfLink, nil } -// this API should be used for kernels > 4.17 -func (p *BPFProg) AttachKprobe(kp string) (*BPFLink, error) { - return doAttachKprobe(p, kp, false) -} +// +// Kprobe and Kretprobe +// -// this API should be used for kernels > 4.17 -func (p *BPFProg) AttachKretprobe(kp string) (*BPFLink, error) { - return doAttachKprobe(p, kp, true) +type attachTo struct { + symName string + symAddr uint64 + isRet bool } -func doAttachKprobe(prog *BPFProg, kp string, isKretprobe bool) (*BPFLink, error) { - kpC := C.CString(kp) - defer C.free(unsafe.Pointer(kpC)) +// attachKprobeCommon is a common function for attaching kprobe and kretprobe. +func (p *BPFProg) attachKprobeCommon(a attachTo) (*BPFLink, error) { + // Create kprobe_opts. + optsC, errno := C.cgo_bpf_kprobe_opts_new( + C.ulonglong(0), // bpf cookie (not used) + C.size_t(a.symAddr), // might be 0 if attaching using symbol name + C.bool(a.isRet), // is kretprobe or kprobe + C.int(0), // attach mode (default) + ) + if optsC == nil { + return nil, fmt.Errorf("failed to create kprobe_opts of %v: %v", a, errno) + } + defer C.cgo_bpf_kprobe_opts_free(optsC) + + // Create kprobe symbol name. + symNameC := C.CString(a.symName) + defer C.free(unsafe.Pointer(symNameC)) - linkC, errno := C.bpf_program__attach_kprobe(prog.prog, C.bool(isKretprobe), kpC) + // Create kprobe link. + var linkC *C.struct_bpf_link + linkC, errno = C.bpf_program__attach_kprobe_opts(p.prog, symNameC, optsC) if linkC == nil { - return nil, fmt.Errorf("failed to attach %s k(ret)probe to program %s: %w", kp, prog.Name(), errno) + return nil, fmt.Errorf("failed to attach to %v: %v", a, errno) } - kpType := Kprobe - if isKretprobe { - kpType = Kretprobe + linkType := Kprobe + if a.isRet { + linkType = Kretprobe } + eventName := a.symName + if eventName == "" { + eventName = fmt.Sprintf("%d", a.symAddr) + } + + // Create bpfLink and append it to the module. bpfLink := &BPFLink{ - link: linkC, - prog: prog, - linkType: kpType, - eventName: kp, + link: linkC, // linkC is a pointer to a struct bpf_link + prog: p, // p is a pointer to the related BPFProg + linkType: linkType, // linkType is a BPFLinkType + eventName: eventName, // eventName is a string } - prog.module.links = append(prog.module.links, bpfLink) + p.module.links = append(p.module.links, bpfLink) return bpfLink, nil } +// AttachKprobe attaches the BPFProgram to the given symbol name. +func (p *BPFProg) AttachKprobe(symbol string) (*BPFLink, error) { + a := attachTo{ + symName: symbol, + isRet: false, + } + return p.attachKprobeCommon(a) +} + +// AttachKretprobe attaches the BPFProgram to the given symbol name (for return). +func (p *BPFProg) AttachKretprobe(symbol string) (*BPFLink, error) { + a := attachTo{ + symName: symbol, + isRet: true, + } + return p.attachKprobeCommon(a) +} + +// AttachKprobeOnOffset attaches the BPFProgram to the given offset. +func (p *BPFProg) AttachKprobeOffset(offset uint64) (*BPFLink, error) { + a := attachTo{ + symAddr: offset, + isRet: false, + } + return p.attachKprobeCommon(a) +} + +// AttachKretprobeOnOffset attaches the BPFProgram to the given offset (for return). +func (p *BPFProg) AttachKretprobeOnOffset(offset uint64) (*BPFLink, error) { + a := attachTo{ + symAddr: offset, + isRet: true, + } + return p.attachKprobeCommon(a) +} + +// End of Kprobe and Kretprobe + func (p *BPFProg) AttachNetns(networkNamespacePath string) (*BPFLink, error) { fd, err := syscall.Open(networkNamespacePath, syscall.O_RDONLY, 0) if fd < 0 { diff --git a/selftest/tracing-by-offset/Makefile b/selftest/tracing-by-offset/Makefile new file mode 120000 index 00000000..d981720c --- /dev/null +++ b/selftest/tracing-by-offset/Makefile @@ -0,0 +1 @@ +../common/Makefile \ No newline at end of file diff --git a/selftest/tracing-by-offset/go.mod b/selftest/tracing-by-offset/go.mod new file mode 100644 index 00000000..7d572001 --- /dev/null +++ b/selftest/tracing-by-offset/go.mod @@ -0,0 +1,14 @@ +module github.com/aquasecurity/libbpfgo/selftest/tracing-by-offset + +go 1.18 + +require ( + github.com/aquasecurity/libbpfgo v0.4.7-libbpf-1.2.0-b2e29a1 + github.com/aquasecurity/libbpfgo/helpers v0.4.5 +) + +require golang.org/x/sys v0.7.0 // indirect + +replace github.com/aquasecurity/libbpfgo => ../../ + +replace github.com/aquasecurity/libbpfgo/helpers => ../../helpers diff --git a/selftest/tracing-by-offset/go.sum b/selftest/tracing-by-offset/go.sum new file mode 100644 index 00000000..70691e6f --- /dev/null +++ b/selftest/tracing-by-offset/go.sum @@ -0,0 +1,6 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/selftest/tracing-by-offset/main.bpf.c b/selftest/tracing-by-offset/main.bpf.c new file mode 100644 index 00000000..abde44f8 --- /dev/null +++ b/selftest/tracing-by-offset/main.bpf.c @@ -0,0 +1,14 @@ +//+build ignore + +#include + +#include + +SEC("kprobe/sys_mmap") +int kprobe__sys_mmap(struct pt_regs *ctx) +{ + bpf_printk("Hello, World!\n"); + return 0; +} + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; diff --git a/selftest/tracing-by-offset/main.go b/selftest/tracing-by-offset/main.go new file mode 100644 index 00000000..33133dcb --- /dev/null +++ b/selftest/tracing-by-offset/main.go @@ -0,0 +1,70 @@ +package main + +import "C" + +import ( + "os" + "runtime" + "time" + + "fmt" + "syscall" + + bpf "github.com/aquasecurity/libbpfgo" + "github.com/aquasecurity/libbpfgo/helpers" +) + +func main() { + funcName := fmt.Sprintf("__%s_sys_mmap", ksymArch()) + + kst, err := helpers.NewKernelSymbolTable() + if err != nil { + fmt.Fprintln(os.Stderr, "NewKernelSymbolTable() failed: %v", err) + os.Exit(-1) + } + + funcSymbol, err := kst.GetSymbolByName(funcName) + if err != nil { + fmt.Fprintln(os.Stderr, "Expected to find symbol %s, but it was not found", funcSymbol) + os.Exit(-1) + } + + bpfModule, err := bpf.NewModuleFromFile("main.bpf.o") + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } + defer bpfModule.Close() + + bpfModule.BPFLoadObject() + prog, err := bpfModule.GetProgram("kprobe__sys_mmap") + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } + + _, err = prog.AttachKprobeOffset(funcSymbol[0].Address) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } + + go func() { + time.Sleep(time.Second) + syscall.Mmap(999, 999, 999, 1, 1) + syscall.Mmap(999, 999, 999, 1, 1) + }() + + time.Sleep(time.Second * 2) +} + +func ksymArch() string { + switch runtime.GOARCH { + case "amd64": + return "x64" + case "arm64": + return "arm64" + default: + panic("unsupported architecture") + } +} diff --git a/selftest/tracing-by-offset/run.sh b/selftest/tracing-by-offset/run.sh new file mode 120000 index 00000000..aee911b2 --- /dev/null +++ b/selftest/tracing-by-offset/run.sh @@ -0,0 +1 @@ +../common/run.sh \ No newline at end of file diff --git a/selftest/tracing/main.go b/selftest/tracing/main.go index 905c7fb5..652e520a 100644 --- a/selftest/tracing/main.go +++ b/selftest/tracing/main.go @@ -29,20 +29,20 @@ func main() { os.Exit(-1) } - m, err := helpers.NewKernelSymbolsMap() + m, err := helpers.NewKernelSymbolTable() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(-1) } funcName := fmt.Sprintf("__%s_sys_mmap", ksymArch()) - sym, err := m.GetSymbolByName("system", funcName) + sym, err := m.GetSymbolByName(funcName) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(-1) } - if sym.Address == 0 || sym.Name == "" { + if sym[0].Address == 0 && sym[0].Name == "" { fmt.Fprintln(os.Stderr, "could not find symbol to attach to") os.Exit(-1) }