Source file src/cmd/objdump/objdump_test.go

     1  // Copyright 2014 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"cmd/internal/notsha256"
     9  	"flag"
    10  	"fmt"
    11  	"internal/platform"
    12  	"internal/testenv"
    13  	"os"
    14  	"path/filepath"
    15  	"runtime"
    16  	"strings"
    17  	"sync"
    18  	"testing"
    19  )
    20  
    21  // TestMain executes the test binary as the objdump command if
    22  // GO_OBJDUMPTEST_IS_OBJDUMP is set, and runs the test otherwise.
    23  func TestMain(m *testing.M) {
    24  	if os.Getenv("GO_OBJDUMPTEST_IS_OBJDUMP") != "" {
    25  		main()
    26  		os.Exit(0)
    27  	}
    28  
    29  	os.Setenv("GO_OBJDUMPTEST_IS_OBJDUMP", "1")
    30  	os.Exit(m.Run())
    31  }
    32  
    33  // objdumpPath returns the path to the "objdump" binary to run.
    34  func objdumpPath(t testing.TB) string {
    35  	t.Helper()
    36  	testenv.MustHaveExec(t)
    37  
    38  	objdumpPathOnce.Do(func() {
    39  		objdumpExePath, objdumpPathErr = os.Executable()
    40  	})
    41  	if objdumpPathErr != nil {
    42  		t.Fatal(objdumpPathErr)
    43  	}
    44  	return objdumpExePath
    45  }
    46  
    47  var (
    48  	objdumpPathOnce sync.Once
    49  	objdumpExePath  string
    50  	objdumpPathErr  error
    51  )
    52  
    53  var x86Need = []string{ // for both 386 and AMD64
    54  	"JMP main.main(SB)",
    55  	"CALL main.Println(SB)",
    56  	"RET",
    57  }
    58  
    59  var amd64GnuNeed = []string{
    60  	"jmp",
    61  	"callq",
    62  	"cmpb",
    63  }
    64  
    65  var i386GnuNeed = []string{
    66  	"jmp",
    67  	"call",
    68  	"cmp",
    69  }
    70  
    71  var armNeed = []string{
    72  	"B main.main(SB)",
    73  	"BL main.Println(SB)",
    74  	"RET",
    75  }
    76  
    77  var arm64Need = []string{
    78  	"JMP main.main(SB)",
    79  	"CALL main.Println(SB)",
    80  	"RET",
    81  }
    82  
    83  var armGnuNeed = []string{ // for both ARM and AMR64
    84  	"ldr",
    85  	"bl",
    86  	"cmp",
    87  }
    88  
    89  var ppcNeed = []string{
    90  	"BR main.main(SB)",
    91  	"CALL main.Println(SB)",
    92  	"RET",
    93  }
    94  
    95  var ppcPIENeed = []string{
    96  	"BR",
    97  	"CALL",
    98  	"RET",
    99  }
   100  
   101  var ppcGnuNeed = []string{
   102  	"mflr",
   103  	"lbz",
   104  	"beq",
   105  }
   106  
   107  func mustHaveDisasm(t *testing.T) {
   108  	switch runtime.GOARCH {
   109  	case "loong64":
   110  		t.Skipf("skipping on %s", runtime.GOARCH)
   111  	case "mips", "mipsle", "mips64", "mips64le":
   112  		t.Skipf("skipping on %s, issue 12559", runtime.GOARCH)
   113  	case "riscv64":
   114  		t.Skipf("skipping on %s, issue 36738", runtime.GOARCH)
   115  	case "s390x":
   116  		t.Skipf("skipping on %s, issue 15255", runtime.GOARCH)
   117  	}
   118  }
   119  
   120  var target = flag.String("target", "", "test disassembly of `goos/goarch` binary")
   121  
   122  // objdump is fully cross platform: it can handle binaries
   123  // from any known operating system and architecture.
   124  // We could in principle add binaries to testdata and check
   125  // all the supported systems during this test. However, the
   126  // binaries would be about 1 MB each, and we don't want to
   127  // add that much junk to the hg repository. Instead, build a
   128  // binary for the current system (only) and test that objdump
   129  // can handle that one.
   130  
   131  func testDisasm(t *testing.T, srcfname string, printCode bool, printGnuAsm bool, flags ...string) {
   132  	mustHaveDisasm(t)
   133  	goarch := runtime.GOARCH
   134  	if *target != "" {
   135  		f := strings.Split(*target, "/")
   136  		if len(f) != 2 {
   137  			t.Fatalf("-target argument must be goos/goarch")
   138  		}
   139  		defer os.Setenv("GOOS", os.Getenv("GOOS"))
   140  		defer os.Setenv("GOARCH", os.Getenv("GOARCH"))
   141  		os.Setenv("GOOS", f[0])
   142  		os.Setenv("GOARCH", f[1])
   143  		goarch = f[1]
   144  	}
   145  
   146  	hash := notsha256.Sum256([]byte(fmt.Sprintf("%v-%v-%v-%v", srcfname, flags, printCode, printGnuAsm)))
   147  	tmp := t.TempDir()
   148  	hello := filepath.Join(tmp, fmt.Sprintf("hello-%x.exe", hash))
   149  	args := []string{"build", "-o", hello}
   150  	args = append(args, flags...)
   151  	args = append(args, srcfname)
   152  	cmd := testenv.Command(t, testenv.GoToolPath(t), args...)
   153  	// "Bad line" bug #36683 is sensitive to being run in the source directory.
   154  	cmd.Dir = "testdata"
   155  	t.Logf("Running %v", cmd.Args)
   156  	out, err := cmd.CombinedOutput()
   157  	if err != nil {
   158  		t.Fatalf("go build %s: %v\n%s", srcfname, err, out)
   159  	}
   160  	need := []string{
   161  		"TEXT main.main(SB)",
   162  	}
   163  
   164  	if printCode {
   165  		need = append(need, `	Println("hello, world")`)
   166  	} else {
   167  		need = append(need, srcfname+":6")
   168  	}
   169  
   170  	switch goarch {
   171  	case "amd64", "386":
   172  		need = append(need, x86Need...)
   173  	case "arm":
   174  		need = append(need, armNeed...)
   175  	case "arm64":
   176  		need = append(need, arm64Need...)
   177  	case "ppc64", "ppc64le":
   178  		var pie bool
   179  		for _, flag := range flags {
   180  			if flag == "-buildmode=pie" {
   181  				pie = true
   182  				break
   183  			}
   184  		}
   185  		if pie {
   186  			// In PPC64 PIE binaries we use a "local entry point" which is
   187  			// function symbol address + 8. Currently we don't symbolize that.
   188  			// Expect a different output.
   189  			need = append(need, ppcPIENeed...)
   190  		} else {
   191  			need = append(need, ppcNeed...)
   192  		}
   193  	}
   194  
   195  	if printGnuAsm {
   196  		switch goarch {
   197  		case "amd64":
   198  			need = append(need, amd64GnuNeed...)
   199  		case "386":
   200  			need = append(need, i386GnuNeed...)
   201  		case "arm", "arm64":
   202  			need = append(need, armGnuNeed...)
   203  		case "ppc64", "ppc64le":
   204  			need = append(need, ppcGnuNeed...)
   205  		}
   206  	}
   207  	args = []string{
   208  		"-s", "main.main",
   209  		hello,
   210  	}
   211  
   212  	if printCode {
   213  		args = append([]string{"-S"}, args...)
   214  	}
   215  
   216  	if printGnuAsm {
   217  		args = append([]string{"-gnu"}, args...)
   218  	}
   219  	cmd = testenv.Command(t, objdumpPath(t), args...)
   220  	cmd.Dir = "testdata" // "Bad line" bug #36683 is sensitive to being run in the source directory
   221  	out, err = cmd.CombinedOutput()
   222  	t.Logf("Running %v", cmd.Args)
   223  
   224  	if err != nil {
   225  		exename := srcfname[:len(srcfname)-len(filepath.Ext(srcfname))] + ".exe"
   226  		t.Fatalf("objdump %q: %v\n%s", exename, err, out)
   227  	}
   228  
   229  	text := string(out)
   230  	ok := true
   231  	for _, s := range need {
   232  		if !strings.Contains(text, s) {
   233  			t.Errorf("disassembly missing '%s'", s)
   234  			ok = false
   235  		}
   236  	}
   237  	if goarch == "386" {
   238  		if strings.Contains(text, "(IP)") {
   239  			t.Errorf("disassembly contains PC-Relative addressing on 386")
   240  			ok = false
   241  		}
   242  	}
   243  
   244  	if !ok || testing.Verbose() {
   245  		t.Logf("full disassembly:\n%s", text)
   246  	}
   247  }
   248  
   249  func testGoAndCgoDisasm(t *testing.T, printCode bool, printGnuAsm bool) {
   250  	t.Parallel()
   251  	testDisasm(t, "fmthello.go", printCode, printGnuAsm)
   252  	if testenv.HasCGO() {
   253  		testDisasm(t, "fmthellocgo.go", printCode, printGnuAsm)
   254  	}
   255  }
   256  
   257  func TestDisasm(t *testing.T) {
   258  	testGoAndCgoDisasm(t, false, false)
   259  }
   260  
   261  func TestDisasmCode(t *testing.T) {
   262  	testGoAndCgoDisasm(t, true, false)
   263  }
   264  
   265  func TestDisasmGnuAsm(t *testing.T) {
   266  	testGoAndCgoDisasm(t, false, true)
   267  }
   268  
   269  func TestDisasmExtld(t *testing.T) {
   270  	testenv.MustHaveCGO(t)
   271  	switch runtime.GOOS {
   272  	case "plan9":
   273  		t.Skipf("skipping on %s", runtime.GOOS)
   274  	}
   275  	t.Parallel()
   276  	testDisasm(t, "fmthello.go", false, false, "-ldflags=-linkmode=external")
   277  }
   278  
   279  func TestDisasmPIE(t *testing.T) {
   280  	if !platform.BuildModeSupported("gc", "pie", runtime.GOOS, runtime.GOARCH) {
   281  		t.Skipf("skipping on %s/%s, PIE buildmode not supported", runtime.GOOS, runtime.GOARCH)
   282  	}
   283  	if !platform.InternalLinkPIESupported(runtime.GOOS, runtime.GOARCH) {
   284  		// require cgo on platforms that PIE needs external linking
   285  		testenv.MustHaveCGO(t)
   286  	}
   287  	t.Parallel()
   288  	testDisasm(t, "fmthello.go", false, false, "-buildmode=pie")
   289  }
   290  
   291  func TestDisasmGoobj(t *testing.T) {
   292  	mustHaveDisasm(t)
   293  	testenv.MustHaveGoBuild(t)
   294  
   295  	tmp := t.TempDir()
   296  
   297  	importcfgfile := filepath.Join(tmp, "hello.importcfg")
   298  	testenv.WriteImportcfg(t, importcfgfile, nil, "testdata/fmthello.go")
   299  
   300  	hello := filepath.Join(tmp, "hello.o")
   301  	args := []string{"tool", "compile", "-p=main", "-importcfg=" + importcfgfile, "-o", hello}
   302  	args = append(args, "testdata/fmthello.go")
   303  	out, err := testenv.Command(t, testenv.GoToolPath(t), args...).CombinedOutput()
   304  	if err != nil {
   305  		t.Fatalf("go tool compile fmthello.go: %v\n%s", err, out)
   306  	}
   307  	need := []string{
   308  		"main(SB)",
   309  		"fmthello.go:6",
   310  	}
   311  
   312  	args = []string{
   313  		"-s", "main",
   314  		hello,
   315  	}
   316  
   317  	out, err = testenv.Command(t, objdumpPath(t), args...).CombinedOutput()
   318  	if err != nil {
   319  		t.Fatalf("objdump fmthello.o: %v\n%s", err, out)
   320  	}
   321  
   322  	text := string(out)
   323  	ok := true
   324  	for _, s := range need {
   325  		if !strings.Contains(text, s) {
   326  			t.Errorf("disassembly missing '%s'", s)
   327  			ok = false
   328  		}
   329  	}
   330  	if runtime.GOARCH == "386" {
   331  		if strings.Contains(text, "(IP)") {
   332  			t.Errorf("disassembly contains PC-Relative addressing on 386")
   333  			ok = false
   334  		}
   335  	}
   336  	if !ok {
   337  		t.Logf("full disassembly:\n%s", text)
   338  	}
   339  }
   340  
   341  func TestGoobjFileNumber(t *testing.T) {
   342  	// Test that file table in Go object file is parsed correctly.
   343  	testenv.MustHaveGoBuild(t)
   344  	mustHaveDisasm(t)
   345  
   346  	t.Parallel()
   347  
   348  	tmp := t.TempDir()
   349  
   350  	obj := filepath.Join(tmp, "p.a")
   351  	cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", obj)
   352  	cmd.Dir = filepath.Join("testdata/testfilenum")
   353  	out, err := cmd.CombinedOutput()
   354  	if err != nil {
   355  		t.Fatalf("build failed: %v\n%s", err, out)
   356  	}
   357  
   358  	cmd = testenv.Command(t, objdumpPath(t), obj)
   359  	out, err = cmd.CombinedOutput()
   360  	if err != nil {
   361  		t.Fatalf("objdump failed: %v\n%s", err, out)
   362  	}
   363  
   364  	text := string(out)
   365  	for _, s := range []string{"a.go", "b.go", "c.go"} {
   366  		if !strings.Contains(text, s) {
   367  			t.Errorf("output missing '%s'", s)
   368  		}
   369  	}
   370  
   371  	if t.Failed() {
   372  		t.Logf("output:\n%s", text)
   373  	}
   374  }
   375  
   376  func TestGoObjOtherVersion(t *testing.T) {
   377  	testenv.MustHaveExec(t)
   378  	t.Parallel()
   379  
   380  	obj := filepath.Join("testdata", "go116.o")
   381  	cmd := testenv.Command(t, objdumpPath(t), obj)
   382  	out, err := cmd.CombinedOutput()
   383  	if err == nil {
   384  		t.Fatalf("objdump go116.o succeeded unexpectedly")
   385  	}
   386  	if !strings.Contains(string(out), "go object of a different version") {
   387  		t.Errorf("unexpected error message:\n%s", out)
   388  	}
   389  }
   390  

View as plain text