diff options
Diffstat (limited to 'src/cmd/nm')
-rw-r--r-- | src/cmd/nm/doc.go | 41 | ||||
-rw-r--r-- | src/cmd/nm/nm.go | 167 | ||||
-rw-r--r-- | src/cmd/nm/nm_cgo_test.go | 57 | ||||
-rw-r--r-- | src/cmd/nm/nm_test.go | 364 |
4 files changed, 629 insertions, 0 deletions
diff --git a/src/cmd/nm/doc.go b/src/cmd/nm/doc.go new file mode 100644 index 0000000..b11a2a8 --- /dev/null +++ b/src/cmd/nm/doc.go @@ -0,0 +1,41 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Nm lists the symbols defined or used by an object file, archive, or executable. +// +// Usage: +// +// go tool nm [options] file... +// +// The default output prints one line per symbol, with three space-separated +// fields giving the address (in hexadecimal), type (a character), and name of +// the symbol. The types are: +// +// T text (code) segment symbol +// t static text segment symbol +// R read-only data segment symbol +// r static read-only data segment symbol +// D data segment symbol +// d static data segment symbol +// B bss segment symbol +// b static bss segment symbol +// C constant address +// U referenced but undefined symbol +// +// Following established convention, the address is omitted for undefined +// symbols (type U). +// +// The options control the printed output: +// +// -n +// an alias for -sort address (numeric), +// for compatibility with other nm commands +// -size +// print symbol size in decimal between address and type +// -sort {address,name,none,size} +// sort output in the given order (default name) +// size orders from largest to smallest +// -type +// print symbol type after name +package main diff --git a/src/cmd/nm/nm.go b/src/cmd/nm/nm.go new file mode 100644 index 0000000..78fa600 --- /dev/null +++ b/src/cmd/nm/nm.go @@ -0,0 +1,167 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "bufio" + "flag" + "fmt" + "log" + "os" + "sort" + + "cmd/internal/objfile" +) + +const helpText = `usage: go tool nm [options] file... + -n + an alias for -sort address (numeric), + for compatibility with other nm commands + -size + print symbol size in decimal between address and type + -sort {address,name,none,size} + sort output in the given order (default name) + size orders from largest to smallest + -type + print symbol type after name +` + +func usage() { + fmt.Fprint(os.Stderr, helpText) + os.Exit(2) +} + +var ( + sortOrder = flag.String("sort", "name", "") + printSize = flag.Bool("size", false, "") + printType = flag.Bool("type", false, "") + + filePrefix = false +) + +func init() { + flag.Var(nflag(0), "n", "") // alias for -sort address +} + +type nflag int + +func (nflag) IsBoolFlag() bool { + return true +} + +func (nflag) Set(value string) error { + if value == "true" { + *sortOrder = "address" + } + return nil +} + +func (nflag) String() string { + if *sortOrder == "address" { + return "true" + } + return "false" +} + +func main() { + log.SetFlags(0) + flag.Usage = usage + flag.Parse() + + switch *sortOrder { + case "address", "name", "none", "size": + // ok + default: + fmt.Fprintf(os.Stderr, "nm: unknown sort order %q\n", *sortOrder) + os.Exit(2) + } + + args := flag.Args() + filePrefix = len(args) > 1 + if len(args) == 0 { + flag.Usage() + } + + for _, file := range args { + nm(file) + } + + os.Exit(exitCode) +} + +var exitCode = 0 + +func errorf(format string, args ...any) { + log.Printf(format, args...) + exitCode = 1 +} + +func nm(file string) { + f, err := objfile.Open(file) + if err != nil { + errorf("%v", err) + return + } + defer f.Close() + + w := bufio.NewWriter(os.Stdout) + + entries := f.Entries() + + var found bool + + for _, e := range entries { + syms, err := e.Symbols() + if err != nil { + errorf("reading %s: %v", file, err) + } + if len(syms) == 0 { + continue + } + + found = true + + switch *sortOrder { + case "address": + sort.Slice(syms, func(i, j int) bool { return syms[i].Addr < syms[j].Addr }) + case "name": + sort.Slice(syms, func(i, j int) bool { return syms[i].Name < syms[j].Name }) + case "size": + sort.Slice(syms, func(i, j int) bool { return syms[i].Size > syms[j].Size }) + } + + for _, sym := range syms { + if len(entries) > 1 { + name := e.Name() + if name == "" { + fmt.Fprintf(w, "%s(%s):\t", file, "_go_.o") + } else { + fmt.Fprintf(w, "%s(%s):\t", file, name) + } + } else if filePrefix { + fmt.Fprintf(w, "%s:\t", file) + } + if sym.Code == 'U' { + fmt.Fprintf(w, "%8s", "") + } else { + fmt.Fprintf(w, "%8x", sym.Addr) + } + if *printSize { + fmt.Fprintf(w, " %10d", sym.Size) + } + fmt.Fprintf(w, " %c %s", sym.Code, sym.Name) + if *printType && sym.Type != "" { + fmt.Fprintf(w, " %s", sym.Type) + } + fmt.Fprintf(w, "\n") + } + } + + if !found { + errorf("reading %s: no symbols", file) + } + + w.Flush() +} diff --git a/src/cmd/nm/nm_cgo_test.go b/src/cmd/nm/nm_cgo_test.go new file mode 100644 index 0000000..210577e --- /dev/null +++ b/src/cmd/nm/nm_cgo_test.go @@ -0,0 +1,57 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build cgo + +package main + +import ( + "runtime" + "testing" +) + +func canInternalLink() bool { + switch runtime.GOOS { + case "aix": + return false + case "dragonfly": + return false + case "freebsd": + switch runtime.GOARCH { + case "arm64", "riscv64": + return false + } + case "linux": + switch runtime.GOARCH { + case "arm64", "loong64", "mips64", "mips64le", "mips", "mipsle", "ppc64", "ppc64le", "riscv64": + return false + } + case "openbsd": + switch runtime.GOARCH { + case "arm64", "mips64": + return false + } + case "windows": + switch runtime.GOARCH { + case "arm64": + return false + } + } + return true +} + +func TestInternalLinkerCgoExec(t *testing.T) { + if !canInternalLink() { + t.Skip("skipping; internal linking is not supported") + } + testGoExec(t, true, false) +} + +func TestExternalLinkerCgoExec(t *testing.T) { + testGoExec(t, true, true) +} + +func TestCgoLib(t *testing.T) { + testGoLib(t, true) +} diff --git a/src/cmd/nm/nm_test.go b/src/cmd/nm/nm_test.go new file mode 100644 index 0000000..7d8358e --- /dev/null +++ b/src/cmd/nm/nm_test.go @@ -0,0 +1,364 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "internal/obscuretestdata" + "internal/testenv" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "testing" + "text/template" +) + +// TestMain executes the test binary as the nm command if +// GO_NMTEST_IS_NM is set, and runs the tests otherwise. +func TestMain(m *testing.M) { + if os.Getenv("GO_NMTEST_IS_NM") != "" { + main() + os.Exit(0) + } + + os.Setenv("GO_NMTEST_IS_NM", "1") // Set for subprocesses to inherit. + os.Exit(m.Run()) +} + +// nmPath returns the path to the "nm" binary to run. +func nmPath(t testing.TB) string { + t.Helper() + testenv.MustHaveExec(t) + + nmPathOnce.Do(func() { + nmExePath, nmPathErr = os.Executable() + }) + if nmPathErr != nil { + t.Fatal(nmPathErr) + } + return nmExePath +} + +var ( + nmPathOnce sync.Once + nmExePath string + nmPathErr error +) + +func TestNonGoExecs(t *testing.T) { + t.Parallel() + testfiles := []string{ + "debug/elf/testdata/gcc-386-freebsd-exec", + "debug/elf/testdata/gcc-amd64-linux-exec", + "debug/macho/testdata/gcc-386-darwin-exec.base64", // golang.org/issue/34986 + "debug/macho/testdata/gcc-amd64-darwin-exec.base64", // golang.org/issue/34986 + // "debug/pe/testdata/gcc-amd64-mingw-exec", // no symbols! + "debug/pe/testdata/gcc-386-mingw-exec", + "debug/plan9obj/testdata/amd64-plan9-exec", + "debug/plan9obj/testdata/386-plan9-exec", + "internal/xcoff/testdata/gcc-ppc64-aix-dwarf2-exec", + } + for _, f := range testfiles { + exepath := filepath.Join(testenv.GOROOT(t), "src", f) + if strings.HasSuffix(f, ".base64") { + tf, err := obscuretestdata.DecodeToTempFile(exepath) + if err != nil { + t.Errorf("obscuretestdata.DecodeToTempFile(%s): %v", exepath, err) + continue + } + defer os.Remove(tf) + exepath = tf + } + + cmd := testenv.Command(t, nmPath(t), exepath) + out, err := cmd.CombinedOutput() + if err != nil { + t.Errorf("go tool nm %v: %v\n%s", exepath, err, string(out)) + } + } +} + +func testGoExec(t *testing.T, iscgo, isexternallinker bool) { + t.Parallel() + tmpdir, err := os.MkdirTemp("", "TestGoExec") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + src := filepath.Join(tmpdir, "a.go") + file, err := os.Create(src) + if err != nil { + t.Fatal(err) + } + err = template.Must(template.New("main").Parse(testexec)).Execute(file, iscgo) + if e := file.Close(); err == nil { + err = e + } + if err != nil { + t.Fatal(err) + } + + exe := filepath.Join(tmpdir, "a.exe") + args := []string{"build", "-o", exe} + if iscgo { + linkmode := "internal" + if isexternallinker { + linkmode = "external" + } + args = append(args, "-ldflags", "-linkmode="+linkmode) + } + args = append(args, src) + out, err := testenv.Command(t, testenv.GoToolPath(t), args...).CombinedOutput() + if err != nil { + t.Fatalf("building test executable failed: %s %s", err, out) + } + + out, err = testenv.Command(t, exe).CombinedOutput() + if err != nil { + t.Fatalf("running test executable failed: %s %s", err, out) + } + names := make(map[string]string) + for _, line := range strings.Split(string(out), "\n") { + if line == "" { + continue + } + f := strings.Split(line, "=") + if len(f) != 2 { + t.Fatalf("unexpected output line: %q", line) + } + names["main."+f[0]] = f[1] + } + + runtimeSyms := map[string]string{ + "runtime.text": "T", + "runtime.etext": "T", + "runtime.rodata": "R", + "runtime.erodata": "R", + "runtime.epclntab": "R", + "runtime.noptrdata": "D", + } + + if runtime.GOOS == "aix" && iscgo { + // pclntab is moved to .data section on AIX. + runtimeSyms["runtime.epclntab"] = "D" + } + + out, err = testenv.Command(t, nmPath(t), exe).CombinedOutput() + if err != nil { + t.Fatalf("go tool nm: %v\n%s", err, string(out)) + } + + relocated := func(code string) bool { + if runtime.GOOS == "aix" { + // On AIX, .data and .bss addresses are changed by the loader. + // Therefore, the values returned by the exec aren't the same + // than the ones inside the symbol table. + // In case of cgo, .text symbols are also changed. + switch code { + case "T", "t", "R", "r": + return iscgo + case "D", "d", "B", "b": + return true + } + } + if runtime.GOOS == "windows" { + return true + } + if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { + return true // On darwin/arm64 everything is PIE + } + return false + } + + dups := make(map[string]bool) + for _, line := range strings.Split(string(out), "\n") { + f := strings.Fields(line) + if len(f) < 3 { + continue + } + name := f[2] + if addr, found := names[name]; found { + if want, have := addr, "0x"+f[0]; have != want { + if !relocated(f[1]) { + t.Errorf("want %s address for %s symbol, but have %s", want, name, have) + } + } + delete(names, name) + } + if _, found := dups[name]; found { + t.Errorf("duplicate name of %q is found", name) + } + if stype, found := runtimeSyms[name]; found { + if runtime.GOOS == "plan9" && stype == "R" { + // no read-only data segment symbol on Plan 9 + stype = "D" + } + if want, have := stype, strings.ToUpper(f[1]); have != want { + t.Errorf("want %s type for %s symbol, but have %s", want, name, have) + } + delete(runtimeSyms, name) + } + } + if len(names) > 0 { + t.Errorf("executable is missing %v symbols", names) + } + if len(runtimeSyms) > 0 { + t.Errorf("executable is missing %v symbols", runtimeSyms) + } +} + +func TestGoExec(t *testing.T) { + testGoExec(t, false, false) +} + +func testGoLib(t *testing.T, iscgo bool) { + t.Parallel() + tmpdir, err := os.MkdirTemp("", "TestGoLib") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + gopath := filepath.Join(tmpdir, "gopath") + libpath := filepath.Join(gopath, "src", "mylib") + + err = os.MkdirAll(libpath, 0777) + if err != nil { + t.Fatal(err) + } + src := filepath.Join(libpath, "a.go") + file, err := os.Create(src) + if err != nil { + t.Fatal(err) + } + err = template.Must(template.New("mylib").Parse(testlib)).Execute(file, iscgo) + if e := file.Close(); err == nil { + err = e + } + if err == nil { + err = os.WriteFile(filepath.Join(libpath, "go.mod"), []byte("module mylib\n"), 0666) + } + if err != nil { + t.Fatal(err) + } + + cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-buildmode=archive", "-o", "mylib.a", ".") + cmd.Dir = libpath + cmd.Env = append(os.Environ(), "GOPATH="+gopath) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("building test lib failed: %s %s", err, out) + } + mylib := filepath.Join(libpath, "mylib.a") + + out, err = testenv.Command(t, nmPath(t), mylib).CombinedOutput() + if err != nil { + t.Fatalf("go tool nm: %v\n%s", err, string(out)) + } + type symType struct { + Type string + Name string + CSym bool + Found bool + } + var syms = []symType{ + {"B", "mylib.Testdata", false, false}, + {"T", "mylib.Testfunc", false, false}, + } + if iscgo { + syms = append(syms, symType{"B", "mylib.TestCgodata", false, false}) + syms = append(syms, symType{"T", "mylib.TestCgofunc", false, false}) + if runtime.GOOS == "darwin" || runtime.GOOS == "ios" || (runtime.GOOS == "windows" && runtime.GOARCH == "386") { + syms = append(syms, symType{"D", "_cgodata", true, false}) + syms = append(syms, symType{"T", "_cgofunc", true, false}) + } else if runtime.GOOS == "aix" { + syms = append(syms, symType{"D", "cgodata", true, false}) + syms = append(syms, symType{"T", ".cgofunc", true, false}) + } else { + syms = append(syms, symType{"D", "cgodata", true, false}) + syms = append(syms, symType{"T", "cgofunc", true, false}) + } + } + + for _, line := range strings.Split(string(out), "\n") { + f := strings.Fields(line) + var typ, name string + var csym bool + if iscgo { + if len(f) < 4 { + continue + } + csym = !strings.Contains(f[0], "_go_.o") + typ = f[2] + name = f[3] + } else { + if len(f) < 3 { + continue + } + typ = f[1] + name = f[2] + } + for i := range syms { + sym := &syms[i] + if sym.Type == typ && sym.Name == name && sym.CSym == csym { + if sym.Found { + t.Fatalf("duplicate symbol %s %s", sym.Type, sym.Name) + } + sym.Found = true + } + } + } + for _, sym := range syms { + if !sym.Found { + t.Errorf("cannot found symbol %s %s", sym.Type, sym.Name) + } + } +} + +func TestGoLib(t *testing.T) { + testGoLib(t, false) +} + +const testexec = ` +package main + +import "fmt" +{{if .}}import "C" +{{end}} + +func main() { + testfunc() +} + +var testdata uint32 + +func testfunc() { + fmt.Printf("main=%p\n", main) + fmt.Printf("testfunc=%p\n", testfunc) + fmt.Printf("testdata=%p\n", &testdata) +} +` + +const testlib = ` +package mylib + +{{if .}} +// int cgodata = 5; +// void cgofunc(void) {} +import "C" + +var TestCgodata = C.cgodata + +func TestCgofunc() { + C.cgofunc() +} +{{end}} + +var Testdata uint32 + +func Testfunc() {} +` |