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
5package main
6
7import (
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.
23func 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.
34func 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
47var (
48	objdumpPathOnce sync.Once
49	objdumpExePath  string
50	objdumpPathErr  error
51)
52
53var x86Need = []string{ // for both 386 and AMD64
54	"JMP main.main(SB)",
55	"CALL main.Println(SB)",
56	"RET",
57}
58
59var amd64GnuNeed = []string{
60	"jmp",
61	"callq",
62	"cmpb",
63}
64
65var i386GnuNeed = []string{
66	"jmp",
67	"call",
68	"cmp",
69}
70
71var armNeed = []string{
72	"B main.main(SB)",
73	"BL main.Println(SB)",
74	"RET",
75}
76
77var arm64Need = []string{
78	"JMP main.main(SB)",
79	"CALL main.Println(SB)",
80	"RET",
81}
82
83var armGnuNeed = []string{ // for both ARM and AMR64
84	"ldr",
85	"bl",
86	"cmp",
87}
88
89var ppcNeed = []string{
90	"BR main.main(SB)",
91	"CALL main.Println(SB)",
92	"RET",
93}
94
95var ppcPIENeed = []string{
96	"BR",
97	"CALL",
98	"RET",
99}
100
101var ppcGnuNeed = []string{
102	"mflr",
103	"lbz",
104	"beq",
105}
106
107func 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
120var 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
131func 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
249func 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
257func TestDisasm(t *testing.T) {
258	testGoAndCgoDisasm(t, false, false)
259}
260
261func TestDisasmCode(t *testing.T) {
262	testGoAndCgoDisasm(t, true, false)
263}
264
265func TestDisasmGnuAsm(t *testing.T) {
266	testGoAndCgoDisasm(t, false, true)
267}
268
269func 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
279func 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
291func 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
341func 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
376func 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