diff --git a/src/cmd/go/internal/work/buildid.go b/src/cmd/go/internal/work/buildid.go index c685263141..39ca20ee4f 100644 --- a/src/cmd/go/internal/work/buildid.go +++ b/src/cmd/go/internal/work/buildid.go @@ -7,6 +7,7 @@ package work import ( "bytes" "fmt" + "io/ioutil" "os" "os/exec" "strings" @@ -203,6 +204,132 @@ func (b *Builder) toolID(name string) string { return id } +// gccToolID returns the unique ID to use for a tool that is invoked +// by the GCC driver. This is in particular gccgo, but this can also +// be used for gcc, g++, gfortran, etc.; those tools all use the GCC +// driver under different names. The approach used here should also +// work for sufficiently new versions of clang. Unlike toolID, the +// name argument is the program to run. The language argument is the +// type of input file as passed to the GCC driver's -x option. +// +// For these tools we have no -V=full option to dump the build ID, +// but we can run the tool with -v -### to reliably get the compiler proper +// and hash that. That will work in the presence of -toolexec. +// +// In order to get reproducible builds for released compilers, we +// detect a released compiler by the absence of "experimental" in the +// --version output, and in that case we just use the version string. +func (b *Builder) gccgoToolID(name, language string) (string, error) { + key := name + "." + language + b.id.Lock() + id := b.toolIDCache[key] + b.id.Unlock() + + if id != "" { + return id, nil + } + + // Invoke the driver with -### to see the subcommands and the + // version strings. Use -x to set the language. Pretend to + // compile an empty file on standard input. + cmdline := str.StringList(cfg.BuildToolexec, name, "-###", "-x", language, "-c", "-") + cmd := exec.Command(cmdline[0], cmdline[1:]...) + cmd.Env = base.EnvForDir(cmd.Dir, os.Environ()) + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("%s: %v; output: %q", name, err, out) + } + + version := "" + lines := strings.Split(string(out), "\n") + for _, line := range lines { + if fields := strings.Fields(line); len(fields) > 1 && fields[1] == "version" { + version = line + break + } + } + if version == "" { + return "", fmt.Errorf("%s: can not find version number in %q", name, out) + } + + if !strings.Contains(version, "experimental") { + // This is a release. Use this line as the tool ID. + id = version + } else { + // This is a development version. The first line with + // a leading space is the compiler proper. + compiler := "" + for _, line := range lines { + if len(line) > 1 && line[0] == ' ' { + compiler = line + break + } + } + if compiler == "" { + return "", fmt.Errorf("%s: can not find compilation command in %q", name, out) + } + + fields := strings.Fields(compiler) + if len(fields) == 0 { + return "", fmt.Errorf("%s: compilation command confusion %q", name, out) + } + exe := fields[0] + if !strings.ContainsAny(exe, `/\`) { + if lp, err := exec.LookPath(exe); err == nil { + exe = lp + } + } + if _, err := os.Stat(exe); err != nil { + return "", fmt.Errorf("%s: can not find compiler %q: %v; output %q", name, exe, err, out) + } + id = b.fileHash(exe) + } + + b.id.Lock() + b.toolIDCache[name] = id + b.id.Unlock() + + return id, nil +} + +// gccgoBuildIDELFFile creates an assembler file that records the +// action's build ID in an SHF_EXCLUDE section. +func (b *Builder) gccgoBuildIDELFFile(a *Action) (string, error) { + sfile := a.Objdir + "_buildid.s" + + var buf bytes.Buffer + fmt.Fprintf(&buf, "\t"+`.section .go.buildid,"e"`+"\n") + fmt.Fprintf(&buf, "\t.byte ") + for i := 0; i < len(a.buildID); i++ { + if i > 0 { + if i%8 == 0 { + fmt.Fprintf(&buf, "\n\t.byte ") + } else { + fmt.Fprintf(&buf, ",") + } + } + fmt.Fprintf(&buf, "%#02x", a.buildID[i]) + } + fmt.Fprintf(&buf, "\n") + fmt.Fprintf(&buf, "\t"+`.section .note.GNU-stack,"",@progbits`+"\n") + fmt.Fprintf(&buf, "\t"+`.section .note.GNU-split-stack,"",@progbits`+"\n") + + if cfg.BuildN || cfg.BuildX { + for _, line := range bytes.Split(buf.Bytes(), []byte("\n")) { + b.Showcmd("", "echo '%s' >> %s", line, sfile) + } + if cfg.BuildN { + return sfile, nil + } + } + + if err := ioutil.WriteFile(sfile, buf.Bytes(), 0666); err != nil { + return "", err + } + + return sfile, nil +} + // buildID returns the build ID found in the given file. // If no build ID is found, buildID returns the content hash of the file. func (b *Builder) buildID(file string) string { diff --git a/src/cmd/go/internal/work/exec.go b/src/cmd/go/internal/work/exec.go index 5951c83a97..c5f0eb70bf 100644 --- a/src/cmd/go/internal/work/exec.go +++ b/src/cmd/go/internal/work/exec.go @@ -252,6 +252,20 @@ func (b *Builder) buildActionID(a *Action) cache.ActionID { // essentially unfindable. fmt.Fprintf(h, "nocache %d\n", time.Now().UnixNano()) } + + case "gccgo": + id, err := b.gccgoToolID(BuildToolchain.compiler(), "go") + if err != nil { + base.Fatalf("%v", err) + } + fmt.Fprintf(h, "compile %s %q %q\n", id, forcedGccgoflags, p.Internal.Gccgoflags) + fmt.Fprintf(h, "pkgpath %s\n", gccgoPkgpath(p)) + if len(p.SFiles) > 0 { + id, err = b.gccgoToolID(BuildToolchain.compiler(), "assembler-with-cpp") + // Ignore error; different assembler versions + // are unlikely to make any difference anyhow. + fmt.Fprintf(h, "asm %q\n", id) + } } // Input files. @@ -608,6 +622,24 @@ func (b *Builder) build(a *Action) (err error) { objects = append(objects, ofiles...) } + // For gccgo on ELF systems, we write the build ID as an assembler file. + // This lets us set the the SHF_EXCLUDE flag. + // This is read by readGccgoArchive in cmd/internal/buildid/buildid.go. + if a.buildID != "" && cfg.BuildToolchainName == "gccgo" { + switch cfg.Goos { + case "android", "dragonfly", "freebsd", "linux", "netbsd", "openbsd", "solaris": + asmfile, err := b.gccgoBuildIDELFFile(a) + if err != nil { + return err + } + ofiles, err := BuildToolchain.asm(b, a, []string{asmfile}) + if err != nil { + return err + } + objects = append(objects, ofiles...) + } + } + // NOTE(rsc): On Windows, it is critically important that the // gcc-compiled objects (cgoObjects) be listed after the ordinary // objects in the archive. I do not know why this is. @@ -692,12 +724,17 @@ func (b *Builder) vet(a *Action) error { return err } + var env []string + if cfg.BuildToolchainName == "gccgo" { + env = append(env, "GCCGO="+BuildToolchain.compiler()) + } + p := a.Package tool := VetTool if tool == "" { tool = base.Tool("vet") } - return b.run(a, p.Dir, p.ImportPath, nil, cfg.BuildToolexec, tool, VetFlags, a.Objdir+"vet.cfg") + return b.run(a, p.Dir, p.ImportPath, env, cfg.BuildToolexec, tool, VetFlags, a.Objdir+"vet.cfg") } // linkActionID computes the action ID for a link action. @@ -776,6 +813,14 @@ func (b *Builder) printLinkerConfig(h io.Writer, p *load.Package) { // TODO(rsc): Do cgo settings and flags need to be included? // Or external linker settings and flags? + + case "gccgo": + id, err := b.gccgoToolID(BuildToolchain.linker(), "go") + if err != nil { + base.Fatalf("%v", err) + } + fmt.Fprintf(h, "link %s %s\n", id, ldBuildmode) + // TODO(iant): Should probably include cgo flags here. } } diff --git a/src/cmd/go/internal/work/gccgo.go b/src/cmd/go/internal/work/gccgo.go index 37a828f592..b576182b41 100644 --- a/src/cmd/go/internal/work/gccgo.go +++ b/src/cmd/go/internal/work/gccgo.go @@ -154,7 +154,8 @@ func (tools gccgoToolchain) asm(b *Builder, a *Action, sfiles []string) ([]strin p := a.Package var ofiles []string for _, sfile := range sfiles { - ofile := a.Objdir + sfile[:len(sfile)-len(".s")] + ".o" + base := filepath.Base(sfile) + ofile := a.Objdir + base[:len(base)-len(".s")] + ".o" ofiles = append(ofiles, ofile) sfile = mkAbs(p.Dir, sfile) defs := []string{"-D", "GOOS_" + cfg.Goos, "-D", "GOARCH_" + cfg.Goarch} @@ -285,7 +286,7 @@ func (tools gccgoToolchain) link(b *Builder, root *Action, out, importcfg string // doesn't work. if !apackagePathsSeen[a.Package.ImportPath] { apackagePathsSeen[a.Package.ImportPath] = true - target := a.Target + target := a.built if len(a.Package.CgoFiles) > 0 || a.Package.UsesSwig() { target, err = readAndRemoveCgoFlags(target) if err != nil { @@ -353,6 +354,15 @@ func (tools gccgoToolchain) link(b *Builder, root *Action, out, importcfg string ldflags = str.StringList("-Wl,-(", ldflags, "-Wl,-)") + if root.buildID != "" { + // On systems that normally use gold or the GNU linker, + // use the --build-id option to write a GNU build ID note. + switch cfg.Goos { + case "android", "dragonfly", "linux", "netbsd": + ldflags = append(ldflags, fmt.Sprintf("-Wl,--build-id=0x%x", root.buildID)) + } + } + for _, shlib := range shlibs { ldflags = append( ldflags, @@ -392,7 +402,9 @@ func (tools gccgoToolchain) link(b *Builder, root *Action, out, importcfg string } // We are creating an object file, so we don't want a build ID. - ldflags = b.disableBuildID(ldflags) + if root.buildID == "" { + ldflags = b.disableBuildID(ldflags) + } realOut = out out = out + ".o" diff --git a/src/cmd/internal/buildid/buildid.go b/src/cmd/internal/buildid/buildid.go index 1740c88292..fa3d7f37ec 100644 --- a/src/cmd/internal/buildid/buildid.go +++ b/src/cmd/internal/buildid/buildid.go @@ -6,10 +6,12 @@ package buildid import ( "bytes" + "debug/elf" "fmt" "io" "os" "strconv" + "strings" ) var ( @@ -26,8 +28,6 @@ var ( ) // ReadFile reads the build ID from an archive or executable file. -// It only supports archives from the gc toolchain. -// TODO(rsc): Figure out what gccgo and llvm are going to do for archives. func ReadFile(name string) (id string, err error) { f, err := os.Open(name) if err != nil { @@ -59,30 +59,30 @@ func ReadFile(name string) (id string, err error) { return "", err } - bad := func() (string, error) { - return "", &os.PathError{Op: "parse", Path: name, Err: errBuildIDMalformed} + tryGccgo := func() (string, error) { + return readGccgoArchive(name, f) } // Archive header. for i := 0; ; i++ { // returns during i==3 j := bytes.IndexByte(data, '\n') if j < 0 { - return bad() + return tryGccgo() } line := data[:j] data = data[j+1:] switch i { case 0: if !bytes.Equal(line, bangArch) { - return bad() + return tryGccgo() } case 1: if !bytes.HasPrefix(line, pkgdef) { - return bad() + return tryGccgo() } case 2: if !bytes.HasPrefix(line, goobject) { - return bad() + return tryGccgo() } case 3: if !bytes.HasPrefix(line, buildid) { @@ -92,13 +92,71 @@ func ReadFile(name string) (id string, err error) { } id, err := strconv.Unquote(string(line[len(buildid):])) if err != nil { - return bad() + return tryGccgo() } return id, nil } } } +// readGccgoArchive tries to parse the archive as a standard Unix +// archive file, and fetch the build ID from the _buildid.o entry. +// The _buildid.o entry is written by (*Builder).gccgoBuildIDELFFile +// in cmd/go/internal/work/exec.go. +func readGccgoArchive(name string, f *os.File) (string, error) { + bad := func() (string, error) { + return "", &os.PathError{Op: "parse", Path: name, Err: errBuildIDMalformed} + } + + off := int64(8) + for { + if _, err := f.Seek(off, io.SeekStart); err != nil { + return "", err + } + + // TODO(iant): Make a debug/ar package, and use it + // here and in cmd/link. + var hdr [60]byte + if _, err := io.ReadFull(f, hdr[:]); err != nil { + if err == io.EOF { + // No more entries, no build ID. + return "", nil + } + return "", err + } + off += 60 + + sizeStr := strings.TrimSpace(string(hdr[48:58])) + size, err := strconv.ParseInt(sizeStr, 0, 64) + if err != nil { + return bad() + } + + name := strings.TrimSpace(string(hdr[:16])) + if name == "_buildid.o/" { + sr := io.NewSectionReader(f, off, size) + e, err := elf.NewFile(sr) + if err != nil { + return bad() + } + s := e.Section(".go.buildid") + if s == nil { + return bad() + } + data, err := s.Data() + if err != nil { + return bad() + } + return string(data), nil + } + + off += size + if off&1 != 0 { + off++ + } + } +} + var ( goBuildPrefix = []byte("\xff Go build ID: \"") goBuildEnd = []byte("\"\n \xff") diff --git a/src/cmd/internal/buildid/note.go b/src/cmd/internal/buildid/note.go index 5156cbd88c..f0439fb0bf 100644 --- a/src/cmd/internal/buildid/note.go +++ b/src/cmd/internal/buildid/note.go @@ -69,6 +69,7 @@ func ReadELFNote(filename, name string, typ int32) ([]byte, error) { } var elfGoNote = []byte("Go\x00\x00") +var elfGNUNote = []byte("GNU\x00") // The Go build ID is stored in a note described by an ELF PT_NOTE prog // header. The caller has already opened filename, to get f, and read @@ -90,11 +91,13 @@ func readELF(name string, f *os.File, data []byte) (buildid string, err error) { } const elfGoBuildIDTag = 4 + const gnuBuildIDTag = 3 ef, err := elf.NewFile(bytes.NewReader(data)) if err != nil { return "", &os.PathError{Path: name, Op: "parse", Err: err} } + var gnu string for _, p := range ef.Progs { if p.Type != elf.PT_NOTE || p.Filesz < 16 { continue @@ -123,26 +126,42 @@ func readELF(name string, f *os.File, data []byte) (buildid string, err error) { } filesz := p.Filesz + off := p.Off for filesz >= 16 { nameSize := ef.ByteOrder.Uint32(note) valSize := ef.ByteOrder.Uint32(note[4:]) tag := ef.ByteOrder.Uint32(note[8:]) - name := note[12:16] - if nameSize == 4 && 16+valSize <= uint32(len(note)) && tag == elfGoBuildIDTag && bytes.Equal(name, elfGoNote) { + nname := note[12:16] + if nameSize == 4 && 16+valSize <= uint32(len(note)) && tag == elfGoBuildIDTag && bytes.Equal(nname, elfGoNote) { return string(note[16 : 16+valSize]), nil } + if nameSize == 4 && 16+valSize <= uint32(len(note)) && tag == gnuBuildIDTag && bytes.Equal(nname, elfGNUNote) { + gnu = string(note[16 : 16+valSize]) + } + nameSize = (nameSize + 3) &^ 3 valSize = (valSize + 3) &^ 3 notesz := uint64(12 + nameSize + valSize) if filesz <= notesz { break } + off += notesz + align := uint64(p.Align) + alignedOff := (off + align - 1) &^ (align - 1) + notesz += alignedOff - off + off = alignedOff filesz -= notesz note = note[notesz:] } } + // If we didn't find a Go note, use a GNU note if available. + // This is what gccgo uses. + if gnu != "" { + return gnu, nil + } + // No note. Treat as successful but build ID empty. return "", nil }