package symbolyze import ( "bufio" "debug/elf" "fmt" "log" "os" "path/filepath" "strconv" "strings" ) type Finder struct { symbol string pathglob string cache map[string]uint64 finds map[int]uint64 ownpid int observer func(key int, value uint64) error *log.Logger debugf func(format string, v ...interface{}) debugln func(v ...interface{}) err error } var ( nodebugf = func(string, ...interface{}) {} nodebugln = func(...interface{}) {} ) func New(symbol, pathglob string) *Finder { return &Finder{ symbol: symbol, pathglob: pathglob, cache: map[string]uint64{}, finds: map[int]uint64{}, ownpid: os.Getpid(), Logger: log.New(os.Stdout, "[symbolyze] ", log.LstdFlags), debugf: nodebugf, debugln: nodebugln, } } // not threadsafe func (F *Finder) Debug(on bool) { if on { F.debugf = F.Printf F.debugln = F.Println F.debugln("starting in debug-mode") } else { F.debugf = nodebugf F.debugln = nodebugln } } func (F *Finder) setErrorf(format string, a ...interface{}) { F.err = fmt.Errorf(format, a) F.Printf(format, a) } type Observer func(int, uint64) error // not threadsafe func (F *Finder) OnFound(fun Observer) { F.observer = fun return } func (F *Finder) Run() error { if F.err != nil { return F.err } proc, err := os.Open("/proc") if err != nil { F.setErrorf("Failed to open /proc: %w\n", err) return F.err } infos, err := proc.Readdir(-1) if err != nil { F.setErrorf("Failed to read /proc: %w\n", err) return F.err } proc.Close() for _, pinfo := range infos { var pid_s = pinfo.Name() // The entry /proc/NNN/ must be a directory with integer name if !pinfo.IsDir() { continue } else if pid, err := strconv.Atoi(pid_s); err != nil { continue } else if pid == F.ownpid { // skip our own pid continue } else if offset, found := F.searchSymbolIn(pid); !found { continue } else { F.finds[pid] = uint64(offset) if F.observer != nil { // TODO: accumulate errors? err = F.observer(pid, uint64(offset)) if err != nil { F.debugf("F.observer error: %w", err) } } } } return nil } func (F *Finder) searchSymbolIn(pid int) (offset uint64, ok bool) { // read the maps file for the binary and shared libraries path := filepath.Join("/proc", strconv.Itoa(pid), "maps") maps, err := os.Open(path) if err != nil { // fmt.Printf("Warning: Failed to read %#q: %v\n", path, err) return } scanner := bufio.NewScanner(maps) for scanner.Scan() { // 0 1 2 3 4 5 // address perms offset dev inode pathname // 7fdd8fece000-7fdd8ff74000 rw-p 00423000 fd:01 14156759 /usr/lib/x86_64-linux-gnu/libpython3.7m.so.1.0 fields := strings.Fields(scanner.Text()) // TODO: we assume that the pathname contains no spaces so // bytes.Fields splits the line excactly into six fields if len(fields) != 6 { continue } pathname := fields[5] if !strings.HasPrefix(pathname, "/") { // Not a pathname continue } filename := filepath.Base(pathname) ok, err := filepath.Match(F.pathglob, filename) if err != nil || !ok { continue } if fields[1] != "rw-p" { // symbol needs to be writable continue } memOffset, found := F.findSymbol(pathname) if !found { continue } start, _, err := parseRange(fields[0]) if err != nil { fmt.Printf("%w\n", err) continue } fileoffset, err := strconv.ParseUint(fields[2], 16, 64) if err != nil { fmt.Printf("Error while parsing fileoffset %#q: %w\n", fields[2], err) continue } F.finds[pid] = start + memOffset - fileoffset return start + memOffset - fileoffset, true } return 0, false } func (F *Finder) findSymbol(pathname string) (offset uint64, found bool) { if offset, found = F.cache[pathname]; found { return offset, found } file, err := elf.Open(pathname) if err != nil { F.setErrorf("elf.Open(%s): %w", pathname, err) return 0, false } defer file.Close() symbols, err := file.DynamicSymbols() if err != nil { F.setErrorf("file.DynamicSymbols(): %w", err) return 0, false } var sym *elf.Symbol for _, s := range symbols { if s.Name == F.symbol { F.debugf("Found symbol %#v in %s: %#v\n", sym, pathname, s) sym = &s break } } if sym == nil { F.debugf("symbol %q not found in %s\n", sym, pathname) return 0, false } if len(file.Sections) < int(sym.Section) { F.debugf("len(file.Section) < int(sym.Section) for symbol %q in %s\n", sym, pathname) return 0, false } section := file.Sections[sym.Section] if section == nil { F.debugf("Section %v not found for ELF-Header %q in %s\n", sym.Section, pathname) return 0, false } header := §ion.SectionHeader memoffset := sym.Value - header.Addr + alignedOffset(header) F.cache[pathname] = memoffset return memoffset, true } func parseRange(input string) (start, end uint64, e error) { // 7fdd8fece000-7fdd8ff74000 parts := strings.Split(input, "-") if len(parts) != 2 { e = fmt.Errorf("[parseRange] unrecognized format for region: %#q", input) return 0, 0, e } start, e = strconv.ParseUint(parts[0], 16, 64) if e != nil { e = fmt.Errorf("[parseRange] couldn't parse start-address %#q in %#q: %w", parts[0], input, e) return 0, 0, e } end, e = strconv.ParseUint(parts[1], 16, 64) if e != nil { e = fmt.Errorf("[parseRange] couldn't parse end-address %#q in %#q: %w", parts[1], input, e) return 0, 0, e } return start, end, e } func alignedOffset(section *elf.SectionHeader) uint64 { mask := section.Addralign - 1 return (section.Offset + mask) & (^mask) }