Skip to content

Commit

Permalink
usm: Added ability to get symbols from pclntab section
Browse files Browse the repository at this point in the history
  • Loading branch information
guyarb committed Sep 8, 2024
1 parent 3bd18a5 commit 329610a
Show file tree
Hide file tree
Showing 2 changed files with 298 additions and 0 deletions.
282 changes: 282 additions & 0 deletions pkg/network/go/bininspect/pclntab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2024-present Datadog, Inc.

package bininspect

import (
"debug/elf"
"encoding/binary"
"errors"
"fmt"
)

const (
pclntabSectionName = ".gopclntab"

go116magic = 0xfffffffa
go118magic = 0xfffffff0
go120magic = 0xfffffff1
)

// version of the pclntab
type version int

const (
verUnknown version = iota
ver11
ver12
ver116
ver118
ver120
)

var (
// ErrMissingPCLNTABSection is returned when the pclntab section is missing.
ErrMissingPCLNTABSection = errors.New("failed to find pclntab section")

// ErrUnsupportedPCLNTABVersion is returned when the pclntab version is not supported.
ErrUnsupportedPCLNTABVersion = errors.New("unsupported pclntab version")

// ErrFailedToFindAllSymbols is returned when not all symbols were found.
ErrFailedToFindAllSymbols = errors.New("failed to find all symbols")
)

// sectionAccess is a wrapper around elf.Section to provide ReadAt functionality.
// This is used to lazy read from the pclntab section, as the pclntab is large and we don't want to read it all at once,
// or store it in memory.
type sectionAccess struct {
section *elf.Section
baseOffset int64
}

// ReadAt reads len(p) bytes from the section starting at the given offset.
func (s *sectionAccess) ReadAt(outBuffer []byte, offset int64) (int, error) {
return s.section.ReadAt(outBuffer, s.baseOffset+offset)
}

// pclntanSymbolParser is a parser for pclntab symbols.
// Similar to LineTable struct in https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L43
type pclntanSymbolParser struct {
// section is the pclntab section.
section *elf.Section
// symbolFilter is the filter for the symbols.
symbolFilter symbolFilter

// byteOrderParser is the binary.ByteOrder for the pclntab.
byteOrderParser binary.ByteOrder
// cachedVersion is the version of the pclntab.
cachedVersion version
// funcNameTable is the sectionAccess for the function name table.
funcNameTable sectionAccess
// funcData is the sectionAccess for the function data.
funcData sectionAccess
// funcTable is the sectionAccess for the function table.
funcTable sectionAccess
// funcTableSize is the size of the function table.
funcTableSize uint32
// ptrSize is the size of a pointer in the architecture of the binary.
ptrSize uint32
// ptrBufferSizeHelper is a buffer for reading pointers of the size ptrSize.
ptrBufferSizeHelper []byte
// funcNameHelper is a buffer for reading function names. Of the maximum size of the symbol names.
funcNameHelper []byte
// funcTableFieldSize is the size of a field in the function table.
funcTableFieldSize int
// funcTableBuffer is a buffer for reading fields in the function table.
funcTableBuffer []byte
}

// GetPCLNTABSymbolParser returns the matching symbols from the pclntab section.
func GetPCLNTABSymbolParser(f *elf.File, symbolFilter symbolFilter) (map[string]*elf.Symbol, error) {
section := f.Section(pclntabSectionName)
if section == nil {
return nil, ErrMissingPCLNTABSection
}

parser := &pclntanSymbolParser{section: section, symbolFilter: symbolFilter}

if err := parser.parsePclntab(); err != nil {
return nil, err
}
// Late initialization, to prevent allocation if the binary is not supported.
_, maxSymbolsSize := symbolFilter.getMinMaxLength()
parser.funcNameHelper = make([]byte, maxSymbolsSize)
parser.funcTableFieldSize = getFuncTableFieldSize(parser.cachedVersion, int(parser.ptrSize))
// Allocate the buffer for reading the function table.
// TODO: Do we need 2*funcTableFieldSize?
parser.funcTableBuffer = make([]byte, 2*parser.funcTableFieldSize)
return parser.getSymbols()
}

// parsePclntab parses the pclntab, setting the version and verifying the header.
// Based on parsePclnTab in https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L194
func (p *pclntanSymbolParser) parsePclntab() error {
p.cachedVersion = ver11

pclntabHeader := make([]byte, 8)
if n, err := p.section.ReadAt(pclntabHeader, 0); err != nil || n != len(pclntabHeader) {
return fmt.Errorf("failed to read pclntab header: %w", err)
}
// Matching the condition https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L216-L220
// Check header: 4-byte magic, two zeros, pc quantum, pointer size.
if p.section.Size < 16 || pclntabHeader[4] != 0 || pclntabHeader[5] != 0 ||
(pclntabHeader[6] != 1 && pclntabHeader[6] != 2 && pclntabHeader[6] != 4) || // pc quantum
(pclntabHeader[7] != 4 && pclntabHeader[7] != 8) { // pointer size
// TODO: add explicit error message
return errors.New("invalid pclntab header")
}

leMagic := binary.LittleEndian.Uint32(pclntabHeader)
beMagic := binary.BigEndian.Uint32(pclntabHeader)
switch {
case leMagic == go116magic:
p.byteOrderParser, p.cachedVersion = binary.LittleEndian, ver116
case beMagic == go116magic:
p.byteOrderParser, p.cachedVersion = binary.BigEndian, ver116
case leMagic == go118magic:
p.byteOrderParser, p.cachedVersion = binary.LittleEndian, ver118
case beMagic == go118magic:
p.byteOrderParser, p.cachedVersion = binary.BigEndian, ver118
case leMagic == go120magic:
p.byteOrderParser, p.cachedVersion = binary.LittleEndian, ver120
case beMagic == go120magic:
p.byteOrderParser, p.cachedVersion = binary.BigEndian, ver120
default:
return ErrUnsupportedPCLNTABVersion
}

p.ptrSize = uint32(pclntabHeader[7])
p.ptrBufferSizeHelper = make([]byte, p.ptrSize)

// offset is based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L252
offset := func(word uint32) uint64 {
off := 8 + word*p.ptrSize
if n, err := p.section.ReadAt(p.ptrBufferSizeHelper, int64(off)); err != nil || n != int(p.ptrSize) {
return 0
}
return p.uintptr(p.ptrBufferSizeHelper)
}

switch p.cachedVersion {
case ver118, ver120:
p.funcTableSize = uint32(offset(0))
p.funcNameTable = sectionAccess{
section: p.section,
baseOffset: int64(offset(3)),
}
p.funcData = sectionAccess{
section: p.section,
baseOffset: int64(offset(7)),
}
p.funcTable = sectionAccess{
section: p.section,
baseOffset: int64(offset(7)),
}
case ver116:
p.funcTableSize = uint32(offset(0))
p.funcNameTable = sectionAccess{
section: p.section,
baseOffset: int64(offset(2)),
}
p.funcData = sectionAccess{
section: p.section,
baseOffset: int64(offset(6)),
}
p.funcTable = sectionAccess{
section: p.section,
baseOffset: int64(offset(6)),
}
}

return nil
}

// uintptr returns the pointer-sized value encoded at b.
// The pointer size is dictated by the table being read.
// based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L186.
func (p *pclntanSymbolParser) uintptr(b []byte) uint64 {
if p.ptrSize == 4 {
return uint64(p.byteOrderParser.Uint32(b))
}
return p.byteOrderParser.Uint64(b)
}

// getFuncTableFieldSize returns the size of a field in the function table.
// based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L388-L392
func getFuncTableFieldSize(version version, ptrSize int) int {
if version >= ver118 {
return 4
}
return ptrSize
}

// getSymbols returns the symbols from the pclntab section that match the symbol filter.
// based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L300-L329
func (p *pclntanSymbolParser) getSymbols() (map[string]*elf.Symbol, error) {
symbols := make(map[string]*elf.Symbol, p.symbolFilter.getNumWanted())
data := sectionAccess{section: p.section}
for currentIdx := uint32(0); currentIdx < p.funcTableSize; currentIdx++ {
// based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L315
_, err := p.funcTable.ReadAt(p.funcTableBuffer, int64((2*currentIdx+1)*uint32(p.funcTableFieldSize)))
if err != nil {
continue
}

// based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L321
data.baseOffset = int64(p.uint(p.funcTableBuffer)) + p.funcData.baseOffset
nameOffset := field(p.ptrSize, p.cachedVersion, p.byteOrderParser, data, p.ptrBufferSizeHelper)
funcName := p.funcName(nameOffset)

if funcName == "" {
continue
}
symbols[funcName] = &elf.Symbol{
Name: funcName,
}
if len(symbols) == p.symbolFilter.getNumWanted() {
break
}
}
if len(symbols) < p.symbolFilter.getNumWanted() {
return symbols, ErrFailedToFindAllSymbols
}
return symbols, nil
}

// funcName returns the name of the function found at off.
func (p *pclntanSymbolParser) funcName(off uint32) string {
n, err := p.funcNameTable.ReadAt(p.funcNameHelper, int64(off))
if err != nil {
return ""
}
if p.symbolFilter.want(string(p.funcNameHelper[:n])) {
return string(p.funcNameHelper[:n])
}
return ""
}

// uint returns the uint stored at b.
// based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L427-L432
func (p *pclntanSymbolParser) uint(b []byte) uint64 {
if p.funcTableFieldSize == 4 {
return uint64(p.byteOrderParser.Uint32(b))
}
return p.byteOrderParser.Uint64(b)
}

// field returns the uint32 field at off.
// based on https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L472-L485
// We can only for the usage of this function for getting the name of the function (https://github.com/golang/go/blob/6a861010be9eed02d5285509cbaf3fb26d2c5041/src/debug/gosym/pclntab.go#L463)
// So we explicitly set `n = 1` in the original implementation.
func field(ptrSize uint32, version version, binary binary.ByteOrder, data sectionAccess, helper []byte) uint32 {
off := ptrSize
if version >= ver118 {
off = 4
}
if n, err := data.ReadAt(helper, int64(off)); err != nil || n != int(ptrSize) {
return 0
}
return binary.Uint32(helper)
}
16 changes: 16 additions & 0 deletions pkg/network/go/bininspect/symbols.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,19 @@ func GetAnySymbolWithPrefix(elfFile *elf.File, prefix string, maxLength int) (*e
// Shouldn't happen
return nil, errors.New("empty symbols map")
}

// GetAnySymbolWithPrefixPCLNTAB returns any one symbol with the given prefix and the
// specified maximum length from the pclntab section in ELF file.
func GetAnySymbolWithPrefixPCLNTAB(elfFile *elf.File, prefix string, maxLength int) (*elf.Symbol, error) {
symbols, err := GetPCLNTABSymbolParser(elfFile, newPrefixSymbolFilter(prefix, maxLength))
if err != nil {
return nil, err
}

for key := range symbols {
return symbols[key], nil
}

// Shouldn't happen
return nil, errors.New("empty symbols map")
}

0 comments on commit 329610a

Please sign in to comment.