Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy kernel symbols #282

Merged
merged 3 commits into from
Dec 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ helpers-test-static-run: libbpfgo-static
CC=$(CLANG) \
CGO_CFLAGS=$(CGO_CFLAGS_STATIC) \
CGO_LDFLAGS=$(CGO_LDFLAGS_STATIC) \
sudo -E $(GO) test -v $(HELPERS)/...
sudo -E env PATH=$(PATH) $(GO) test -v $(HELPERS)/...

helpers-test-dynamic-run: libbpfgo-dynamic
sudo $(GO) test -v $(HELPERS)/...
Expand Down
1 change: 1 addition & 0 deletions helpers/elf.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func SymbolToOffset(path, symbol string) (uint32, error) {
if err != nil {
return 0, fmt.Errorf("could not open elf file to resolve symbol offset: %w", err)
}
defer f.Close()

regularSymbols, regularSymbolsErr := f.Symbols()
dynamicSymbols, dynamicSymbolsErr := f.DynamicSymbols()
Expand Down
297 changes: 252 additions & 45 deletions helpers/kernel_symbols.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,31 @@ package helpers

import (
"bufio"
"errors"
"fmt"
"math"
"os"
"sort"
"strconv"
"strings"
)

/*
* The helpers in this file gives the ability to hold all the known kernel symbols.
* the package parse the /proc/kallsyms file that hold the known kernel symbol
*
* The KernelSymbolTable type holds map of all the kernel symbols with a key which is the kernel object owner and the name with under-case between them
* which means that symbolMap looks like [objectOwner_objectname{SymbolData}, objectOwner_objectname{SymbolData}, etc...]
* the key naming is because sometimes kernel symbols can have the same name or the same address which prevents to key the map with only one of them
* 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.
*/

type KernelSymbolTable struct {
symbolMap map[string]*KernelSymbol
symbolAddrMap map[uint64]*KernelSymbol
initialized bool
const (
kallsymsPath = "/proc/kallsyms"
)

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 {
Expand All @@ -32,17 +36,72 @@ type KernelSymbol struct {
Owner string
}

/* NewKernelSymbolsMap initiates the 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)
// errors

func SymbolNotFound(owner, name string) error {
return fmt.Errorf("symbol not found: %s_%s", owner, name)
}
func SymbolNotFoundAtAddress(addr uint64) error {
return fmt.Errorf("symbol not found at address: 0x%x", addr)
}

// general

func symbolKey(owner, name string) string {
return owner + "_" + name
}

// fullKernelSymbolTable

type fullKernelSymbolTable struct {
symbolMap map[string]*KernelSymbol
symbolAddrMap map[uint64]*KernelSymbol
textSegStart uint64
textSegEnd uint64
}

/* 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) {
var KernelSymbols = KernelSymbolTable{}
KernelSymbols.symbolMap = make(map[string]*KernelSymbol)
KernelSymbols.symbolAddrMap = make(map[uint64]*KernelSymbol)
file, err := os.Open("/proc/kallsyms")
func NewKernelSymbolsMap() (KernelSymbolTable, error) {
k := fullKernelSymbolTable{}
err := k.Refresh()

return &k, err
}

// 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
}

// 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
}
return nil, SymbolNotFound(owner, name)
}

// 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
}
return nil, SymbolNotFoundAtAddress(addr)
}

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 nil, fmt.Errorf("could not open /proc/kallsyms: %w", err)
return fmt.Errorf("could not open /proc/kallsyms: %w", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
Expand Down Expand Up @@ -71,51 +130,199 @@ func NewKernelSymbolsMap() (*KernelSymbolTable, error) {

symbolKey := symbolOwner + "_" + symbolName
symbol := &KernelSymbol{symbolName, symbolType, symbolAddr, symbolOwner}
KernelSymbols.symbolMap[symbolKey] = symbol
KernelSymbols.symbolAddrMap[symbolAddr] = symbol
}
KernelSymbols.initialized = true
return &KernelSymbols, nil
}

// TextSegmentContains checks if a given address is in the kernel text segment
// by comparing it to the kernel text segment address boundaries
func (k *KernelSymbolTable) TextSegmentContains(addr uint64) (bool, error) {
if !k.initialized {
return false, errors.New("kernel symbols map isnt initialized")
k.symbolMap[symbolKey] = symbol
k.symbolAddrMap[symbolAddr] = symbol
}
stext, err := k.GetSymbolByName("system", "_stext")
if err != nil {
return false, err
return err
}
k.textSegStart = stext.Address
etext, err := k.GetSymbolByName("system", "_etext")
if err != nil {
return false, err
return err
}
return ((addr >= stext.Address) && (addr < etext.Address)), nil
k.textSegStart = etext.Address
return nil
}

// GetSymbolByName returns a symbol by a given name and owner
func (k *KernelSymbolTable) GetSymbolByName(owner string, name string) (*KernelSymbol, error) {
if !k.initialized {
return nil, errors.New("kernel symbols map isnt initialized")
// lazyKernelSymbols
type lazyKernelSymbols struct {
fileContent []string
symbolMap map[string]*KernelSymbol
symbolAddrMap map[uint64]*KernelSymbol
textSegStart uint64
textSegEnd uint64
}

// 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
}

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
}
key := fmt.Sprintf("%s_%s", owner, name)
return addr >= k.textSegStart && addr < k.textSegEnd, 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
}
return nil, fmt.Errorf("symbol not found: %s_%s", owner, name)
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) {
continue
}
symbolAddr, err := strconv.ParseUint(line[0], 16, 64)
if err != nil {
continue
}
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, "]")
}

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
}
}
return nil, SymbolNotFound(owner, name)
}

func (k *lazyKernelSymbols) GetSymbolByAddr(addr uint64) (*KernelSymbol, error) {
symbol, exist := k.symbolAddrMap[addr]
if exist {
return symbol, nil
}

var (
symbolAddr uint64
err error
)

// since kallsyms is sorted in address ascending order, use binary search
i := sort.Search(len(k.fileContent), 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 {
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, "]")
}

symbolKey := symbolKey(symbolOwner, symbolName)
symbol = &KernelSymbol{symbolName, symbolType, symbolAddr, symbolOwner}
k.symbolMap[symbolKey] = symbol
k.symbolAddrMap[symbolAddr] = symbol
return true
}
return symbolAddr > addr
})

if i < len(k.fileContent) && symbolAddr == addr {
return symbol, nil
} else {
return nil, SymbolNotFoundAtAddress(addr)
}
}

// GetSymbolByAddr returns a symbol by a given address
func (k *KernelSymbolTable) GetSymbolByAddr(addr uint64) (*KernelSymbol, error) {
if !k.initialized {
return nil, errors.New("kernel symbols map isnt initialized")
func (k *lazyKernelSymbols) Refresh() error {
file, err := os.ReadFile(kallsymsPath)
if err != nil {
return fmt.Errorf("could not open /proc/kallsyms: %w", err)
}
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
}

// kept for benchmarking purpose
func (k *lazyKernelSymbols) getSymbolByAddrNotBinary(addr uint64) (*KernelSymbol, error) {
symbol, exist := k.symbolAddrMap[addr]
if exist {
return symbol, nil
}
return nil, fmt.Errorf("symbol not found at address: 0x%x", addr)
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 := 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, "]")
}

symbolKey := symbolKey(symbolOwner, symbolName)
symbol := &KernelSymbol{symbolName, symbolType, symbolAddr, symbolOwner}
k.symbolMap[symbolKey] = symbol
k.symbolAddrMap[symbolAddr] = symbol
return symbol, nil
}
return nil, SymbolNotFoundAtAddress(addr)
}
Loading