Skip to content

Commit

Permalink
link: lazy load executable symbol table
Browse files Browse the repository at this point in the history
Certain situations require the user to provide symbol addresses to the library. This is why we already support passing arbitrary address+offset pairs when placing uprobes. A real life example of this is problem is probing golang processes - since golang has known issues with uretprobes, the current popular replacement to a uretprobe is manually finding the return addresses of the probed function and applying uprobes to every single return instruction by explicitly providing address+offset.

This makes the loading of symbols uneeded to begin with. The reason this is even an issue is that symbol tables tend to be large, especially when analyzing golang processes.

Load the symbol table lazily instead of when creating `Executable`. There is a risk that the binary may change between the call to `OpenExecutable` and reading the symbols table. This is deemed acceptable, since the underlying tracefs / perf API is susceptible to the same problem.

Fixes #987

Signed-off-by: Mattia Meleleo <[email protected]>
  • Loading branch information
mmat11 authored Mar 30, 2023
1 parent 1c5fc27 commit a056764
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 16 deletions.
40 changes: 24 additions & 16 deletions link/uprobe.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"strings"
"sync"

"github.com/cilium/ebpf"
"github.com/cilium/ebpf/internal"
Expand Down Expand Up @@ -34,6 +35,8 @@ type Executable struct {
path string
// Parsed ELF and dynamic symbols' addresses.
addresses map[string]uint64
// Keep track of symbol table lazy load.
addressesOnce sync.Once
}

// UprobeOptions defines additional parameters that will be used
Expand Down Expand Up @@ -83,32 +86,21 @@ func OpenExecutable(path string) (*Executable, error) {
return nil, fmt.Errorf("path cannot be empty")
}

f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open file '%s': %w", path, err)
}
defer f.Close()

se, err := internal.NewSafeELFFile(f)
f, err := internal.OpenSafeELFFile(path)
if err != nil {
return nil, fmt.Errorf("parse ELF file: %w", err)
}
defer f.Close()

if se.Type != elf.ET_EXEC && se.Type != elf.ET_DYN {
if f.Type != elf.ET_EXEC && f.Type != elf.ET_DYN {
// ELF is not an executable or a shared object.
return nil, errors.New("the given file is not an executable or a shared object")
}

ex := Executable{
return &Executable{
path: path,
addresses: make(map[string]uint64),
}

if err := ex.load(se); err != nil {
return nil, err
}

return &ex, nil
}, nil
}

func (ex *Executable) load(f *internal.SafeELFFile) error {
Expand Down Expand Up @@ -165,6 +157,22 @@ func (ex *Executable) address(symbol string, opts *UprobeOptions) (uint64, error
return opts.Address + opts.Offset, nil
}

var err error
ex.addressesOnce.Do(func() {
var f *internal.SafeELFFile
f, err = internal.OpenSafeELFFile(ex.path)
if err != nil {
err = fmt.Errorf("parse ELF file: %w", err)
return
}
defer f.Close()

err = ex.load(f)
})
if err != nil {
return 0, fmt.Errorf("lazy load symbols: %w", err)
}

address, ok := ex.addresses[symbol]
if !ok {
return 0, fmt.Errorf("symbol %s: %w", symbol, ErrNoSymbol)
Expand Down
24 changes: 24 additions & 0 deletions link/uprobe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,30 @@ func TestExecutableOffset(t *testing.T) {
c.Assert(offset, qt.Equals, uint64(0x1+0x2))
}

func TestExecutableLazyLoadSymbols(t *testing.T) {
c := qt.New(t)

ex, err := OpenExecutable("/bin/bash")
c.Assert(err, qt.IsNil)
// Addresses must be empty, will be lazy loaded.
c.Assert(ex.addresses, qt.DeepEquals, map[string]uint64{})

prog := mustLoadProgram(t, ebpf.Kprobe, 0, "")
up, err := ex.Uprobe(bashSym, prog, &UprobeOptions{Address: 123})
c.Assert(err, qt.IsNil)
up.Close()

// Addresses must still be empty as Address has been provided via options.
c.Assert(ex.addresses, qt.DeepEquals, map[string]uint64{})

up, err = ex.Uprobe(bashSym, prog, nil)
c.Assert(err, qt.IsNil)
up.Close()

// Symbol table should be loaded.
c.Assert(len(ex.addresses), qt.Not(qt.Equals), 0)
}

func TestUprobe(t *testing.T) {
c := qt.New(t)

Expand Down

0 comments on commit a056764

Please sign in to comment.