Skip to content

Commit 16ebe9f

Browse files
committed
cmd/go: use ELF note instead of binary stamp on ELF systems
Other binary formats to follow. For golang#11048. Change-Id: Ia2d8b47c99c99d171c014b7cfd23c1c7ada5231c Reviewed-on: https://go-review.googlesource.com/10707 Reviewed-by: Ian Lance Taylor <[email protected]>
1 parent ac1f48e commit 16ebe9f

4 files changed

Lines changed: 124 additions & 7 deletions

File tree

src/cmd/go/build.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2372,6 +2372,9 @@ func (gcToolchain) ld(b *builder, p *Package, out string, allactions []*action,
23722372
}
23732373
ldflags = setextld(ldflags, compiler)
23742374
ldflags = append(ldflags, "-buildmode="+ldBuildmode)
2375+
if p.buildID != "" {
2376+
ldflags = append(ldflags, "-buildid="+p.buildID)
2377+
}
23752378
ldflags = append(ldflags, buildLdflags...)
23762379
return b.run(".", p.ImportPath, nil, buildToolExec, tool("link"), "-o", out, importArgs, ldflags, mainpkg)
23772380
}

src/cmd/go/note.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
package main
66

77
import (
8+
"bytes"
89
"debug/elf"
910
"encoding/binary"
1011
"fmt"
1112
"io"
13+
"os"
1214
)
1315

1416
func readAligned4(r io.Reader, sz int32) ([]byte, error) {
@@ -64,3 +66,51 @@ func readELFNote(filename, name string, typ int32) ([]byte, error) {
6466
}
6567
return nil, nil
6668
}
69+
70+
var elfGoNote = []byte("Go\x00\x00")
71+
72+
// readELFGoBuildID the Go build ID string from an ELF binary.
73+
// The Go build ID is stored in a note described by an ELF PT_NOTE prog header.
74+
// The caller has already opened filename, to get f, and read the first 4 kB out, in data.
75+
func readELFGoBuildID(filename string, f *os.File, data []byte) (buildid string, err error) {
76+
// Assume the note content is in the first 4 kB, already read.
77+
// Rewrite the ELF header to set shnum to 0, so that we can pass
78+
// the data to elf.NewFile and it will decode the Prog list but not
79+
// try to read the section headers and the string table from disk.
80+
// That's a waste of I/O when all we care about is the Prog list
81+
// and the one ELF note.
82+
switch elf.Class(data[elf.EI_CLASS]) {
83+
case elf.ELFCLASS32:
84+
data[48] = 0
85+
data[49] = 0
86+
case elf.ELFCLASS64:
87+
data[60] = 0
88+
data[61] = 0
89+
}
90+
91+
const elfGoBuildIDTag = 4
92+
93+
ef, err := elf.NewFile(bytes.NewReader(data))
94+
if err != nil {
95+
return "", &os.PathError{Path: filename, Op: "parse", Err: err}
96+
}
97+
for _, p := range ef.Progs {
98+
if p.Type != elf.PT_NOTE || p.Off >= uint64(len(data)) || p.Off+p.Filesz >= uint64(len(data)) || p.Filesz < 16 {
99+
continue
100+
}
101+
102+
note := data[p.Off : p.Off+p.Filesz]
103+
nameSize := ef.ByteOrder.Uint32(note)
104+
valSize := ef.ByteOrder.Uint32(note[4:])
105+
tag := ef.ByteOrder.Uint32(note[8:])
106+
name := note[12:16]
107+
if nameSize != 4 || 16+valSize > uint32(len(note)) || tag != elfGoBuildIDTag || !bytes.Equal(name, elfGoNote) {
108+
continue
109+
}
110+
111+
return string(note[16 : 16+valSize]), nil
112+
}
113+
114+
// No note. Treat as successful but build ID empty.
115+
return "", nil
116+
}

src/cmd/go/note_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2015 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+
"io/ioutil"
9+
"os/exec"
10+
"runtime"
11+
"testing"
12+
)
13+
14+
func TestNoteReading(t *testing.T) {
15+
// TODO: Enable on non-ELF systems.
16+
switch runtime.GOOS {
17+
case "darwin", "windows", "plan9", "nacl":
18+
t.Skipf("skipping on %q", runtime.GOOS)
19+
}
20+
21+
// TODO: Replace with new test scaffolding by iant.
22+
d, err := ioutil.TempDir("", "go-test-")
23+
if err != nil {
24+
t.Fatal(err)
25+
}
26+
out, err := exec.Command("go", "build", "-o", d+"/go.exe", "cmd/go").CombinedOutput()
27+
if err != nil {
28+
t.Fatalf("go build cmd/go: %v\n%s", err, out)
29+
}
30+
31+
const buildID = "TestNoteReading-Build-ID"
32+
out, err = exec.Command(d+"/go.exe", "build", "-ldflags", "-buildid="+buildID, "-o", d+"/hello.exe", "../../../test/helloworld.go").CombinedOutput()
33+
if err != nil {
34+
t.Fatalf("go build hello: %v\n%s", err, out)
35+
}
36+
37+
id, err := readBuildIDFromBinary(d + "/hello.exe")
38+
if err != nil {
39+
t.Fatalf("reading build ID from hello binary: %v", err)
40+
}
41+
42+
if id != buildID {
43+
t.Fatalf("buildID in hello binary = %q, want %q", id, buildID)
44+
}
45+
}

src/cmd/go/pkg.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,7 @@ func readBuildID(p *Package) (id string, err error) {
10911091

10921092
// For commands, read build ID directly from binary.
10931093
if p.Name == "main" {
1094-
return readBuildIDFromBinary(p)
1094+
return readBuildIDFromBinary(p.Target)
10951095
}
10961096

10971097
// Otherwise, we expect to have an archive (.a) file,
@@ -1166,9 +1166,15 @@ var (
11661166
goBinary = []byte("\x00\n\ngo binary\n")
11671167
endGoBinary = []byte("\nend go binary\n")
11681168
newlineAndBuildid = []byte("\nbuild id ")
1169+
1170+
elfPrefix = []byte("ELF\x7F")
11691171
)
11701172

11711173
// readBuildIDFromBinary reads the build ID from a binary.
1174+
//
1175+
// The location of the build ID differs by object file type.
1176+
// ELF uses a proper PT_NOTE section.
1177+
//
11721178
// Instead of trying to be good citizens and store the build ID in a
11731179
// custom section of the binary, which would be different for each
11741180
// of the four binary types we support (ELF, Mach-O, Plan 9, PE),
@@ -1182,17 +1188,30 @@ var (
11821188
// build id "XXX"
11831189
// end go binary
11841190
//
1185-
func readBuildIDFromBinary(p *Package) (id string, err error) {
1186-
if p.Target == "" {
1187-
return "", &os.PathError{Op: "parse", Path: p.Target, Err: errBuildIDUnknown}
1191+
func readBuildIDFromBinary(filename string) (id string, err error) {
1192+
if filename == "" {
1193+
return "", &os.PathError{Op: "parse", Path: filename, Err: errBuildIDUnknown}
11881194
}
11891195

1190-
f, err := os.Open(p.Target)
1196+
f, err := os.Open(filename)
11911197
if err != nil {
11921198
return "", err
11931199
}
11941200
defer f.Close()
11951201

1202+
data := make([]byte, 4096)
1203+
_, err = io.ReadFull(f, data)
1204+
if err == io.ErrUnexpectedEOF {
1205+
err = nil
1206+
}
1207+
if err != nil {
1208+
return "", err
1209+
}
1210+
1211+
if bytes.HasPrefix(data, elfPrefix) {
1212+
return readELFGoBuildID(filename, f, data)
1213+
}
1214+
11961215
off, err := f.Seek(0, 2)
11971216
if err != nil {
11981217
return "", err
@@ -1204,7 +1223,7 @@ func readBuildIDFromBinary(p *Package) (id string, err error) {
12041223
if _, err := f.Seek(off-int64(n), 0); err != nil {
12051224
return "", err
12061225
}
1207-
data := make([]byte, n)
1226+
data = make([]byte, n)
12081227
if _, err := io.ReadFull(f, data); err != nil {
12091228
return "", err
12101229
}
@@ -1229,7 +1248,7 @@ func readBuildIDFromBinary(p *Package) (id string, err error) {
12291248
j := bytes.IndexByte(line, '\n') // must succeed - endGoBinary is at end and has newlines
12301249
id, err = strconv.Unquote(string(line[:j]))
12311250
if err != nil {
1232-
return "", &os.PathError{Op: "parse", Path: p.Target, Err: errBuildIDMalformed}
1251+
return "", &os.PathError{Op: "parse", Path: filename, Err: errBuildIDMalformed}
12331252
}
12341253
return id, nil
12351254
}

0 commit comments

Comments
 (0)