Instead of using cgo we call the syscall for BPF directly from go. The
API hasn't changed, however, and we also closely follow the
C-implementation as given in bpf(2).
Not sure if this pure go variant is beneficial. Manual maintenance of
all constants and structs upon changes of the BPF API would be necessary
and cumbersome.
We would at least need to complement this with auto-generation of
constants and fields from /usr/include/linux/bpf.h.
1. symbolyze.Scanner now implements the RunEvery(time.Duration) method
that will call scanner.Run() periodically. It should be called to
run in its own goroutine. The loop can be stopped by calling
scanner.Stop().
2. The scanner now collects all errors from all observers in a private
error type `errors []error`. It's Error() returns a cumulated list of
errors, seperated by a newline.
We now call wg.Add(n) before we loop over n observers. We also run the
loop inside a goroutine and therefore more quickly handle all (pid,
offset) pairs.
gdb_test.go and testdata implement a go test to compare and verify that
we get correct results from the scanner.
testdata/simple.c is a small program that embeds Python and runs the
same code as in runforever.py. The Makefile compiles this using
python3.7. Adjustments to the flags might be needed in your environment
gdb_test.go contains only one test, TestSimpleGDB, that
1. compiles simple.c
2. runs it multiple times
3. calls gdb to extract the address of symbol _PyRuntime for each pid
4. runs our Scanner
5. compares the results from gdb and our scanner
By introducing a lowercase type alias 'logger' for *log.Logger we can
now embed 'logger' in Scanner and not export it:
% go doc Scanner
package symbolyze // import "."
type Scanner struct {
// Has unexported fields.
}
symbolyze.go has been simplified and cleaned up. It now also is documented,
f.e.:
% go doc Scanner
package symbolyze // import "."
type Scanner struct {
*log.Logger // Embedded logger
// Has unexported fields.
}
Scanner represents an engine for scanning for a specific symbol in all
ELF-files matching a certain pattern. The pattern is described in
fileapth.Match().
Once a Scanner is created with New(), it should be populated with Observer
functions using OnFound(). Optionally, the scanner can be put into debugging
mode by a call to DebugOn() prior to a call to Run().
A call to Scanner.Run() then starts the engine and it will scan all pids in
/proc. Whenever a match is found, all observers will be called with the
(pid, offset), concurrently.
func New(symbol, pathglob string) *Scanner
func (S *Scanner) DebugOn()
func (S *Scanner) OnFound(fun Observer)
func (S *Scanner) Run() error
symbolyze/ now contains a module that exposes a Finder type with a
simple API, like:
finder := symbolyze.New("_PyRuntime", "*python3*")
finder.Debug(true)
finder.OnFound(mapFD.Set)
finder.Run()
Instead of writing (pid, offset) directly to a eBPF-map, it implements
an observer-pattern and expects a callback.
TODOs/next steps:
- Write documentation
- Add tests
- Experiment and re-evaluate design
main.go:
- reading /proc
- iteration over entries in NNN/maps
- filter glob-search for "*python3*" in pathname
- find symbol and its offset in pathnanme
- calculate offset in memory
- add pid and offset to map
TODO: encapsulating this into a module
ebpf.go:
- added type MapFD int, changing all function on a FD to methods
This allows us to enrich the data type going forward
- added bpf_update_elem() from the manpage ebpf2.
.updateElement() is the verbatim wrapper to it.
- added .Add/.Change/.Set methods, which call .updateElement
with specific flags
TODO: re-implement ebpf.go with pure go, using direct syscalls.