nm

nm

原文:https://pkg.go.dev/cmd/nm@go1.19.3

Overview 概述

Nm lists the symbols defined or used by an object file, archive, or executable.

nm列出对象文件、存案或可执行文件所定义或使用的符号。

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
	=> bss 段符号
	
b	static bss segment symbol
	=> 静态bss 段符号
	
C	constant address
	=> 常量地址
	
U	referenced but undefined symbol
	=> 被引用但未定义的符号

Following established convention, the address is omitted for undefined symbols (type U).

​ 按照既定惯例,未定义的符号(U类型)省略了地址。

The options control the printed output:

​ 以下这些选项(options )控制打印输出:

-n
	an alias for -sort address (numeric),
	for compatibility with other nm commands
	=> 	是 -sort 地址(数字)的别名,
		为了与其他nm命令兼容

-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
	=> 按照给定的顺序对输出进行排序(默认为名称)。
	size的顺序从大到小排序

-type
	print symbol type after name
	=> 	在名称之后打印符号类型

=== “doc.go”

```go 
// 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
```

=== “nm.go”

```go 
// 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.Fprintf(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()
}
```

=== “nm_test.go”

```go 
// 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":
            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)
}
```

=== “nm_cgo_test.go”

```go 
// 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 (
    "fmt"
    "internal/obscuretestdata"
    "internal/testenv"
    "os"
    "os/exec"
    "path/filepath"
    "runtime"
    "strings"
    "testing"
    "text/template"
)

var testnmpath string // path to nm command created for testing purposes

// The TestMain function creates a nm command for testing purposes and
// deletes it after the tests have been run.
func TestMain(m *testing.M) {
    os.Exit(testMain(m))
}

func testMain(m *testing.M) int {
    if !testenv.HasGoBuild() {
        return 0
    }

    tmpDir, err := os.MkdirTemp("", "TestNM")
    if err != nil {
        fmt.Println("TempDir failed:", err)
        return 2
    }
    defer os.RemoveAll(tmpDir)

    testnmpath = filepath.Join(tmpDir, "testnm.exe")
    gotool, err := testenv.GoTool()
    if err != nil {
        fmt.Println("GoTool failed:", err)
        return 2
    }
    out, err := exec.Command(gotool, "build", "-o", testnmpath, "cmd/nm").CombinedOutput()
    if err != nil {
        fmt.Printf("go build -o %v cmd/nm: %v\n%s", testnmpath, err, string(out))
        return 2
    }

    return m.Run()
}

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 := exec.Command(testnmpath, 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 := exec.Command(testenv.GoToolPath(t), args...).CombinedOutput()
    if err != nil {
        t.Fatalf("building test executable failed: %s %s", err, out)
    }

    out, err = exec.Command(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 = exec.Command(testnmpath, 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 := exec.Command(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 = exec.Command(testnmpath, 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() {}
`
```
最后修改 March 10, 2024: 更新 (ddf4687)