1// Copyright 2017 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	"bytes"
9	cmddwarf "cmd/internal/dwarf"
10	"cmd/internal/objfile"
11	"cmd/internal/quoted"
12	"debug/dwarf"
13	"internal/platform"
14	"internal/testenv"
15	"os"
16	"os/exec"
17	"path"
18	"path/filepath"
19	"runtime"
20	"strings"
21	"testing"
22)
23
24// TestMain allows this test binary to run as a -toolexec wrapper for the 'go'
25// command. If LINK_TEST_TOOLEXEC is set, TestMain runs the binary as if it were
26// cmd/link, and otherwise runs the requested tool as a subprocess.
27//
28// This allows the test to verify the behavior of the current contents of the
29// cmd/link package even if the installed cmd/link binary is stale.
30func TestMain(m *testing.M) {
31	if os.Getenv("LINK_TEST_TOOLEXEC") == "" {
32		// Not running as a -toolexec wrapper. Just run the tests.
33		os.Exit(m.Run())
34	}
35
36	if strings.TrimSuffix(filepath.Base(os.Args[1]), ".exe") == "link" {
37		// Running as a -toolexec linker, and the tool is cmd/link.
38		// Substitute this test binary for the linker.
39		os.Args = os.Args[1:]
40		main()
41		os.Exit(0)
42	}
43
44	cmd := exec.Command(os.Args[1], os.Args[2:]...)
45	cmd.Stdin = os.Stdin
46	cmd.Stdout = os.Stdout
47	cmd.Stderr = os.Stderr
48	if err := cmd.Run(); err != nil {
49		os.Exit(1)
50	}
51	os.Exit(0)
52}
53
54func testDWARF(t *testing.T, buildmode string, expectDWARF bool, env ...string) {
55	testenv.MustHaveCGO(t)
56	testenv.MustHaveGoBuild(t)
57
58	if !platform.ExecutableHasDWARF(runtime.GOOS, runtime.GOARCH) {
59		t.Skipf("skipping on %s/%s: no DWARF symbol table in executables", runtime.GOOS, runtime.GOARCH)
60	}
61
62	t.Parallel()
63
64	for _, prog := range []string{"testprog", "testprogcgo"} {
65		prog := prog
66		expectDWARF := expectDWARF
67		if runtime.GOOS == "aix" && prog == "testprogcgo" {
68			extld := os.Getenv("CC")
69			if extld == "" {
70				extld = "gcc"
71			}
72			extldArgs, err := quoted.Split(extld)
73			if err != nil {
74				t.Fatal(err)
75			}
76			expectDWARF, err = cmddwarf.IsDWARFEnabledOnAIXLd(extldArgs)
77			if err != nil {
78				t.Fatal(err)
79			}
80		}
81
82		t.Run(prog, func(t *testing.T) {
83			t.Parallel()
84
85			tmpDir := t.TempDir()
86
87			exe := filepath.Join(tmpDir, prog+".exe")
88			dir := "../../runtime/testdata/" + prog
89			cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-toolexec", os.Args[0], "-o", exe)
90			if buildmode != "" {
91				cmd.Args = append(cmd.Args, "-buildmode", buildmode)
92			}
93			cmd.Args = append(cmd.Args, dir)
94			cmd.Env = append(os.Environ(), env...)
95			cmd.Env = append(cmd.Env, "CGO_CFLAGS=") // ensure CGO_CFLAGS does not contain any flags. Issue #35459
96			cmd.Env = append(cmd.Env, "LINK_TEST_TOOLEXEC=1")
97			out, err := cmd.CombinedOutput()
98			if err != nil {
99				t.Fatalf("go build -o %v %v: %v\n%s", exe, dir, err, out)
100			}
101
102			if buildmode == "c-archive" {
103				// Extract the archive and use the go.o object within.
104				cmd := testenv.Command(t, "ar", "-x", exe)
105				cmd.Dir = tmpDir
106				if out, err := cmd.CombinedOutput(); err != nil {
107					t.Fatalf("ar -x %s: %v\n%s", exe, err, out)
108				}
109				exe = filepath.Join(tmpDir, "go.o")
110			}
111
112			darwinSymbolTestIsTooFlaky := true // Turn this off, it is too flaky -- See #32218
113			if runtime.GOOS == "darwin" && !darwinSymbolTestIsTooFlaky {
114				if _, err = exec.LookPath("symbols"); err == nil {
115					// Ensure Apple's tooling can parse our object for symbols.
116					out, err = testenv.Command(t, "symbols", exe).CombinedOutput()
117					if err != nil {
118						t.Fatalf("symbols %v: %v: %s", filepath.Base(exe), err, out)
119					} else {
120						if bytes.HasPrefix(out, []byte("Unable to find file")) {
121							// This failure will cause the App Store to reject our binaries.
122							t.Fatalf("symbols %v: failed to parse file", filepath.Base(exe))
123						} else if bytes.Contains(out, []byte(", Empty]")) {
124							t.Fatalf("symbols %v: parsed as empty", filepath.Base(exe))
125						}
126					}
127				}
128			}
129
130			f, err := objfile.Open(exe)
131			if err != nil {
132				t.Fatal(err)
133			}
134			defer f.Close()
135
136			syms, err := f.Symbols()
137			if err != nil {
138				t.Fatal(err)
139			}
140
141			var addr uint64
142			for _, sym := range syms {
143				if sym.Name == "main.main" {
144					addr = sym.Addr
145					break
146				}
147			}
148			if addr == 0 {
149				t.Fatal("cannot find main.main in symbols")
150			}
151
152			d, err := f.DWARF()
153			if err != nil {
154				if expectDWARF {
155					t.Fatal(err)
156				}
157				return
158			} else {
159				if !expectDWARF {
160					t.Fatal("unexpected DWARF section")
161				}
162			}
163
164			// TODO: We'd like to use filepath.Join here.
165			// Also related: golang.org/issue/19784.
166			wantFile := path.Join(prog, "main.go")
167			wantLine := 24
168			r := d.Reader()
169			entry, err := r.SeekPC(addr)
170			if err != nil {
171				t.Fatal(err)
172			}
173			lr, err := d.LineReader(entry)
174			if err != nil {
175				t.Fatal(err)
176			}
177			var line dwarf.LineEntry
178			if err := lr.SeekPC(addr, &line); err == dwarf.ErrUnknownPC {
179				t.Fatalf("did not find file:line for %#x (main.main)", addr)
180			} else if err != nil {
181				t.Fatal(err)
182			}
183			if !strings.HasSuffix(line.File.Name, wantFile) || line.Line != wantLine {
184				t.Errorf("%#x is %s:%d, want %s:%d", addr, line.File.Name, line.Line, filepath.Join("...", wantFile), wantLine)
185			}
186		})
187	}
188}
189
190func TestDWARF(t *testing.T) {
191	testDWARF(t, "", true)
192	if !testing.Short() {
193		if runtime.GOOS == "windows" {
194			t.Skip("skipping Windows/c-archive; see Issue 35512 for more.")
195		}
196		if !platform.BuildModeSupported(runtime.Compiler, "c-archive", runtime.GOOS, runtime.GOARCH) {
197			t.Skipf("skipping c-archive test on unsupported platform %s-%s", runtime.GOOS, runtime.GOARCH)
198		}
199		t.Run("c-archive", func(t *testing.T) {
200			testDWARF(t, "c-archive", true)
201		})
202	}
203}
204
205func TestDWARFiOS(t *testing.T) {
206	// Normally we run TestDWARF on native platform. But on iOS we don't have
207	// go build, so we do this test with a cross build.
208	// Only run this on darwin/amd64, where we can cross build for iOS.
209	if testing.Short() {
210		t.Skip("skipping in short mode")
211	}
212	if runtime.GOARCH != "amd64" || runtime.GOOS != "darwin" {
213		t.Skip("skipping on non-darwin/amd64 platform")
214	}
215	if err := testenv.Command(t, "xcrun", "--help").Run(); err != nil {
216		t.Skipf("error running xcrun, required for iOS cross build: %v", err)
217	}
218	// Check to see if the ios tools are installed. It's possible to have the command line tools
219	// installed without the iOS sdk.
220	if output, err := testenv.Command(t, "xcodebuild", "-showsdks").CombinedOutput(); err != nil {
221		t.Skipf("error running xcodebuild, required for iOS cross build: %v", err)
222	} else if !strings.Contains(string(output), "iOS SDK") {
223		t.Skipf("iOS SDK not detected.")
224	}
225	cc := "CC=" + runtime.GOROOT() + "/misc/ios/clangwrap.sh"
226	// iOS doesn't allow unmapped segments, so iOS executables don't have DWARF.
227	t.Run("exe", func(t *testing.T) {
228		testDWARF(t, "", false, cc, "CGO_ENABLED=1", "GOOS=ios", "GOARCH=arm64")
229	})
230	// However, c-archive iOS objects have embedded DWARF.
231	t.Run("c-archive", func(t *testing.T) {
232		testDWARF(t, "c-archive", true, cc, "CGO_ENABLED=1", "GOOS=ios", "GOARCH=arm64")
233	})
234}
235