diff --git a/GetRuntimeAddresses/main.go b/GetRuntimeAddresses/main.go index 3a4eddc..c13cdb7 100644 --- a/GetRuntimeAddresses/main.go +++ b/GetRuntimeAddresses/main.go @@ -4,6 +4,7 @@ import ( "flag" "fmt" "os" + "time" "github.com/optimyze-interviews/OezguerKesim/GetRuntimeAddresses/ebpf" "github.com/optimyze-interviews/OezguerKesim/GetRuntimeAddresses/symbolyze" @@ -28,9 +29,12 @@ func main() { scanner := symbolyze.NewScanner(*symbol, *glob) scanner.OnFound(mapFD.Set) - if *debug { - scanner.DebugOn() - } + scanner.Debug(*debug) + + go scanner.RunEvery(time.Second) + + time.Sleep(10 * time.Second) + scanner.Stop() err = scanner.Run() if err != nil { diff --git a/GetRuntimeAddresses/symbolyze/symbolyze.go b/GetRuntimeAddresses/symbolyze/symbolyze.go index a20a10f..79d565e 100644 --- a/GetRuntimeAddresses/symbolyze/symbolyze.go +++ b/GetRuntimeAddresses/symbolyze/symbolyze.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "sync" + "time" ) // Scanner represents an engine for scanning for a specific symbol in all @@ -24,25 +25,51 @@ import ( // /proc. Whenever a match is found, all observers will be called with the // (pid, offset), concurrently. type Scanner struct { + rwmutex + symbol string pathglob string cache map[string]uint64 // Contains (pathname, offset) observers []Observer // Callbacks - logger // Embedded logger - // Instead of using a boolean to indicate debugging, we use function // members. This way we can populate them with noop-functions in the // non-debug case and not polute the code with if-statements. debugf func(format string, v ...interface{}) debugln func(v ...interface{}) + logger // Embedded logger - err error // error state of the scanner. + errors errors + + ticker *time.Ticker // Used to run the scanner repeatedly +} + +type errors []error + +func (e errors) Error() string { + switch len(e) { + case 0: + return "nil" + case 1: + return e[0].Error() + default: + list := make([]string, len(e)) + for i := range e { + list[i] = e[i].Error() + } + return fmt.Sprintf("multiple errors:\n%s", strings.Join(list, "\n")) + } } // We use a lowercase type alias for *log.Logger so that we can embedd it in // Scanner without exporting it. type logger = *log.Logger +type rwmutex = sync.RWMutex + +var ( + nodebugf = func(format string, v ...interface{}) {} + nodebugln = func(v ...interface{}) {} +) // An Observer is a callback that can be registered with Scanner.OnFound. It // will be called with a pid and an offset. Observers are called concurrently. @@ -62,41 +89,76 @@ func NewScanner(symbol, pathglob string) *Scanner { logger: log.New(os.Stderr, "[symbolyze] ", log.Ltime|log.Lmicroseconds), // debugging is off per default. - debugf: func(string, ...interface{}) {}, - debugln: func(...interface{}) {}, + debugf: nodebugf, + debugln: nodebugln, } } // Debug sets the scanner into debugging mode. It must be called only once // before a call to Scanner.Run(). -func (S *Scanner) DebugOn() { - // Use the embedded *log.Logger for debugging. - S.debugf = S.Printf - S.debugln = S.Println - S.debugln("starting in debug-mode") +func (S *Scanner) Debug(on bool) { + S.Lock() + defer S.Unlock() + + if on { + // Use the embedded *log.Logger for debugging. + S.debugf = S.Printf + S.debugln = S.Println + S.debugln("starting in debug-mode") + } else { + S.debugf = nodebugf + S.debugln = nodebugln + } } // setErrorf puts the Scanner into an error state with the given error // statement. It also logs the error. setErrorf is not thread-safe. func (S *Scanner) setErrorf(format string, a ...interface{}) { - S.err = fmt.Errorf(format, a...) + S.Lock() + S.errors = append(S.errors, fmt.Errorf(format, a...)) + S.Unlock() S.Printf(format, a...) } -// OnFound puts an Observer function into the internal queue. The functions -// are called in sequence in their own goroutine whenever the scanner finds the -// symbol in a running program. That implies that an Observer has to be -// thread-safe. Errors from the observers will be logged. -// -// Calling OnFound is not thread-safe. -func (S *Scanner) OnFound(fun Observer) { - S.observers = append(S.observers, fun) +func (S *Scanner) HasErrors() bool { + S.RLock() + defer S.RUnlock() + return len(S.errors) > 0 +} + +func (S *Scanner) Errors() error { + S.RLock() + defer S.RUnlock() + if len(S.errors) == 0 { + return nil + } else { + e2 := make(errors, len(S.errors)) + copy(e2, S.errors) + return e2 + } +} + +func (S *Scanner) addError(err error) { + S.Lock() + defer S.Unlock() + S.errors = append(S.errors, err) +} + +// OnFound puts Observer functions at the end of the internal queue. All +// Observer functions are called in sequence in their own goroutine whenever +// the scanner finds the symbol in a running program. That implies that an +// Observer has to be thread-safe. Errors from the observers will be logged. +func (S *Scanner) OnFound(fun ...Observer) { + S.Lock() + defer S.Unlock() + + S.observers = append(S.observers, fun...) return } // Run starts the scanning process. It scans the entries of all /proc/NNN/maps // files for pathnames that match the provided path-glob and are executables or -// shared libraries in ELF formmat. It searches for the provided symbol in +// shared libraries in ELF format. It searches for the provided symbol in // those files and calls the registered Observer functions, concurrently, with // the pid and offset of the symbol. // @@ -104,20 +166,20 @@ func (S *Scanner) OnFound(fun Observer) { // it will try to continue to loop over all pids, writing potential errors to // the console. Errors from the observer functions are logged. func (S *Scanner) Run() error { - if S.err != nil { - return S.err + if S.HasErrors() { + return S.Errors() } proc, err := os.Open("/proc") if err != nil { S.setErrorf("Failed to open /proc: %v\n", err) - return S.err + return S.Errors() } infos, err := proc.Readdir(-1) if err != nil { S.setErrorf("Failed to read /proc: %v\n", err) - return S.err + return S.Errors() } proc.Close() @@ -132,7 +194,7 @@ func (S *Scanner) Run() error { continue } else if pid, err := strconv.Atoi(pid_s); err != nil { continue - } else if offset, found := S.searchSymbolIn(pid); !found { + } else if offset, found := S.searchSymbolInPid(pid); !found { continue } else { // Call the observers with (pid, offset), in the @@ -143,8 +205,7 @@ func (S *Scanner) Run() error { go func() { err = observer(pid, offset) if err != nil { - S.Printf("S.observer[%d](%d, %d) error: %v", n, pid, offset, err) - // TODO: accumulate errors from all Observers. + S.addError(fmt.Errorf("S.observer[%d](%d, %d) error: %v", n, pid, offset, err)) } wg.Done() }() @@ -154,10 +215,31 @@ func (S *Scanner) Run() error { } wg.Wait() // Wait for all observers to finish - return S.err + return S.Errors() } -// searchSymbolIn loops over the entries in /proc//maps and searches for +// RunEvery() starts a scanning process and repeats at the given time step. +func (S *Scanner) RunEvery(step time.Duration) { + S.ticker = time.NewTicker(step) + for { + select { + case <-S.ticker.C: + err := S.Run() + if err != nil { + S.Println(err) + } + } + } +} + +func (S *Scanner) Stop() { + if S.ticker != nil { + S.debugln("Stopping ticker") + S.ticker.Stop() + } +} + +// searchSymbolInPid loops over the entries in /proc//maps and searches for // the symbol in the mapped files. // // The current implementation makes the following assumptions: @@ -167,12 +249,12 @@ func (S *Scanner) Run() error { // 4. The symbol is present at most in one mapped file at the same time. // // It returns the offsets in memory of the running program, if found. -func (S *Scanner) searchSymbolIn(pid int) (offset uint64, found bool) { +func (S *Scanner) searchSymbolInPid(pid int) (offset uint64, found bool) { path := filepath.Join("/proc", strconv.Itoa(pid), "maps") maps, err := os.Open(path) if err != nil { - S.Printf("%v\n", err) + S.debugf("%v\n", err) return 0, false } @@ -230,7 +312,7 @@ func (S *Scanner) searchSymbolIn(pid int) (offset uint64, found bool) { // Finally, find the symbol in the binary. If found, // findSymbol returns the offset of the symbol in memory, // taking alignment into account. - memOffset, found := S.findSymbol(pathname) + memOffset, found := S.findSymbolInELF(pathname) if !found { continue } @@ -242,15 +324,15 @@ func (S *Scanner) searchSymbolIn(pid int) (offset uint64, found bool) { return 0, false } -// findSymbol searches for the provided symbol in the given pathname to an +// findSymbolInELF searches for the provided symbol in the given pathname to an // ELF-file. If found, it returns the offset of the symbol in the virtual // memory according to the fomula: // // vmOffset = alignedOffset(section) + offsetInSection(symbol) // -// The result will be cached so that subsequent calls to findSymbol with the -// same pathname can quickly return. -func (S *Scanner) findSymbol(pathname string) (offset uint64, found bool) { +// The result will be cached so that subsequent calls to findSymbolInELF with +// the same pathname can quickly return. +func (S *Scanner) findSymbolInELF(pathname string) (offset uint64, found bool) { // 0. Return the value from the cache, if found. if offset, found = S.cache[pathname]; found {