-
Notifications
You must be signed in to change notification settings - Fork 18k
debug/elf: add SHT_GNU_VERDEF section parsing #63952
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
This proposal has been added to the active column of the proposals project |
This API diff is nice and small and seems reasonable for the benefit:
Are there any remaining concerns about this proposal? |
Could you explain what the meanings of |
They come from readelf.c If we take a look at some output from it: $ readelf -sW /usr/lib/x86_64-linux-gnu/libtiffxx.so | tail -4
23: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZNSt8ios_base4InitD1Ev@GLIBCXX_3.4 (3)
24: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS LIBTIFFXX_4.0
25: 0000000000001890 169 FUNC GLOBAL DEFAULT 15 _Z14TIFFStreamOpenPKcPSo@@LIBTIFFXX_4.0
26: 0000000000001940 19 FUNC GLOBAL DEFAULT 15 _Z14TIFFStreamOpenPKcPSi@@LIBTIFFXX_4.0
// Versioned symbol info
const (
SymUndefined byte = 0
SymHidden byte = 1
SymPublic byte = 2
) Comes from: https://github.com/bminor/binutils-gdb/blob/master/binutils/readelf.c#L363 /* Versioned symbol info. */
enum versioned_symbol_info
{
symbol_undefined,
symbol_hidden,
symbol_public
}; https://github.com/bminor/binutils-gdb/blob/master/binutils/readelf.c#L13764 if (version_string)
{
if (sym_info == symbol_undefined)
printf ("@%s (%d)", version_string, vna_other);
else
printf (sym_info == symbol_hidden ? "@%s" : "@@%s",
version_string);
} In the case of So in the actual implementation in // Versioned symbol info
const (
SymUndefined byte = 0
SymHidden byte = 1
SymPublic byte = 2
)
type verdef struct {
Other uint16
Name string
}
type verneed struct {
File string
Other uint16
Name string
}
// gnuVersionInit parses the GNU version tables
// for use by calls to gnuVersion.
func (f *File) gnuVersionInit(str []byte) bool {
// Accumulate verdef information.
var def []verdef
vd := f.SectionByType(SHT_GNU_VERDEF)
if vd != nil {
d, _ := vd.Data()
i := 0
for {
if i+20 > len(d) {
break
}
other := f.ByteOrder.Uint16(d[i : i+2])
//flags := f.ByteOrder.Uint16(d[i+2 : i+4])
ndx := int(f.ByteOrder.Uint16(d[i+4 : i+6]))
cnt := f.ByteOrder.Uint16(d[i+6 : i+8])
// hash := f.ByteOrder.Uint32(d[i+8 : i+12])
aux := f.ByteOrder.Uint32(d[i+12 : i+16])
next := f.ByteOrder.Uint32(d[i+16 : i+20])
var name string
j := i + int(aux)
for c := 0; c < int(cnt); c++ {
if j+8 > len(d) {
break
}
vname := f.ByteOrder.Uint32(d[j : j+4])
vnext := f.ByteOrder.Uint32(d[j+4 : j+8])
name, _ = getString(str, int(vname))
if c == 0 {
if ndx >= len(def) {
a := make([]verdef, ndx)
copy(a, def)
def = a
}
def[ndx-1] = verdef{other, name}
}
j += int(vnext)
}
if next == 0 {
break
}
i += int(next)
}
}
// Accumulate verneed information.
var need []verneed
vn := f.SectionByType(SHT_GNU_VERNEED)
if vn != nil {
d, _ := vn.Data()
i := 0
for {
if i+16 > len(d) {
break
}
vers := f.ByteOrder.Uint16(d[i : i+2])
if vers != 1 {
break
}
cnt := f.ByteOrder.Uint16(d[i+2 : i+4])
fileoff := f.ByteOrder.Uint32(d[i+4 : i+8])
aux := f.ByteOrder.Uint32(d[i+8 : i+12])
next := f.ByteOrder.Uint32(d[i+12 : i+16])
file, _ := getString(str, int(fileoff))
var name string
j := i + int(aux)
for c := 0; c < int(cnt); c++ {
if j+16 > len(d) {
break
}
// hash := f.ByteOrder.Uint32(d[j : j+4])
// flags := f.ByteOrder.Uint16(d[j+4 : j+6])
other := f.ByteOrder.Uint16(d[j+6 : j+8])
nameoff := f.ByteOrder.Uint32(d[j+8 : j+12])
next := f.ByteOrder.Uint32(d[j+12 : j+16])
name, _ = getString(str, int(nameoff))
ndx := int(other)
if ndx >= len(need) {
a := make([]verneed, 2*(ndx+1))
copy(a, need)
need = a
}
need[ndx] = verneed{file, other, name}
if next == 0 {
break
}
j += int(next)
}
if next == 0 {
break
}
i += int(next)
}
}
// Versym parallels symbol table, indexing into verneed.
vs := f.SectionByType(SHT_GNU_VERSYM)
if vs == nil {
return false
}
d, _ := vs.Data()
f.gnuVerdef = def
f.gnuNeed = need
f.gnuVersym = d
return true
}
// gnuVersion adds Library and Version information to sym,
// which came from offset i of the symbol table.
func (f *File) gnuVersion(i int) (library string, version string, info byte, other byte) {
// Each entry is two bytes; skip undef entry at beginning.
i = (i + 1) * 2
if i >= len(f.gnuVersym) {
return
}
s := f.gnuVersym[i:]
if len(s) < 2 {
return
}
j := int(f.ByteOrder.Uint16(s))
if j >= 2 && j < len(f.gnuNeed) {
n := &f.gnuNeed[j]
if n.File != "" {
return n.File, n.Name, SymHidden, byte(n.Other)
}
}
var verInfo byte = SymUndefined
if j&0x8000 != 0 {
verInfo = SymHidden
} else {
verInfo = SymPublic
}
j = (j & 0x7fff) - 1
if j >= 1 && j < len(f.gnuVerdef) {
v := &f.gnuVerdef[j]
return "", v.Name, verInfo, byte(v.Other)
}
return
} The https://github.com/bminor/binutils-gdb/blob/master/include/elf/common.h#L1306 /* This flag appears in a Versym structure. It means that the symbol
is hidden, and is only visible with an explicit version number.
This is a GNU extension. */
#define VERSYM_HIDDEN 0x8000
/* This is the mask for the rest of the Versym information. */
#define VERSYM_VERSION 0x7fff |
It does seem strange for Sym* constants to go into a VerInfo field. Perhaps the names should be made closer? |
|
@rsc would you prefer something like this? |
I know you know all this, but just to reprise: There are three sections that contain version information: The The The The flags in Anyhow, the point is, the part of the version information that is symbol-specific is the So how about: Add a Add a // DynamicVersion is a version defined by a dynamic object.
type DynamicVersion struct {
Version uint16 // Version of data structure.
Flags DynamicVersionFlags
Index uint16
Name string
Deps []string
} We define flags for the type DynamicVersionFlags uint16
const (
VER_FLG_BASE DynamicVersionFlags = 1
VER_FLG_WEAK DynamicVersionFlags = 2
VER_FLG_INFO DynamicVersionFlags = 4
) Add a type DynamicVersionNeed struct {
Version uint16 // version of data structure
Name string // shared library name
Needs []DynamicVersionDep
}
type DynamicVersionDep struct {
Flags DynamicVersionFlags
Other uint16
Dep string // Name of required version
} I think this approach will let us extract all the information from the dynamic object, and let people write tools to do whatever they want. |
@ianlancetaylor thanks for your feedback and additional info. While your approach does appear to work I don't see the need to separate the version data from To be sure I wrote a test which will compare the output of //go:build linux
package main
import (
"bufio"
"debug/elf"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"testing"
)
type SymbolInfo struct {
Num int
Value string
Size string
Type string
Bind string
Vis string
Ndx string
Name string
}
func TestSymbols(t *testing.T) {
folderPath := "/usr/lib/x86_64-linux-gnu"
searchPattern := ".so"
files, err := searchFiles(folderPath, searchPattern)
if err != nil {
t.Errorf("Error %v", err)
return
}
t.Logf("Found %d files", len(files))
for _, file := range files {
symbols, err := parseSymbols(file)
if err != nil {
continue
}
f, err := elf.Open(file)
if err != nil {
continue
}
var checkSymbols = make(map[string][]elf.Symbol)
checkSymbols[".dynsym"], _ = f.DynamicSymbols()
checkSymbols[".symtab"], _ = f.Symbols()
for tableName, tableSymbols := range symbols {
if len(tableSymbols) != len(checkSymbols[tableName]) {
t.Errorf("expected %d got %d\n", len(tableSymbols), len(checkSymbols[tableName]))
}
for index, symbol := range tableSymbols {
symbolCheck := checkSymbols[tableName][index]
value := fmt.Sprintf("%016x", symbolCheck.Value)
size := fmt.Sprintf("%d", symbolCheck.Size)
if strings.HasPrefix(symbol.Size, "0x") {
size = fmt.Sprintf("0x%x", symbolCheck.Size)
}
type_ := parseType(int(symbolCheck.Info & 0xf))
bind := parseBind(int(symbolCheck.Info >> 4))
vis := parseVisibility(int(symbolCheck.Other))
ndx := parseNdx(int(symbolCheck.Section))
name := getSymbolName(symbolCheck, AddLibrary, AddVersion)
if value != symbol.Value {
t.Errorf("expected %v got %v\n", symbol.Value, value)
}
if size != symbol.Size {
t.Errorf("expected %x got %x\n", symbol.Size, size)
}
if type_ != symbol.Type {
t.Errorf("expected %v got %v\n", symbol.Type, type_)
}
if bind != symbol.Bind {
t.Errorf("expected %x got %v\n", symbol.Bind, bind)
}
if vis != symbol.Vis {
t.Errorf("expected %v got %v\n", symbol.Vis, vis)
}
if ndx != symbol.Ndx {
t.Errorf("expected %v got %v\n", symbol.Ndx, ndx)
}
if name != symbol.Name {
t.Errorf("expected %v got %v\n", symbol.Name, name)
}
}
}
}
}
func searchFiles(folderPath, searchPattern string) ([]string, error) {
var files []string
file, err := os.Open(folderPath)
if err != nil {
return nil, err
}
defer file.Close()
fileInfos, err := file.Readdir(-1)
if err != nil {
return nil, err
}
for _, fileInfo := range fileInfos {
if !fileInfo.IsDir() && strings.Contains(fileInfo.Name(), searchPattern) {
files = append(files, folderPath+"/"+fileInfo.Name())
}
}
return files, nil
}
func parseSymbols(filePath string) (map[string][]SymbolInfo, error) {
cmd := exec.Command("readelf", "-sW", filePath)
output, err := cmd.CombinedOutput()
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(strings.NewReader(string(output)))
symbols := make(map[string][]SymbolInfo)
var tableName string
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "Symbol table") {
startPos := strings.Index(line, "'")
endPos := strings.LastIndex(line, "'")
tableName = line[startPos+1 : endPos]
continue
}
if strings.HasPrefix(line, " Num:") {
scanner.Scan()
continue
}
fields := strings.Fields(line)
if len(fields) < 7 {
continue
}
name := ""
if len(fields) == 8 {
name = fields[7]
}
symbol := SymbolInfo{
Num: len(symbols[tableName]),
Value: fields[1],
Size: fields[2],
Type: fields[3],
Bind: fields[4],
Vis: fields[5],
Ndx: fields[6],
Name: name,
}
if len(fields) == 9 {
symbol.Name = fields[7] + " " + fields[8]
}
symbols[tableName] = append(symbols[tableName], symbol)
}
return symbols, nil
}
func parseType(typeInt int) string {
typeTable := map[int]string{
0: "NOTYPE",
1: "OBJECT",
2: "FUNC",
3: "SECTION",
4: "FILE",
5: "COMMON",
6: "TLS",
10: "IFUNC",
13: "LOPROC",
15: "HIPROC",
}
return typeTable[typeInt]
}
func parseBind(bindInt int) string {
bindTable := map[int]string{
0: "LOCAL",
1: "GLOBAL",
2: "WEAK",
10: "UNIQUE",
11: "UNIQUE",
12: "UNIQUE",
}
return bindTable[bindInt]
}
func parseVisibility(visInt int) string {
visTable := map[int]string{
0: "DEFAULT",
1: "INTERNAL",
2: "HIDDEN",
3: "PROTECTED",
}
return visTable[visInt]
}
func parseNdx(ndxInt int) string {
ndxTable := map[int]string{
0: "UND",
0xff00: "BEFORE",
0xff01: "AFTER",
0xff1f: "HIPROC",
0xfff1: "ABS",
0xfff2: "COMMON",
0xffff: "HIRESERVE",
}
if val, ok := ndxTable[ndxInt]; ok {
return val
}
return strconv.FormatInt(int64(ndxInt), 10)
} $ go test ./cmd/profile -v
=== RUN TestSymbols
main_test.go:37: Found 3979 files
--- PASS: TestSymbols (40.48s)
PASS
ok github.com/benbaker76/libbpf-tools/cmd/profile 40.488s |
@benbaker76 I don't doubt that your approach works for your purposes. But your approach doesn't provide any mechanism for reading the other version data, like which versions requires which other versions. And I think it essentially duplicates the same information across a bunch of different |
@ianlancetaylor I think you've hit the nail on the head when you said "We've already locked ourselves into representing that as the I'm not entirely sure what particular version info is missing in my changes apart from the But as you suggest we should include these flags and I would do that by adding another byte called
So those are the changes I'd make but happy to go with your method as they do make better sense going forward especially if we were designing the parser from scratch and didn't have to worry about backward compatibility. I'd expect this isn't a heavily traveled part of the standard library anyway. |
@ianlancetaylor I'll hopefully get some time over the weekend to implement your suggestions. |
@ianlancetaylor here's my implementation based on your suggestions. // A File represents an open ELF file.
type File struct {
FileHeader
Sections []*Section
Progs []*Prog
closer io.Closer
dynVers []DynamicVersion
dynVerNeeds []DynamicVersionNeed
gnuVersym []byte
}
...
// A Symbol represents an entry in an ELF symbol table section
type Symbol struct {
Name string
Info, Other byte
VersionIndex int32
Section SectionIndex
Value, Size uint64
// Version and Library are present only for the dynamic symbol
// table.
Version string
Library string
}
...
const (
VER_NDX_LOCAL int32 = 0 // Symbol has local scope
VER_NDX_GLOBAL int32 = 1 // Symbol has global scope
VERSYM_VERSION int32 = 0x7fff // Version index mask
VERSYM_HIDDEN int32 = 0x8000 // Symbol is hidden
)
type DynamicVersionFlags uint16
const (
VER_FLG_BASE DynamicVersionFlags = 1 // Version definition of the file
VER_FLG_WEAK DynamicVersionFlags = 2 // Weak version identifier
VER_FLG_INFO DynamicVersionFlags = 4 // Reference exists for informational purposes
)
// DynamicVersion is a version defined by a dynamic object
type DynamicVersion struct {
Version uint16 // Version of data structure
Flags DynamicVersionFlags
Index uint16 // Version index
Deps []string // Dependencies
}
type DynamicVersionNeed struct {
Version uint16 // Version of data structure
Name string // Shared library name
Needs []DynamicVersionDep // Dependencies
}
type DynamicVersionDep struct {
Flags DynamicVersionFlags
Other uint16 // Version index
Dep string // Name of required version
}
// dynamicVersions returns version information for a dynamic object
func (f *File) dynamicVersions(str []byte) bool {
if f.dynVers != nil {
// Already initialized
return true
}
// Accumulate verdef information.
vd := f.SectionByType(SHT_GNU_VERDEF)
if vd == nil {
return false
}
d, _ := vd.Data()
var dynVers []DynamicVersion
i := 0
for {
if i+20 > len(d) {
break
}
version := f.ByteOrder.Uint16(d[i : i+2])
flags := DynamicVersionFlags(f.ByteOrder.Uint16(d[i+2 : i+4]))
ndx := f.ByteOrder.Uint16(d[i+4 : i+6])
cnt := f.ByteOrder.Uint16(d[i+6 : i+8])
aux := f.ByteOrder.Uint32(d[i+12 : i+16])
next := f.ByteOrder.Uint32(d[i+16 : i+20])
var depName string
var deps []string
j := i + int(aux)
for c := 0; c < int(cnt); c++ {
if j+8 > len(d) {
break
}
vname := f.ByteOrder.Uint32(d[j : j+4])
vnext := f.ByteOrder.Uint32(d[j+4 : j+8])
depName, _ = getString(str, int(vname))
deps = append(deps, depName)
j += int(vnext)
}
dynVers = append(dynVers, DynamicVersion{
Version: version,
Flags: flags,
Index: ndx,
Deps: deps,
})
if next == 0 {
break
}
i += int(next)
}
f.dynVers = dynVers
return true
}
// DynamicVersions returns version information for a dynamic object
func (f *File) DynamicVersions() (*[]DynamicVersion, error) {
if f.dynVers == nil {
return nil, errors.New("DynamicVersions: not initialized")
}
return &f.dynVers, nil
}
// dynamicVersionNeeds returns version dependencies for a dynamic object
func (f *File) dynamicVersionNeeds(str []byte) bool {
if f.dynVerNeeds != nil {
// Already initialized
return true
}
// Accumulate verneed information.
vn := f.SectionByType(SHT_GNU_VERNEED)
if vn == nil {
return false
}
d, _ := vn.Data()
var dynVerNeeds []DynamicVersionNeed
i := 0
for {
if i+16 > len(d) {
break
}
vers := f.ByteOrder.Uint16(d[i : i+2])
if vers != 1 {
break
}
cnt := f.ByteOrder.Uint16(d[i+2 : i+4])
fileoff := f.ByteOrder.Uint32(d[i+4 : i+8])
aux := f.ByteOrder.Uint32(d[i+8 : i+12])
next := f.ByteOrder.Uint32(d[i+12 : i+16])
file, _ := getString(str, int(fileoff))
var deps []DynamicVersionDep
j := i + int(aux)
for c := 0; c < int(cnt); c++ {
if j+16 > len(d) {
break
}
flags := DynamicVersionFlags(f.ByteOrder.Uint16(d[j+4 : j+6]))
other := f.ByteOrder.Uint16(d[j+6 : j+8])
nameoff := f.ByteOrder.Uint32(d[j+8 : j+12])
next := f.ByteOrder.Uint32(d[j+12 : j+16])
depName, _ := getString(str, int(nameoff))
deps = append(deps, DynamicVersionDep{
Flags: flags,
Other: other,
Dep: depName,
})
if next == 0 {
break
}
j += int(next)
}
dynVerNeeds = append(dynVerNeeds, DynamicVersionNeed{
Version: vers,
Name: file,
Needs: deps,
})
if next == 0 {
break
}
i += int(next)
}
f.dynVerNeeds = dynVerNeeds
return true
}
// DynamicVersionNeeds returns version dependencies for a dynamic object
func (f *File) DynamicVersionNeeds() (*[]DynamicVersionNeed, error) {
if f.dynVerNeeds == nil {
return nil, errors.New("DynamicVersionNeeds: not initialized")
}
return &f.dynVerNeeds, nil
}
// gnuVersionInit parses the GNU version tables
// for use by calls to gnuVersion.
func (f *File) gnuVersionInit(str []byte) bool {
// Versym parallels symbol table, indexing into verneed.
vs := f.SectionByType(SHT_GNU_VERSYM)
if vs == nil {
return false
}
d, _ := vs.Data()
f.dynamicVersions(str)
f.dynamicVersionNeeds(str)
f.gnuVersym = d
return true
}
// gnuVersion adds Library and Version information to sym,
// which came from offset i of the symbol table.
func (f *File) gnuVersion(i int) (library string, version string, versionIndex int32) {
// Each entry is two bytes; skip undef entry at beginning.
i = (i + 1) * 2
if i >= len(f.gnuVersym) {
return "", "", -1
}
s := f.gnuVersym[i:]
if len(s) < 2 {
return "", "", -1
}
j := int32(f.ByteOrder.Uint16(s))
var ndx = (j & VERSYM_VERSION)
if ndx == VER_NDX_LOCAL || ndx == VER_NDX_GLOBAL {
return "", "", j
}
for _, v := range f.dynVerNeeds {
for _, n := range v.Needs {
if n.Other == uint16(ndx) {
return v.Name, n.Dep, j | VERSYM_HIDDEN
}
}
}
for _, v := range f.dynVers {
if v.Index == uint16(ndx) {
if len(v.Deps) > 0 {
return "", v.Deps[0], j
}
}
}
return "", "", -1
} |
@benbaker76 can you post the full API diff for the revised version? (Run all.bash and the API check should fail, like in #63952 (comment); that's the output I'm asking for.) |
Thanks @rsc here's the API diff
NOTE: I had to update the tests to pass with the addition of the |
I'm not sure we need to export |
@ianlancetaylor I do use So here's the latest API diff:
|
What do you use |
Here are a couple of functions that I'm using these constants. IMO it's really not necessary to have them exposed by API.
|
I see, thanks. I wonder if we should separate the Any opinion? |
@ianlancetaylor without adding another field we could use getters and setters along with a private
|
In this approach I think But a bigger issue is that I don't think anything else in debug/elf uses methods like that. It would be better to avoid two different ways of getting |
@ianlancetaylor I'm not a huge fan of the approach either. Another option is to move
And another option is to create a new
|
Another option is to move version out of
|
I think I was focusing on |
Here's where I am with the API:
|
I'm unsure about VersionScopeGlobal. Quoting the OpenSolaris docs: "Versions defined by an object are assigned version indexes starting at 1 and incremented by 1 for each version. Index 1 is reserved for the first global version. If the object does not have a SHT_SUNW_verdef version definition section, then all the global symbols defined by the object receive index 1. If the object does have a version definition section, then VER_NDX_GLOBAL simply refers to the first such version." So VER_NDX_GLOBAL is really just version index 1, and version index 1 has to be the base version. If we have VersionScopeGlobal and don't set VersionIndex, then the consumer of this API has to know VersionScopeGlobal means to look for version index 1 (or the version marked VER_FLG_BASE, which I think has to be the same thing?) That suggests to me we shouldn't have VersionScopeGlobal, and should just set VersionIndex to 1 in this case. It's unfortunate that there's the caveat that index 1 can be used even if there's no version definition section, which complicates how to resolve VersionIndex, but in a different way. |
To me the fact that it's possible to have symbols with |
Sent https://go.dev/cl/635079 with the currently suggested API changes. |
@benbaker76 Any opinion on these changes? Thanks. |
Change https://go.dev/cl/635079 mentions this issue: |
I ran #63952 (comment) by proposal committee and there were no objections. |
This updates the new version API for the discussion on #63952. This change reveals that in fact none of the tests set the VERSYM_HIDDEN bit. The code before this CL set the hidden flag for symbols that appear in DynamicVersionNeed, but that is not an accurate representation of the ELF. The readelf program does print undefined symbols that way (with a single '@'), but that doesn't mean that the hidden flag is set. Leaving tests with the hidden bit set for later. For #63952 Change-Id: Ida60831e0c9922dfc10f10c7a64bc76a2b197537 Reviewed-on: https://go-review.googlesource.com/c/go/+/635079 Reviewed-by: Austin Clements <[email protected]> Commit-Queue: Ian Lance Taylor <[email protected]> Auto-Submit: Ian Lance Taylor <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]>
I don't mind the name changes but my test application is now failing. func getSymbolName(sym elf.Symbol, dynVers []elf.DynamicVersion, dynVerNeeds []elf.DynamicVersionNeed, options ...SymbolOption) string {
demangleSymbol := false
addVersion := false
addLibrary := false
for _, o := range options {
switch {
case o == Demangle:
demangleSymbol = true
case o == AddVersion:
addVersion = true
case o == AddLibrary:
addLibrary = true
}
}
suffix := ""
if (sym.Info&0xf == 0x2 || sym.Info&0xf == 0x1 && sym.Section == 0 || sym.Section != 0xfff1) && sym.Version != "" {
if sym.VersionScope == elf.VersionScopeHidden {
suffix = "@" + sym.Version
} else {
suffix = "@@" + sym.Version
}
}
symName := sym.Name
if addLibrary {
symName += suffix
}
if demangleSymbol {
demangleName, err := demangle.ToString(sym.Name, demangle.NoRust)
if err == nil {
symName = demangleName
}
}
_, dynVerNeedDep := getDynamicVersionNeed(sym, dynVerNeeds)
if addVersion && dynVerNeedDep != nil {
if dynVerNeedDep.Index > 1 {
symName += fmt.Sprintf(" (%d)", dynVerNeedDep.Index)
}
}
return symName
} Which is equivalent to readelf.c#L14314 I'm getting lots of these:
|
Thanks. This is the code from readelf: if (version_string)
{
if (sym_info == symbol_undefined)
printf ("@%s (%d)", version_string, vna_other);
else
printf (sym_info == symbol_hidden ? "@%s" : "@@%s",
version_string);
} I think that if you do something similar then there is no need to return I'm less clear on changing |
I'm not having much luck getting it to work. I'll attach my test app that compares output with |
I get no reported differences with current Go tip and this patch to your readelf_cmp.go source code.
|
Thanks, that fixes one issue. The only thing remaining for me is if I run it as-is I get:
If I make these changes (change
Now I get:
|
That's interesting, it looks like some of the symbols in libfuse.so.2 have a versym entry of 0x8001. This would seem to indicate a hidden symbol in the base version, which I didn't think was possible. Your suggested change doesn't seem quite right to me, because it loses the fact that the symbol is marked hidden. I think with our current approach we need to extend |
Repeating what I just posted to https://go.dev/cl/636095, which I probably should have posted here: This strongly suggests that we need to treat "hidden" as orthogonal to local/global/specific, and not bundle it into one enum. We could add another bool to Symbol, or if we don't want to make Symbol larger, we could pack all of this (including the version) into a uint16 with accessors for "Scope", "Hidden", and "Version". In fact, other than VersionScopeNone, that could just be the ELF encoding. Roughly: type Symbol struct {
...
Version SymbolVersion
}
type SymbolVersion uint16
// Scope describes the version in which the symbol is defined.
// When no symbol versioning information is available, this is VersionScopeNone.
func (SymbolVersion) Scope() SymbolVersionScope
func (SymbolVersion) Hidden() bool
// If Scope is VersionScopeSpecific or VersionScopeHidden, this is the version index.
// This is only set for the dynamic symbol table.
// This index will match either [DynamicVersion.Index] in the slice returned by
// [File.DynamicVersions], or [DynamicVersionDep.Index] in the Needs field
// of the elements of the slice returned by [File.DynamicVersionNeeds].
// In general, a defined symbol will have an index referring to DynamicVersions,
// and an undefined symbol will have an index referring to some version in DynamicVersionNeeds.
func (SymbolVersion) Index() int16
// SymbolVersionScope describes the version in which a [Symbol] is defined.
// This is only used for the dynamic symbol table.
type SymbolVersionScope byte
const (
VersionScopeNone SymbolVersionScope = iota // no symbol version available
VersionScopeLocal // symbol has local scope
VersionScopeGlobal // symbol has global scope
VersionScopeSpecific // symbol is in the version given by VersionIndex
) (I'm not quite sure how to fit VersionScopeNone into this. Would it be okay to drop that and return VersionScopeLocal if there's no version info?) |
Another approach we can take is to drop |
In that case would VersionScope only report None/Specific/Hidden? (I think we'd need a different term for Specific in that case, and maybe it's not even a "Scope" any more.) |
Stepping back, what we're trying to express is that a symbol version is a 16-bit integer. The high bit is set if the symbol is hidden (meaning that the symbol is only usable with an explicit version specification). The rest of the integer is a version index. The values 0 is special. Other values correspond to the DynamicVersion.Index field or the DynamicVersionDep.Index field. The value 0 means that the symbol does not have a version, and is not visible outside the dynamic object. The value 1 corresponds to a DynamicVersion.Index field, but it's a special one that isn't a real version. And, of course, besides those values, we need to express the possibility that the symbol has no version. From a linking perspective, I don't see any reason for a symbol version of 0x8001. That means that the symbol is in the base version, meaning that it has no version. And it means that the symbol is hidden, meaning that the version must be explicitly specified. But there is no version, so there is nothing to explicitly specify. In the only case I know where it happens. 0x8001 appears for a symbol like In the libfuse sources this is due to a line
In other words: a hidden symbol with no version, just as the object file is recording. I'm not yet sure what is best here. |
My sense is that we're both under- and over-interpreting this data for the consumer. One thing I liked in my proposed type SymbolVersion uint16 is that can literally be the value from the ELF file and we can provide helpers to interpret it. (Maybe we need a bool field to indicate if there's any version info?) If we wanted to heavily interpret this, we would do the index lookup for people, which isn't the direction of this API. Dropping VersionScopeGlobal and VersionScopeLocal leaves the "hidden" bit as the only thing we're interpreting, which feels like a weird middle ground to me. Why interpret that one bit and not the special meanings of 0 and 1? Another option is we just give people the |
Your proposal is much like an earlier one I made here
I believe that's why we originally went with an enum and flags to set the hidden bit.
In common.h hidden bit and mask are called
Yes and I did make a similar proposal to this here |
Yes, I did suggest moving I do think we need to have a way to record "no version information." So that leads us to changing everything around yet again, and coming up with type Symbol {
...
// HasVersion reports whether the symbol has any version information.
HasVersion bool
// VersionIndex is the symbol's version index.
// Use the methods of the VersionIndex type to access it.
VersionIndex VersionIndex
...
}
// VersionIndex is the type of a symbol version index.
type VersionIndex uint16
// IsHidden reports whether the symbol is hidden within the version.
// This means that the symbol can only be seen by specifying the exact version.
func (vi VersionIndex) IsHidden(bool) { return vi&0x8000 != 0 }
// Index returns the version index.
// If this is the value 0, it means that the symbol is local and not visible externally.
// If this is the value 1, it means that the symbol is in the base version, and has no specific version.
// Other values will match either [DynamicVersion.Index]
// in the slice returned by [File.DynamicVersions],
// or [DynamicVersiondep.Index] in the Needs field
// of the elements of the slice returned by [File.DynamicVersionNeeds].
func (vi VersionIndex) Index() uint16 { return uint16(vi & 0x7fff) } If we adopt this approach then |
I'm good with @ianlancetaylor 's latest API |
Me too |
Change https://go.dev/cl/637235 mentions this issue: |
This updates the new version API for the discussion on #63952. Note that the current tests do not have symbols with hidden versions. Leaving that for later. For #63952 Change-Id: I1ad4b1e485429a216ba8e5b68f7f4299d120628f Reviewed-on: https://go-review.googlesource.com/c/go/+/637235 Reviewed-by: Ian Lance Taylor <[email protected]> Auto-Submit: Ian Lance Taylor <[email protected]> Reviewed-by: Austin Clements <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Commit-Queue: Ian Lance Taylor <[email protected]>
Here's an updated I didn't find a need for |
It seems to me that we need |
The proposal committee had no objections to the re-updated API @ianlancetaylor proposed (other than the typo that IsHidden should return a bool, not take it) |
I'm porting @brendangregg's eBPF profiling application profile.py to Golang. I'm attempting to use the
debug/elf
package for retrieving debug symbol info but have stumbled upon a limitation.As a demonstration I've picked a library called
libtiffxx.so
as it's relatively small and clearly demontrates the issues I have with the package.Let's take a look at the
.gnu.version*
sections contained in the library:$ readelf -SW /usr/lib/x86_64-linux-gnu/libtiffxx.so | grep .gnu.version [ 6] .gnu.version VERSYM 0000000000000882 000882 000036 02 A 4 0 2 [ 7] .gnu.version_d VERDEF 00000000000008b8 0008b8 000038 00 A 5 2 8 [ 8] .gnu.version_r VERNEED 00000000000008f0 0008f0 000090 00 A 5 3 8
There's important debug symbol information contained in the
.gnu.version_d
(SHT_GNU_VERDEF) section which is currently not parsed by the package. Let's look at the last four functions in the.dynsym
section.$ readelf -sW /usr/lib/x86_64-linux-gnu/libtiffxx.so | tail -4 23: 0000000000000000 0 FUNC GLOBAL DEFAULT UND _ZNSt8ios_base4InitD1Ev@GLIBCXX_3.4 (3) 24: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS LIBTIFFXX_4.0 25: 0000000000001890 169 FUNC GLOBAL DEFAULT 15 _Z14TIFFStreamOpenPKcPSo@@LIBTIFFXX_4.0 26: 0000000000001940 19 FUNC GLOBAL DEFAULT 15 _Z14TIFFStreamOpenPKcPSi@@LIBTIFFXX_4.0
The
debug/elf
package can retreive the function name, but what about the@
or@@
, the library name after it or even the number in the brackets? In my proposal and my current implementation I provide a way to access this important debug symbol information.If we take a look at the dynamic symbol output of these four functions in the current implementation of
debug/elf
we have the following output:In my new implemenation we get this output:
We now have the
Version
entry as is often retrieved fromSHT_GNU_VERNEED
but in the last three entries can only be obtained from theSHT_GNU_VERDEF
section. We also have two newSymbol
members calledVerInfo
andVerOther
.So what are these new values and where do they come from? Using binutil's
readelf.c
as a reference we can find out where this data is located and how to extract it.VerInfo
in this case refers to the number in the brackets. In the case of_ZNSt8ios_base4InitD1Ev@GLIBCXX_3.4 (3)
the number 3 comes fromVerOther
which is the version number. If it's1
we don't display it in the brackets. The single@
comes fromVerInfo
which can be one of three values:If the value is
SymHidden
it will be@
and if it'sSymPublic
it will be@@
. This is taken fromreadelf.c
So I have implemented the code to do the necessary parsing of the
SHT_GNU_VERDEF
section and am writing this proposal so I can submit my changes to Gerrit for review by the Golang team.Currently I have made changes to the following files in
src/debug/elf
:file_test.go
file.go
symbols_test.go
testdata/libtiffxx.so
(should be renamed?)As I mentioned I use
libtiffxx.so
as my testdata ELF binary because it's relatively small (14.4 kB) and it demonstrates the new data from which my new branch can parse.My changes currently pass all tests except for the
API check
Thanks for taking the time to review my proposal.
The text was updated successfully, but these errors were encountered: