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

feat(ksymbols): reintroduce lazy symbol queries #437

Closed
Closed
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
176 changes: 137 additions & 39 deletions helpers/kernel_symbols.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,9 @@ func symNotFoundErr(v interface{}) error {
// Interface implementation
//

type name struct {
name string
}
type name string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call.


type addr struct {
addr uint64
}
type addr uint64

type nameAndOwner struct {
name string
Expand All @@ -48,41 +44,79 @@ type addrAndOwner struct {
}

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
symbols map[name][]*KernelSymbol
addrs map[addr][]*KernelSymbol
requiredSyms map[string]struct{}
requiredAddrs map[uint64]struct{}
onlyRequired bool
symByName map[nameAndOwner][]*KernelSymbol
symByAddr map[addrAndOwner][]*KernelSymbol
updateLock *sync.Mutex
updateWg *sync.WaitGroup
}

func NewKernelSymbolTable() (*KernelSymbolTable, error) {
func NewKernelSymbolTable(opts ...KSymbTableOption) (*KernelSymbolTable, error) {
k := KernelSymbolTable{
updateLock: &sync.RWMutex{},
updateLock: &sync.Mutex{},
}
for _, opt := range opts {
err := opt(&k)
if err != nil {
return nil, err
}
}

k.onlyRequired = k.requiredAddrs != nil || k.requiredSyms != nil

return &k, k.Refresh()
}

type KSymbTableOption func(k *KernelSymbolTable) error

func WithRequiredSymbols(reqSyms []string) KSymbTableOption {
return func(k *KernelSymbolTable) error {
k.requiredSyms = sliceToValidationMap(reqSyms)
return nil
}
}

func WithRequiredAddresses(reqAddrs []uint64) KSymbTableOption {
return func(k *KernelSymbolTable) error {
k.requiredAddrs = sliceToValidationMap(reqAddrs)
return nil
}
}

//
// 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()
k.updateLock.Lock()
defer k.updateLock.Unlock()
Copy link
Member

@geyslan geyslan May 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the goal is to squeeze as much as possible (optimise wise), what do you think unlocking right after the getTextSegmentAddresses() call?


segStart, segEnd, err := k.getTextSegmentAddresses()
if err != nil {
return false, err
}

return addr >= k.textSegStart && addr < k.textSegEnd, nil
return addr >= segStart && addr < segEnd, nil
}

// GetSymbolByName returns all the symbols with the given name.
// NOTE: If table is in required only mode, method will add the new symbol
// and rescan the file.
func (k *KernelSymbolTable) GetSymbolByName(n string) ([]KernelSymbol, error) {
k.updateLock.RLock()
defer k.updateLock.RUnlock()
k.updateLock.Lock()
defer k.updateLock.Unlock()

symbols, exist := k.symbols[name{n}]
err := k.validateOrAddRequiredSym(name(n))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about returning symbols and exist from validateOrAddRequiredSym, so we could release the lock right after it returns checking them later.

if err != nil {
return []KernelSymbol{}, nil
}

symbols, exist := k.symbols[name(n)]
if !exist {
return []KernelSymbol{}, symNotFoundErr(n)
}
Expand All @@ -91,11 +125,18 @@ func (k *KernelSymbolTable) GetSymbolByName(n string) ([]KernelSymbol, error) {
}

// GetSymbolByAddr returns all the symbols with the given address.
// NOTE: If table is in required only mode, method will add the new symbol
// and rescan the file.
func (k *KernelSymbolTable) GetSymbolByAddr(a uint64) ([]KernelSymbol, error) {
k.updateLock.RLock()
defer k.updateLock.RUnlock()
k.updateLock.Lock()
defer k.updateLock.Unlock()

err := k.validateOrAddRequiredAddr(addr(a))
if err != nil {
return []KernelSymbol{}, err
}

symbols, exist := k.addrs[addr{a}]
symbols, exist := k.addrs[addr(a)]
if !exist {
return []KernelSymbol{}, symNotFoundErr(a)
}
Expand All @@ -105,8 +146,13 @@ func (k *KernelSymbolTable) GetSymbolByAddr(a uint64) ([]KernelSymbol, error) {

// 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()
k.updateLock.Lock()
defer k.updateLock.Unlock()

err := k.validateOrAddRequiredSym(name(n))
if err != nil {
return []KernelSymbol{}, err
}

symbols, exist := k.symByName[nameAndOwner{n, o}]
if !exist {
Expand All @@ -118,8 +164,13 @@ func (k *KernelSymbolTable) GetSymbolByOwnerAndName(o, n string) ([]KernelSymbol

// 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()
k.updateLock.Lock()
defer k.updateLock.Unlock()

err := k.validateOrAddRequiredAddr(addr(a))
if err != nil {
return []KernelSymbol{}, err
}

symbols, exist := k.symByAddr[addrAndOwner{a, o}]
if !exist {
Expand All @@ -144,8 +195,11 @@ func (k *KernelSymbolTable) GetSymbolByOwnerAndAddr(o string, a uint64) ([]Kerne

// Refresh refreshes the KernelSymbolTable, reading the symbols from /proc/kallsyms.
func (k *KernelSymbolTable) Refresh() error {
k.updateLock.Lock()
defer k.updateLock.Unlock()
// Refresh may be called internally, after lock was already takne
needUnlock := k.updateLock.TryLock()
if needUnlock {
defer k.updateLock.Unlock()
}

// re-initialize the maps to include all new symbols.
k.symbols = make(map[name][]*KernelSymbol)
Expand Down Expand Up @@ -182,25 +236,54 @@ func (k *KernelSymbolTable) Refresh() error {
// Finally, wait for the map update goroutines to finish.
k.updateWg.Wait()

// Get the kernel text segment addresses.
return k.getTextSegmentAddresses()
return nil
}

//
// Private methods.
//

// getTextSegmentAddresses gets the start and end addresses of the kernel text segment.
func (k *KernelSymbolTable) getTextSegmentAddresses() error {
func (k *KernelSymbolTable) getTextSegmentAddresses() (uint64, uint64, 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 0, 0, fmt.Errorf("kernel text segment symbol(s) not found")
}

k.textSegStart = stext[0].Address
k.textSegEnd = etext[0].Address
textSegStart := stext[0].Address
textSegEnd := etext[0].Address

return textSegStart, textSegEnd, nil
}

// validateOrAddRequiredSym checks if the given symbol is in the required list.
// If not, it adds it and rescans the kernel symbols.
func (k *KernelSymbolTable) validateOrAddRequiredSym(sym name) error {
if k.onlyRequired && k.requiredSyms != nil {
sym := string(sym)
if _, ok := k.requiredSyms[sym]; !ok {
k.requiredSyms[sym] = struct{}{}
err := k.Refresh()
return err
}
}

return nil
}

// validateOrAddRequiredAddr checks if the given address is in the required list.
// If not, it adds it and rescans the kernel symbols.
func (k *KernelSymbolTable) validateOrAddRequiredAddr(addr addr) error {
if k.onlyRequired && k.requiredAddrs != nil {
addr := uint64(addr)
if _, ok := k.requiredAddrs[addr]; !ok {
k.requiredAddrs[addr] = struct{}{}
err := k.Refresh()
return err
}
}

return nil
}
Expand All @@ -225,6 +308,13 @@ func (k *KernelSymbolTable) processLines(chans []chan *KernelSymbol) error {
continue
}
if sym := parseLine(fields); sym != nil {
if k.onlyRequired {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we assume at this stage that if !k.onlyRequired we should continue?

_, symRequired := k.requiredSyms[sym.Name]
_, addrRequired := k.requiredAddrs[sym.Address]
if !symRequired && !addrRequired {
continue
}
}
for _, ch := range chans {
ch <- sym
}
Expand All @@ -244,7 +334,7 @@ func (k *KernelSymbolTable) updateSymbolMap(symbolChan chan *KernelSymbol) {
defer k.updateWg.Done()

for sym := range symbolChan {
key := name{sym.Name}
key := name(sym.Name)
k.symbols[key] = append(k.symbols[key], sym)
}
}
Expand All @@ -254,7 +344,7 @@ func (k *KernelSymbolTable) updateAddrsMap(addrChan chan *KernelSymbol) {
defer k.updateWg.Done()

for sym := range addrChan {
key := addr{sym.Address}
key := addr(sym.Address)
k.addrs[key] = append(k.addrs[key], sym)
}
}
Expand Down Expand Up @@ -315,3 +405,11 @@ func copySliceOfPointersToSliceOfStructs(s []*KernelSymbol) []KernelSymbol {
}
return ret
}

func sliceToValidationMap[T comparable](items []T) map[T]struct{} {
res := make(map[T]struct{})
for _, item := range items {
res[item] = struct{}{}
}
return res
}