blob: 27285ff566ead9023f825ec8a12ac7664f29b4d0 [file] [log] [blame]
// Copyright 2019 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 dragonfly || freebsd || linux || netbsd || openbsd
package main
import (
"debug/elf"
"fmt"
"internal/platform"
"internal/testenv"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"text/template"
)
func getCCAndCCFLAGS(t *testing.T, env []string) (string, []string) {
goTool := testenv.GoToolPath(t)
cmd := testenv.Command(t, goTool, "env", "CC")
cmd.Env = env
ccb, err := cmd.Output()
if err != nil {
t.Fatal(err)
}
cc := strings.TrimSpace(string(ccb))
cmd = testenv.Command(t, goTool, "env", "GOGCCFLAGS")
cmd.Env = env
cflagsb, err := cmd.Output()
if err != nil {
t.Fatal(err)
}
cflags := strings.Fields(string(cflagsb))
return cc, cflags
}
var asmSource = `
.section .text1,"ax"
s1:
.byte 0
.section .text2,"ax"
s2:
.byte 0
`
var goSource = `
package main
func main() {}
`
// The linker used to crash if an ELF input file had multiple text sections
// with the same name.
func TestSectionsWithSameName(t *testing.T) {
testenv.MustHaveGoBuild(t)
testenv.MustHaveCGO(t)
t.Parallel()
objcopy, err := exec.LookPath("objcopy")
if err != nil {
t.Skipf("can't find objcopy: %v", err)
}
dir := t.TempDir()
gopath := filepath.Join(dir, "GOPATH")
env := append(os.Environ(), "GOPATH="+gopath)
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module elf_test\n"), 0666); err != nil {
t.Fatal(err)
}
asmFile := filepath.Join(dir, "x.s")
if err := os.WriteFile(asmFile, []byte(asmSource), 0444); err != nil {
t.Fatal(err)
}
goTool := testenv.GoToolPath(t)
cc, cflags := getCCAndCCFLAGS(t, env)
asmObj := filepath.Join(dir, "x.o")
t.Logf("%s %v -c -o %s %s", cc, cflags, asmObj, asmFile)
if out, err := testenv.Command(t, cc, append(cflags, "-c", "-o", asmObj, asmFile)...).CombinedOutput(); err != nil {
t.Logf("%s", out)
t.Fatal(err)
}
asm2Obj := filepath.Join(dir, "x2.syso")
t.Logf("%s --rename-section .text2=.text1 %s %s", objcopy, asmObj, asm2Obj)
if out, err := testenv.Command(t, objcopy, "--rename-section", ".text2=.text1", asmObj, asm2Obj).CombinedOutput(); err != nil {
t.Logf("%s", out)
t.Fatal(err)
}
for _, s := range []string{asmFile, asmObj} {
if err := os.Remove(s); err != nil {
t.Fatal(err)
}
}
goFile := filepath.Join(dir, "main.go")
if err := os.WriteFile(goFile, []byte(goSource), 0444); err != nil {
t.Fatal(err)
}
cmd := testenv.Command(t, goTool, "build")
cmd.Dir = dir
cmd.Env = env
t.Logf("%s build", goTool)
if out, err := cmd.CombinedOutput(); err != nil {
t.Logf("%s", out)
t.Fatal(err)
}
}
var cSources35779 = []string{`
static int blah() { return 42; }
int Cfunc1() { return blah(); }
`, `
static int blah() { return 42; }
int Cfunc2() { return blah(); }
`,
}
// TestMinusRSymsWithSameName tests a corner case in the new
// loader. Prior to the fix this failed with the error 'loadelf:
// $WORK/b001/_pkg_.a(ldr.syso): duplicate symbol reference: blah in
// both main(.text) and main(.text)'. See issue #35779.
func TestMinusRSymsWithSameName(t *testing.T) {
testenv.MustHaveGoBuild(t)
testenv.MustHaveCGO(t)
t.Parallel()
dir := t.TempDir()
gopath := filepath.Join(dir, "GOPATH")
env := append(os.Environ(), "GOPATH="+gopath)
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module elf_test\n"), 0666); err != nil {
t.Fatal(err)
}
goTool := testenv.GoToolPath(t)
cc, cflags := getCCAndCCFLAGS(t, env)
objs := []string{}
csrcs := []string{}
for i, content := range cSources35779 {
csrcFile := filepath.Join(dir, fmt.Sprintf("x%d.c", i))
csrcs = append(csrcs, csrcFile)
if err := os.WriteFile(csrcFile, []byte(content), 0444); err != nil {
t.Fatal(err)
}
obj := filepath.Join(dir, fmt.Sprintf("x%d.o", i))
objs = append(objs, obj)
t.Logf("%s %v -c -o %s %s", cc, cflags, obj, csrcFile)
if out, err := testenv.Command(t, cc, append(cflags, "-c", "-o", obj, csrcFile)...).CombinedOutput(); err != nil {
t.Logf("%s", out)
t.Fatal(err)
}
}
sysoObj := filepath.Join(dir, "ldr.syso")
t.Logf("%s %v -nostdlib -r -o %s %v", cc, cflags, sysoObj, objs)
if out, err := testenv.Command(t, cc, append(cflags, "-nostdlib", "-r", "-o", sysoObj, objs[0], objs[1])...).CombinedOutput(); err != nil {
t.Logf("%s", out)
t.Fatal(err)
}
cruft := [][]string{objs, csrcs}
for _, sl := range cruft {
for _, s := range sl {
if err := os.Remove(s); err != nil {
t.Fatal(err)
}
}
}
goFile := filepath.Join(dir, "main.go")
if err := os.WriteFile(goFile, []byte(goSource), 0444); err != nil {
t.Fatal(err)
}
t.Logf("%s build", goTool)
cmd := testenv.Command(t, goTool, "build")
cmd.Dir = dir
cmd.Env = env
if out, err := cmd.CombinedOutput(); err != nil {
t.Logf("%s", out)
t.Fatal(err)
}
}
func TestMergeNoteSections(t *testing.T) {
testenv.MustHaveGoBuild(t)
expected := 1
switch runtime.GOOS {
case "linux", "dragonfly":
case "openbsd", "netbsd", "freebsd":
// These OSes require independent segment
expected = 2
default:
t.Skip("We should only test on elf output.")
}
t.Parallel()
goFile := filepath.Join(t.TempDir(), "notes.go")
if err := os.WriteFile(goFile, []byte(goSource), 0444); err != nil {
t.Fatal(err)
}
outFile := filepath.Join(t.TempDir(), "notes.exe")
goTool := testenv.GoToolPath(t)
// sha1sum of "gopher"
id := "0xf4e8cd51ce8bae2996dc3b74639cdeaa1f7fee5f"
cmd := testenv.Command(t, goTool, "build", "-o", outFile, "-ldflags",
"-B "+id, goFile)
cmd.Dir = t.TempDir()
if out, err := cmd.CombinedOutput(); err != nil {
t.Logf("%s", out)
t.Fatal(err)
}
ef, err := elf.Open(outFile)
if err != nil {
t.Fatalf("open elf file failed:%v", err)
}
defer ef.Close()
sec := ef.Section(".note.gnu.build-id")
if sec == nil {
t.Fatalf("can't find gnu build id")
}
sec = ef.Section(".note.go.buildid")
if sec == nil {
t.Fatalf("can't find go build id")
}
cnt := 0
for _, ph := range ef.Progs {
if ph.Type == elf.PT_NOTE {
cnt += 1
}
}
if cnt != expected {
t.Fatalf("want %d PT_NOTE segment, got %d", expected, cnt)
}
}
const pieSourceTemplate = `
package main
import "fmt"
// Force the creation of a lot of type descriptors that will go into
// the .data.rel.ro section.
{{range $index, $element := .}}var V{{$index}} interface{} = [{{$index}}]int{}
{{end}}
func main() {
{{range $index, $element := .}} fmt.Println(V{{$index}})
{{end}}
}
`
func TestPIESize(t *testing.T) {
testenv.MustHaveGoBuild(t)
// We don't want to test -linkmode=external if cgo is not supported.
// On some systems -buildmode=pie implies -linkmode=external, so just
// always skip the test if cgo is not supported.
testenv.MustHaveCGO(t)
if !platform.BuildModeSupported(runtime.Compiler, "pie", runtime.GOOS, runtime.GOARCH) {
t.Skip("-buildmode=pie not supported")
}
t.Parallel()
tmpl := template.Must(template.New("pie").Parse(pieSourceTemplate))
writeGo := func(t *testing.T, dir string) {
f, err := os.Create(filepath.Join(dir, "pie.go"))
if err != nil {
t.Fatal(err)
}
// Passing a 100-element slice here will cause
// pieSourceTemplate to create 100 variables with
// different types.
if err := tmpl.Execute(f, make([]byte, 100)); err != nil {
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}
}
for _, external := range []bool{false, true} {
external := external
name := "TestPieSize-"
if external {
name += "external"
} else {
name += "internal"
}
t.Run(name, func(t *testing.T) {
t.Parallel()
dir := t.TempDir()
writeGo(t, dir)
binexe := filepath.Join(dir, "exe")
binpie := filepath.Join(dir, "pie")
if external {
binexe += "external"
binpie += "external"
}
build := func(bin, mode string) error {
cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", bin, "-buildmode="+mode)
if external {
cmd.Args = append(cmd.Args, "-ldflags=-linkmode=external")
}
cmd.Args = append(cmd.Args, "pie.go")
cmd.Dir = dir
t.Logf("%v", cmd.Args)
out, err := cmd.CombinedOutput()
if len(out) > 0 {
t.Logf("%s", out)
}
if err != nil {
t.Error(err)
}
return err
}
var errexe, errpie error
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
errexe = build(binexe, "exe")
}()
go func() {
defer wg.Done()
errpie = build(binpie, "pie")
}()
wg.Wait()
if errexe != nil || errpie != nil {
t.Fatal("link failed")
}
var sizeexe, sizepie uint64
if fi, err := os.Stat(binexe); err != nil {
t.Fatal(err)
} else {
sizeexe = uint64(fi.Size())
}
if fi, err := os.Stat(binpie); err != nil {
t.Fatal(err)
} else {
sizepie = uint64(fi.Size())
}
elfexe, err := elf.Open(binexe)
if err != nil {
t.Fatal(err)
}
defer elfexe.Close()
elfpie, err := elf.Open(binpie)
if err != nil {
t.Fatal(err)
}
defer elfpie.Close()
// The difference in size between exe and PIE
// should be approximately the difference in
// size of the .text section plus the size of
// the PIE dynamic data sections plus the
// difference in size of the .got and .plt
// sections if they exist.
// We ignore unallocated sections.
// There may be gaps between non-writeable and
// writable PT_LOAD segments. We also skip those
// gaps (see issue #36023).
textsize := func(ef *elf.File, name string) uint64 {
for _, s := range ef.Sections {
if s.Name == ".text" {
return s.Size
}
}
t.Fatalf("%s: no .text section", name)
return 0
}
textexe := textsize(elfexe, binexe)
textpie := textsize(elfpie, binpie)
dynsize := func(ef *elf.File) uint64 {
var ret uint64
for _, s := range ef.Sections {
if s.Flags&elf.SHF_ALLOC == 0 {
continue
}
switch s.Type {
case elf.SHT_DYNSYM, elf.SHT_STRTAB, elf.SHT_REL, elf.SHT_RELA, elf.SHT_HASH, elf.SHT_GNU_HASH, elf.SHT_GNU_VERDEF, elf.SHT_GNU_VERNEED, elf.SHT_GNU_VERSYM:
ret += s.Size
}
if s.Flags&elf.SHF_WRITE != 0 && (strings.Contains(s.Name, ".got") || strings.Contains(s.Name, ".plt")) {
ret += s.Size
}
}
return ret
}
dynexe := dynsize(elfexe)
dynpie := dynsize(elfpie)
extrasize := func(ef *elf.File) uint64 {
var ret uint64
// skip unallocated sections
for _, s := range ef.Sections {
if s.Flags&elf.SHF_ALLOC == 0 {
ret += s.Size
}
}
// also skip gaps between PT_LOAD segments
var prev *elf.Prog
for _, seg := range ef.Progs {
if seg.Type != elf.PT_LOAD {
continue
}
if prev != nil {
ret += seg.Off - prev.Off - prev.Filesz
}
prev = seg
}
return ret
}
extraexe := extrasize(elfexe)
extrapie := extrasize(elfpie)
if sizepie < sizeexe || sizepie-extrapie < sizeexe-extraexe {
return
}
diffReal := (sizepie - extrapie) - (sizeexe - extraexe)
diffExpected := (textpie + dynpie) - (textexe + dynexe)
t.Logf("real size difference %#x, expected %#x", diffReal, diffExpected)
if diffReal > (diffExpected + diffExpected/10) {
t.Errorf("PIE unexpectedly large: got difference of %d (%d - %d), expected difference %d", diffReal, sizepie, sizeexe, diffExpected)
}
})
}
}
func TestIssue51939(t *testing.T) {
testenv.MustHaveGoBuild(t)
t.Parallel()
td := t.TempDir()
goFile := filepath.Join(td, "issue51939.go")
if err := os.WriteFile(goFile, []byte(goSource), 0444); err != nil {
t.Fatal(err)
}
outFile := filepath.Join(td, "issue51939.exe")
goTool := testenv.GoToolPath(t)
cmd := testenv.Command(t, goTool, "build", "-o", outFile, goFile)
if out, err := cmd.CombinedOutput(); err != nil {
t.Logf("%s", out)
t.Fatal(err)
}
ef, err := elf.Open(outFile)
if err != nil {
t.Fatal(err)
}
for _, s := range ef.Sections {
if s.Flags&elf.SHF_ALLOC == 0 && s.Addr != 0 {
t.Errorf("section %s should not allocated with addr %x", s.Name, s.Addr)
}
}
}