cmd/compile: add coverage fixup mode

Adds a -coveragecfg=<configfile> command line option to the compiler
to help support a cooperative "tool and compiler" mode for coverage
instrumentation. In this mode the cmd/cover tool generates most of the
counter instrumentation via source-to-source rewriting, but the
compiler fixes up the result if passed the "-coveragecfg" option. The
fixups include:

  - reclassifying counter variables (special storage class)
  - marking meta-data variables are read-only
  - adding in an init call to do registation

Updates #51430.

Change-Id: Iead72b85209725ee044542374465f118a3ee72e0
Reviewed-on: https://go-review.googlesource.com/c/go/+/395895
Reviewed-by: David Chase <drchase@google.com>
Run-TryBot: Than McIntosh <thanm@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
Than McIntosh 2022-03-07 10:32:51 -05:00
parent 072c7d4969
commit e6d9057e2f
12 changed files with 411 additions and 43 deletions

View file

@ -9,6 +9,8 @@ import (
"flag"
"fmt"
"internal/buildcfg"
"internal/coverage"
"io/ioutil"
"log"
"os"
"reflect"
@ -110,6 +112,7 @@ type CmdFlags struct {
MemProfileRate int "help:\"set runtime.MemProfileRate to `rate`\""
MutexProfile string "help:\"write mutex profile to `file`\""
NoLocalImports bool "help:\"reject local (relative) imports\""
CoverageCfg func(string) "help:\"read coverage configuration from `file`\""
Pack bool "help:\"write to file.a instead of file.o\""
Race bool "help:\"enable race detector\""
Shared *bool "help:\"generate code that can be linked into a shared library\"" // &Ctxt.Flag_shared, set below
@ -127,10 +130,11 @@ type CmdFlags struct {
Patterns map[string][]string
Files map[string]string
}
ImportDirs []string // appended to by -I
ImportMap map[string]string // set by -importcfg
PackageFile map[string]string // set by -importcfg; nil means not in use
SpectreIndex bool // set by -spectre=index or -spectre=all
ImportDirs []string // appended to by -I
ImportMap map[string]string // set by -importcfg
PackageFile map[string]string // set by -importcfg; nil means not in use
CoverageInfo *coverage.CoverFixupConfig // set by -coveragecfg
SpectreIndex bool // set by -spectre=index or -spectre=all
// Whether we are adding any sort of code instrumentation, such as
// when the race detector is enabled.
Instrumenting bool
@ -154,6 +158,7 @@ func ParseFlags() {
Flag.EmbedCfg = readEmbedCfg
Flag.GenDwarfInl = 2
Flag.ImportCfg = readImportCfg
Flag.CoverageCfg = readCoverageCfg
Flag.LinkShared = &Ctxt.Flag_linkshared
Flag.Shared = &Ctxt.Flag_shared
Flag.WB = true
@ -430,6 +435,18 @@ func readImportCfg(file string) {
}
}
func readCoverageCfg(file string) {
var cfg coverage.CoverFixupConfig
data, err := ioutil.ReadFile(file)
if err != nil {
log.Fatalf("-coveragecfg: %v", err)
}
if err := json.Unmarshal(data, &cfg); err != nil {
log.Fatalf("error reading -coveragecfg file %q: %v", file, err)
}
Flag.Cfg.CoverageInfo = &cfg
}
func readEmbedCfg(file string) {
data, err := os.ReadFile(file)
if err != nil {

View file

@ -0,0 +1,189 @@
// Copyright 2022 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.
package coverage
import (
"cmd/compile/internal/base"
"cmd/compile/internal/ir"
"cmd/compile/internal/typecheck"
"cmd/compile/internal/types"
"cmd/internal/objabi"
"internal/coverage"
"strconv"
"strings"
)
// Fixup is the main entry point for coverage compiler fixup. It
// collects and reclassifies the variables mentioned in the
// -coveragecfg file, then adds calls to the pkg init function as
// appropriate to register the proper variables with the runtime.
func Fixup() {
metavar, pkgIdVar, initfn, covermode, covergran :=
fixupMetaAndCounterVariables()
hashv, len := metaHashAndLen()
if covermode != coverage.CtrModeTestMain {
registerMeta(metavar, initfn, hashv, len,
pkgIdVar, covermode, covergran)
}
if base.Ctxt.Pkgpath == "main" {
addInitHookCall(initfn, covermode)
}
}
// fixupMetaAndCounterVariables collects and returns the package ID
// and meta-data variables being used for this "-cover" build, along
// with the init function for the package and the coverage mode. It
// also reclassifies certain variables (for example, tagging coverage
// counter variables with flags so that they can be handled properly
// downstream).
func fixupMetaAndCounterVariables() (*ir.Name, *ir.Name, *ir.Func, coverage.CounterMode, coverage.CounterGranularity) {
metaVarName := base.Flag.Cfg.CoverageInfo.MetaVar
pkgIdVarName := base.Flag.Cfg.CoverageInfo.PkgIdVar
counterMode := base.Flag.Cfg.CoverageInfo.CounterMode
counterGran := base.Flag.Cfg.CoverageInfo.CounterGranularity
counterPrefix := base.Flag.Cfg.CoverageInfo.CounterPrefix
var metavar *ir.Name
var pkgidvar *ir.Name
var initfn *ir.Func
ckTypSanity := func(nm *ir.Name, tag string) {
if nm.Type() == nil || nm.Type().HasPointers() {
base.Fatalf("unsuitable %s %q mentioned in coveragecfg, improper type '%v'", tag, nm.Sym().Name, nm.Type())
}
}
for _, n := range typecheck.Target.Decls {
if fn, ok := n.(*ir.Func); ok && ir.FuncName(fn) == "init" {
if initfn != nil {
panic("unexpected")
}
initfn = fn
continue
}
as, ok := n.(*ir.AssignStmt)
if !ok {
continue
}
nm, ok := as.X.(*ir.Name)
if !ok {
continue
}
s := nm.Sym()
switch s.Name {
case metaVarName:
metavar = nm
ckTypSanity(nm, "metavar")
nm.MarkReadonly()
continue
case pkgIdVarName:
pkgidvar = nm
ckTypSanity(nm, "pkgidvar")
nm.SetCoverageAuxVar(true)
s := nm.Linksym()
s.Type = objabi.SCOVERAGE_AUXVAR
continue
}
if strings.HasPrefix(s.Name, counterPrefix) {
ckTypSanity(nm, "countervar")
nm.SetCoverageCounter(true)
s := nm.Linksym()
s.Type = objabi.SCOVERAGE_COUNTER
}
}
cm := coverage.ParseCounterMode(counterMode)
if cm == coverage.CtrModeInvalid {
base.Fatalf("bad setting %q for covermode in coveragecfg:",
counterMode)
}
var cg coverage.CounterGranularity
switch counterGran {
case "perblock":
cg = coverage.CtrGranularityPerBlock
case "perfunc":
cg = coverage.CtrGranularityPerFunc
default:
base.Fatalf("bad setting %q for covergranularity in coveragecfg:",
counterGran)
}
return metavar, pkgidvar, initfn, cm, cg
}
func metaHashAndLen() ([16]byte, int) {
// Read meta-data hash from config entry.
mhash := base.Flag.Cfg.CoverageInfo.MetaHash
if len(mhash) != 32 {
base.Fatalf("unexpected: got metahash length %d want 32", len(mhash))
}
var hv [16]byte
for i := 0; i < 16; i++ {
nib := string(mhash[i*2 : i*2+2])
x, err := strconv.ParseInt(nib, 16, 32)
if err != nil {
base.Fatalf("metahash bad byte %q", nib)
}
hv[i] = byte(x)
}
// Return hash and meta-data len
return hv, base.Flag.Cfg.CoverageInfo.MetaLen
}
func registerMeta(mdname *ir.Name, initfn *ir.Func, hash [16]byte, mdlen int, pkgIdVar *ir.Name, cmode coverage.CounterMode, cgran coverage.CounterGranularity) {
// Materialize expression for hash (an array literal)
pos := initfn.Pos()
elist := make([]ir.Node, 0, 16)
for i := 0; i < 16; i++ {
elem := ir.NewInt(int64(hash[i]))
elist = append(elist, elem)
}
ht := types.NewArray(types.Types[types.TUINT8], 16)
hashx := ir.NewCompLitExpr(pos, ir.OCOMPLIT, ht, elist)
// Materalize expression corresponding to address of the meta-data symbol.
mdax := typecheck.NodAddr(mdname)
mdauspx := typecheck.ConvNop(mdax, types.Types[types.TUNSAFEPTR])
// Materialize expression for length.
lenx := ir.NewInt(int64(mdlen)) // untyped
// Generate a call to runtime.addCovMeta, e.g.
//
// pkgIdVar = runtime.addCovMeta(&sym, len, hash, pkgpath, pkid, cmode, cgran)
//
fn := typecheck.LookupRuntime("addCovMeta")
pkid := coverage.HardCodedPkgID(base.Ctxt.Pkgpath)
pkIdNode := ir.NewInt(int64(pkid))
cmodeNode := ir.NewInt(int64(cmode))
cgranNode := ir.NewInt(int64(cgran))
pkPathNode := ir.NewString(base.Ctxt.Pkgpath)
callx := typecheck.Call(pos, fn, []ir.Node{mdauspx, lenx, hashx,
pkPathNode, pkIdNode, cmodeNode, cgranNode}, false)
assign := callx
if pkid == coverage.NotHardCoded {
assign = typecheck.Stmt(ir.NewAssignStmt(pos, pkgIdVar, callx))
}
// Tack the call onto the start of our init function. We do this
// early in the init since it's possible that instrumented function
// bodies (with counter updates) might be inlined into init.
initfn.Body.Prepend(assign)
}
// addInitHookCall generates a call to runtime/coverage.initHook() and
// inserts it into the package main init function, which will kick off
// the process for coverage data writing (emit meta data, and register
// an exit hook to emit counter data).
func addInitHookCall(initfn *ir.Func, cmode coverage.CounterMode) {
typecheck.InitCoverage()
pos := initfn.Pos()
istest := cmode == coverage.CtrModeTestMain
initf := typecheck.LookupCoverage("initHook")
istestNode := ir.NewBool(istest)
args := []ir.Node{istestNode}
callx := typecheck.Call(pos, initf, args, false)
initfn.Body.Append(callx)
}

View file

@ -8,6 +8,7 @@ import (
"bufio"
"bytes"
"cmd/compile/internal/base"
"cmd/compile/internal/coverage"
"cmd/compile/internal/deadcode"
"cmd/compile/internal/devirtualize"
"cmd/compile/internal/dwarfgen"
@ -97,6 +98,10 @@ func Main(archInit func(*ssagen.ArchInfo)) {
// pseudo-package used for methods with anonymous receivers
ir.Pkgs.Go = types.NewPkg("go", "")
// pseudo-package for use with code coverage instrumentation.
ir.Pkgs.Coverage = types.NewPkg("go.coverage", "runtime/coverage")
ir.Pkgs.Coverage.Prefix = "runtime/coverage"
// Record flags that affect the build result. (And don't
// record flags that don't, since that would cause spurious
// changes in the binary.)
@ -207,6 +212,11 @@ func Main(archInit func(*ssagen.ArchInfo)) {
// removal can skew the results (e.g., #43444).
pkginit.MakeInit()
// Fix up init routines if building for code coverage.
if base.Flag.Cfg.CoverageInfo != nil {
coverage.Fixup()
}
// Eliminate some obviously dead code.
// Must happen after typechecking.
for _, n := range typecheck.Target.Decls {

View file

@ -69,7 +69,8 @@ var Syms struct {
// Pkgs holds known packages.
var Pkgs struct {
Go *types.Pkg
Itab *types.Pkg
Runtime *types.Pkg
Go *types.Pkg
Itab *types.Pkg
Runtime *types.Pkg
Coverage *types.Pkg
}

View file

@ -195,3 +195,18 @@ func Task() *ir.Name {
objw.Global(lsym, int32(ot), obj.NOPTR)
return task
}
// initRequiredForCoverage returns TRUE if we need to force creation
// of an init function for the package so as to insert a coverage
// runtime registration call.
func initRequiredForCoverage(l []ir.Node) bool {
if base.Flag.Cfg.CoverageInfo == nil {
return false
}
for _, n := range l {
if n.Op() == ir.ODCLFUNC {
return true
}
}
return false
}

View file

@ -7,6 +7,21 @@ import (
"cmd/internal/src"
)
// Not inlining this function removes a significant chunk of init code.
//
//go:noinline
func newSig(params, results []*types.Field) *types.Type {
return types.NewSignature(types.NoPkg, nil, nil, params, results)
}
func params(tlist ...*types.Type) []*types.Field {
flist := make([]*types.Field, len(tlist))
for i, typ := range tlist {
flist[i] = types.NewField(src.NoXPos, nil, typ)
}
return flist
}
var runtimeDecls = [...]struct {
name string
tag int
@ -210,6 +225,7 @@ var runtimeDecls = [...]struct {
{"libfuzzerTraceConstCmp8", funcTag, 149},
{"libfuzzerHookStrCmp", funcTag, 150},
{"libfuzzerHookEqualFold", funcTag, 150},
{"addCovMeta", funcTag, 152},
{"x86HasPOPCNT", varTag, 6},
{"x86HasSSE41", varTag, 6},
{"x86HasFMA", varTag, 6},
@ -217,23 +233,8 @@ var runtimeDecls = [...]struct {
{"arm64HasATOMICS", varTag, 6},
}
// Not inlining this function removes a significant chunk of init code.
//
//go:noinline
func newSig(params, results []*types.Field) *types.Type {
return types.NewSignature(types.NoPkg, nil, nil, params, results)
}
func params(tlist ...*types.Type) []*types.Field {
flist := make([]*types.Field, len(tlist))
for i, typ := range tlist {
flist[i] = types.NewField(src.NoXPos, nil, typ)
}
return flist
}
func runtimeTypes() []*types.Type {
var typs [151]*types.Type
var typs [153]*types.Type
typs[0] = types.ByteType
typs[1] = types.NewPtr(typs[0])
typs[2] = types.Types[types.TANY]
@ -385,5 +386,22 @@ func runtimeTypes() []*types.Type {
typs[148] = newSig(params(typs[62], typs[62], typs[15]), nil)
typs[149] = newSig(params(typs[24], typs[24], typs[15]), nil)
typs[150] = newSig(params(typs[28], typs[28], typs[15]), nil)
typs[151] = types.NewArray(typs[0], 16)
typs[152] = newSig(params(typs[7], typs[62], typs[151], typs[28], typs[15], typs[66], typs[66]), params(typs[62]))
return typs[:]
}
var coverageDecls = [...]struct {
name string
tag int
typ int
}{
{"initHook", funcTag, 1},
}
func coverageTypes() []*types.Type {
var typs [2]*types.Type
typs[0] = types.Types[types.TBOOL]
typs[1] = newSig(params(typs[0]), nil)
return typs[:]
}

View file

@ -0,0 +1,14 @@
// Copyright 2022 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.
// NOTE: If you change this file you must run "go generate"
// to update builtin.go. This is not done automatically
// to avoid depending on having a working compiler binary.
//go:build ignore
// +build ignore
package coverage
func initHook(istest bool)

View file

@ -273,6 +273,8 @@ func libfuzzerTraceConstCmp8(uint64, uint64, int)
func libfuzzerHookStrCmp(string, string, int)
func libfuzzerHookEqualFold(string, string, int)
func addCovMeta(p unsafe.Pointer, len uint32, hash [16]byte, pkpath string, pkgId int, cmode uint8, cgran uint8) uint32
// architecture variants
var x86HasPOPCNT bool
var x86HasSSE41 bool

View file

@ -26,6 +26,7 @@ import (
)
var stdout = flag.Bool("stdout", false, "write to stdout instead of builtin.go")
var nofmt = flag.Bool("nofmt", false, "skip formatting builtin.go")
func main() {
flag.Parse()
@ -40,11 +41,32 @@ func main() {
fmt.Fprintln(&b, ` "cmd/internal/src"`)
fmt.Fprintln(&b, `)`)
mkbuiltin(&b, "runtime")
fmt.Fprintln(&b, `
// Not inlining this function removes a significant chunk of init code.
//go:noinline
func newSig(params, results []*types.Field) *types.Type {
return types.NewSignature(types.NoPkg, nil, nil, params, results)
}
out, err := format.Source(b.Bytes())
if err != nil {
log.Fatal(err)
func params(tlist ...*types.Type) []*types.Field {
flist := make([]*types.Field, len(tlist))
for i, typ := range tlist {
flist[i] = types.NewField(src.NoXPos, nil, typ)
}
return flist
}
`)
mkbuiltin(&b, "runtime")
mkbuiltin(&b, "coverage")
var err error
out := b.Bytes()
if !*nofmt {
out, err = format.Source(out)
if err != nil {
log.Fatal(err)
}
}
if *stdout {
_, err = os.Stdout.Write(out)
@ -102,22 +124,6 @@ func mkbuiltin(w io.Writer, name string) {
}
fmt.Fprintln(w, "}")
fmt.Fprintln(w, `
// Not inlining this function removes a significant chunk of init code.
//
//go:noinline
func newSig(params, results []*types.Field) *types.Type {
return types.NewSignature(types.NoPkg, nil, nil, params, results)
}
func params(tlist ...*types.Type) []*types.Field {
flist := make([]*types.Field, len(tlist))
for i, typ := range tlist {
flist[i] = types.NewField(src.NoXPos, nil, typ)
}
return flist
}`)
fmt.Fprintln(w)
fmt.Fprintf(w, "func %sTypes() []*types.Type {\n", name)
fmt.Fprintf(w, "var typs [%d]*types.Type\n", len(interner.typs))

View file

@ -101,3 +101,32 @@ func LookupRuntimeVar(name string) *obj.LSym {
func LookupRuntimeABI(name string, abi obj.ABI) *obj.LSym {
return base.PkgLinksym("runtime", name, abi)
}
// InitCoverage loads the definitions for routines called
// by code coverage instrumentation (similar to InitRuntime above).
func InitCoverage() {
typs := coverageTypes()
for _, d := range &coverageDecls {
sym := ir.Pkgs.Coverage.Lookup(d.name)
typ := typs[d.typ]
switch d.tag {
case funcTag:
importfunc(src.NoXPos, sym, typ)
case varTag:
importvar(src.NoXPos, sym, typ)
default:
base.Fatalf("unhandled declaration tag %v", d.tag)
}
}
}
// LookupCoverage looks up the Go function 'name' in package
// runtime/coverage. This function must follow the internal calling
// convention.
func LookupCoverage(name string) *ir.Name {
sym := ir.Pkgs.Coverage.Lookup(name)
if sym == nil {
base.Fatalf("LookupCoverage: can't find runtime/coverage.%s", name)
}
return ir.AsNode(sym.Def).(*ir.Name)
}

View file

@ -60,6 +60,7 @@ var bootstrapDirs = []string{
"debug/macho",
"debug/pe",
"go/constant",
"internal/coverage",
"internal/buildcfg",
"internal/goexperiment",
"internal/goversion",

View file

@ -0,0 +1,66 @@
// Copyright 2022 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.
package coverage
// CoverPkgConfig is a bundle of information passed from the Go
// command to the cover command during "go build -cover" runs. The
// Go command creates and fills in a struct as below, then passes
// file containing the encoded JSON for the struct to the "cover"
// tool when instrumenting the source files in a Go package.
type CoverPkgConfig struct {
// File into which cmd/cover should emit summary info
// when instrumentation is complete.
OutConfig string
// Import path for the package being instrumented.
PkgPath string
// Package name.
PkgName string
// Instrumentation granularity: one of "perfunc" or "perblock" (default)
Granularity string
// Module path for this package (empty if no go.mod in use)
ModulePath string
}
// CoverFixupConfig contains annotations/notes generated by the
// cmd/cover tool (during instrumentation) to be passed on to the
// compiler when the instrumented code is compiled. The cmd/cover tool
// creates a struct of this type, JSON-encodes it, and emits the
// result to a file, which the Go command then passes to the compiler
// when the instrumented package is built.
type CoverFixupConfig struct {
// Name of the variable (created by cmd/cover) containing the
// encoded meta-data for the package.
MetaVar string
// Length of the meta-data.
MetaLen int
// Hash computed by cmd/cover of the meta-data.
MetaHash string
// Instrumentation strategy. For now this is always set to
// "normal", but in the future we may add new values (for example,
// if panic paths are instrumented, or if the instrumenter
// eliminates redundant counters).
Strategy string
// Prefix assigned to the names of counter variables generated
// during instrumentation by cmd/cover.
CounterPrefix string
// Name chosen for the package ID variable generated during
// instrumentation.
PkgIdVar string
// Counter mode (e.g. set/count/atomic)
CounterMode string
// Counter granularity (perblock or perfunc).
CounterGranularity string
}