From 3e671cc5ce6612d8a67495a107df5ff8091113ea Mon Sep 17 00:00:00 2001 From: Dmitry Vyukov Date: Sun, 6 Dec 2020 10:08:26 +0100 Subject: pkg/cover: split into ELF-dependent/independent parts --- pkg/cover/backend/backend.go | 35 ++ pkg/cover/backend/elf.go | 434 ++++++++++++++++ pkg/cover/html.go | 448 +++++++++++++++++ pkg/cover/pc.go | 55 ++ pkg/cover/report.go | 1128 ++++-------------------------------------- 5 files changed, 1074 insertions(+), 1026 deletions(-) create mode 100644 pkg/cover/backend/backend.go create mode 100644 pkg/cover/backend/elf.go create mode 100644 pkg/cover/html.go create mode 100644 pkg/cover/pc.go (limited to 'pkg') diff --git a/pkg/cover/backend/backend.go b/pkg/cover/backend/backend.go new file mode 100644 index 000000000..c99e40893 --- /dev/null +++ b/pkg/cover/backend/backend.go @@ -0,0 +1,35 @@ +// Copyright 2020 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package backend + +import ( + "github.com/google/syzkaller/pkg/symbolizer" + "github.com/google/syzkaller/sys/targets" +) + +type Impl struct { + Units []*CompileUnit + Symbols []*Symbol + Frames []symbolizer.Frame + Symbolize func(pcs []uint64) ([]symbolizer.Frame, error) +} + +type CompileUnit struct { + Name string + Path string + PCs []uint64 +} + +type Symbol struct { + Unit *CompileUnit + Name string + Start uint64 + End uint64 + PCs []uint64 + Symbolized bool +} + +func Make(target *targets.Target, kernelObject, srcDir, buildDir string) (*Impl, error) { + return makeELF(target, kernelObject, srcDir, buildDir) +} diff --git a/pkg/cover/backend/elf.go b/pkg/cover/backend/elf.go new file mode 100644 index 000000000..1e46578ed --- /dev/null +++ b/pkg/cover/backend/elf.go @@ -0,0 +1,434 @@ +// Copyright 2020 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package backend + +import ( + "bufio" + "bytes" + "debug/dwarf" + "debug/elf" + "encoding/binary" + "fmt" + "io/ioutil" + "runtime" + "sort" + "strconv" + + "github.com/google/syzkaller/pkg/osutil" + "github.com/google/syzkaller/pkg/symbolizer" + "github.com/google/syzkaller/sys/targets" +) + +func makeELF(target *targets.Target, kernelObject, srcDir, buildDir string) (*Impl, error) { + file, err := elf.Open(kernelObject) + if err != nil { + return nil, err + } + var coverPoints []uint64 + var symbols []*Symbol + errc := make(chan error, 1) + go func() { + var err error + var tracePC uint64 + symbols, tracePC, err = readSymbols(file) + if err != nil { + errc <- err + return + } + if target.Arch == targets.AMD64 { + coverPoints, err = readCoverPoints(file, tracePC) + } else { + coverPoints, err = objdump(target, kernelObject) + } + errc <- err + }() + ranges, units, err := readTextRanges(file) + if err != nil { + return nil, err + } + if err := <-errc; err != nil { + return nil, err + } + if len(coverPoints) == 0 { + return nil, fmt.Errorf("%v doesn't contain coverage callbacks (set CONFIG_KCOV=y)", kernelObject) + } + symbols = buildSymbols(symbols, ranges, coverPoints) + nunit := 0 + for _, unit := range units { + if len(unit.PCs) == 0 { + continue // drop the unit + } + units[nunit] = unit + nunit++ + } + units = units[:nunit] + if len(symbols) == 0 || len(units) == 0 { + return nil, fmt.Errorf("failed to parse DWARF (set CONFIG_DEBUG_INFO=y?)") + } + impl := &Impl{ + Units: units, + Symbols: symbols, + Symbolize: func(pcs []uint64) ([]symbolizer.Frame, error) { + return symbolize(target, kernelObject, pcs) + }, + } + return impl, nil +} + +type pcRange struct { + start uint64 + end uint64 + unit *CompileUnit +} + +func buildSymbols(symbols []*Symbol, ranges []pcRange, coverPoints []uint64) []*Symbol { + // Assign coverage point PCs to symbols. + // Both symbols and coverage points are sorted, so we do it one pass over both. + var curSymbol *Symbol + firstSymbolPC, symbolIdx := -1, 0 + for i := 0; i < len(coverPoints); i++ { + pc := coverPoints[i] + for ; symbolIdx < len(symbols) && pc >= symbols[symbolIdx].End; symbolIdx++ { + } + var symb *Symbol + if symbolIdx < len(symbols) && pc >= symbols[symbolIdx].Start && pc < symbols[symbolIdx].End { + symb = symbols[symbolIdx] + } + if curSymbol != nil && curSymbol != symb { + curSymbol.PCs = coverPoints[firstSymbolPC:i] + firstSymbolPC = -1 + } + curSymbol = symb + if symb != nil && firstSymbolPC == -1 { + firstSymbolPC = i + } + } + if curSymbol != nil { + curSymbol.PCs = coverPoints[firstSymbolPC:] + } + // Assign compile units to symbols based on unit pc ranges. + // Do it one pass as both are sorted. + nsymbol := 0 + rangeIndex := 0 + for _, s := range symbols { + for ; rangeIndex < len(ranges) && ranges[rangeIndex].end <= s.Start; rangeIndex++ { + } + if rangeIndex == len(ranges) || s.Start < ranges[rangeIndex].start || len(s.PCs) == 0 { + continue // drop the symbol + } + unit := ranges[rangeIndex].unit + s.Unit = unit + symbols[nsymbol] = s + nsymbol++ + } + symbols = symbols[:nsymbol] + + for _, s := range symbols { + pos := len(s.Unit.PCs) + s.Unit.PCs = append(s.Unit.PCs, s.PCs...) + s.PCs = s.Unit.PCs[pos:] + } + return symbols +} + +func readSymbols(file *elf.File) ([]*Symbol, uint64, error) { + text := file.Section(".text") + if text == nil { + return nil, 0, fmt.Errorf("no .text section in the object file") + } + allSymbols, err := file.Symbols() + if err != nil { + return nil, 0, fmt.Errorf("failed to read ELF symbols: %v", err) + } + var tracePC uint64 + var symbols []*Symbol + for _, symb := range allSymbols { + if symb.Value < text.Addr || symb.Value+symb.Size > text.Addr+text.Size { + continue + } + symbols = append(symbols, &Symbol{ + Name: symb.Name, + Start: symb.Value, + End: symb.Value + symb.Size, + }) + if tracePC == 0 && symb.Name == "__sanitizer_cov_trace_pc" { + tracePC = symb.Value + } + } + if tracePC == 0 { + return nil, 0, fmt.Errorf("no __sanitizer_cov_trace_pc symbol in the object file") + } + sort.Slice(symbols, func(i, j int) bool { + return symbols[i].Start < symbols[j].Start + }) + return symbols, tracePC, nil +} + +func readTextRanges(file *elf.File) ([]pcRange, []*CompileUnit, error) { + text := file.Section(".text") + if text == nil { + return nil, nil, fmt.Errorf("no .text section in the object file") + } + kaslr := file.Section(".rela.text") != nil + debugInfo, err := file.DWARF() + if err != nil { + return nil, nil, fmt.Errorf("failed to parse DWARF: %v (set CONFIG_DEBUG_INFO=y?)", err) + } + var ranges []pcRange + var units []*CompileUnit + for r := debugInfo.Reader(); ; { + ent, err := r.Next() + if err != nil { + return nil, nil, err + } + if ent == nil { + break + } + if ent.Tag != dwarf.TagCompileUnit { + return nil, nil, fmt.Errorf("found unexpected tag %v on top level", ent.Tag) + } + attrName := ent.Val(dwarf.AttrName) + if attrName == nil { + continue + } + unit := &CompileUnit{ + Name: attrName.(string), + } + units = append(units, unit) + ranges1, err := debugInfo.Ranges(ent) + if err != nil { + return nil, nil, err + } + for _, r := range ranges1 { + if r[0] >= r[1] || r[0] < text.Addr || r[1] > text.Addr+text.Size { + if kaslr { + // Linux kernel binaries with CONFIG_RANDOMIZE_BASE=y are strange. + // .text starts at 0xffffffff81000000 and symbols point there as well, + // but PC ranges point to addresses around 0. + // So try to add text offset and retry the check. + // It's unclear if we also need some offset on top of text.Addr, + // it gives approximately correct addresses, but not necessary precisely + // correct addresses. + r[0] += text.Addr + r[1] += text.Addr + if r[0] >= r[1] || r[0] < text.Addr || r[1] > text.Addr+text.Size { + continue + } + } + } + ranges = append(ranges, pcRange{r[0], r[1], unit}) + } + r.SkipChildren() + } + sort.Slice(ranges, func(i, j int) bool { + return ranges[i].start < ranges[j].start + }) + return ranges, units, nil +} + +func symbolize(target *targets.Target, obj string, pcs []uint64) ([]symbolizer.Frame, error) { + procs := runtime.GOMAXPROCS(0) / 2 + if need := len(pcs) / 1000; procs > need { + procs = need + } + const ( + minProcs = 1 + maxProcs = 4 + ) + // addr2line on a beefy vmlinux takes up to 1.6GB of RAM, so don't create too many of them. + if procs > maxProcs { + procs = maxProcs + } + if procs < minProcs { + procs = minProcs + } + type symbolizerResult struct { + frames []symbolizer.Frame + err error + } + symbolizerC := make(chan symbolizerResult, procs) + pcchan := make(chan []uint64, procs) + for p := 0; p < procs; p++ { + go func() { + symb := symbolizer.NewSymbolizer(target) + defer symb.Close() + var res symbolizerResult + for pcs := range pcchan { + frames, err := symb.SymbolizeArray(obj, pcs) + if err != nil { + res.err = fmt.Errorf("failed to symbolize: %v", err) + } + res.frames = append(res.frames, frames...) + } + symbolizerC <- res + }() + } + for i := 0; i < len(pcs); { + end := i + 100 + if end > len(pcs) { + end = len(pcs) + } + pcchan <- pcs[i:end] + i = end + } + close(pcchan) + var err0 error + var frames []symbolizer.Frame + for p := 0; p < procs; p++ { + res := <-symbolizerC + if res.err != nil { + err0 = res.err + } + frames = append(frames, res.frames...) + } + if err0 != nil { + return nil, err0 + } + return frames, nil +} + +// readCoverPoints finds all coverage points (calls of __sanitizer_cov_trace_pc) in the object file. +// Currently it is amd64-specific: looks for e8 opcode and correct offset. +// Running objdump on the whole object file is too slow. +func readCoverPoints(file *elf.File, tracePC uint64) ([]uint64, error) { + text := file.Section(".text") + if text == nil { + return nil, fmt.Errorf("no .text section in the object file") + } + data, err := text.Data() + if err != nil { + return nil, fmt.Errorf("failed to read .text: %v", err) + } + var pcs []uint64 + const callLen = 5 + end := len(data) - callLen + 1 + for i := 0; i < end; i++ { + pos := bytes.IndexByte(data[i:end], 0xe8) + if pos == -1 { + break + } + pos += i + i = pos + off := uint64(int64(int32(binary.LittleEndian.Uint32(data[pos+1:])))) + pc := text.Addr + uint64(pos) + target := pc + off + callLen + if target == tracePC { + pcs = append(pcs, pc) + } + } + return pcs, nil +} + +// objdump is an old, slow way of finding coverage points. +// amd64 uses faster option of parsing binary directly (readCoverPoints). +// TODO: use the faster approach for all other arches and drop this. +func objdump(target *targets.Target, obj string) ([]uint64, error) { + cmd := osutil.Command(target.Objdump, "-d", "--no-show-raw-insn", obj) + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + defer stdout.Close() + stderr, err := cmd.StderrPipe() + if err != nil { + return nil, err + } + defer stderr.Close() + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to run objdump on %v: %v", obj, err) + } + defer func() { + cmd.Process.Kill() + cmd.Wait() + }() + s := bufio.NewScanner(stdout) + callInsns, traceFuncs := archCallInsn(target) + var pcs []uint64 + for s.Scan() { + if pc := parseLine(callInsns, traceFuncs, s.Bytes()); pc != 0 { + pcs = append(pcs, pc) + } + } + stderrOut, _ := ioutil.ReadAll(stderr) + if err := cmd.Wait(); err != nil { + return nil, fmt.Errorf("failed to run objdump on %v: %v\n%s", obj, err, stderrOut) + } + if err := s.Err(); err != nil { + return nil, fmt.Errorf("failed to run objdump on %v: %v\n%s", obj, err, stderrOut) + } + return pcs, nil +} + +func parseLine(callInsns, traceFuncs [][]byte, ln []byte) uint64 { + pos := -1 + for _, callInsn := range callInsns { + if pos = bytes.Index(ln, callInsn); pos != -1 { + break + } + } + if pos == -1 { + return 0 + } + hasCall := false + for _, traceFunc := range traceFuncs { + if hasCall = bytes.Contains(ln[pos:], traceFunc); hasCall { + break + } + } + if !hasCall { + return 0 + } + for len(ln) != 0 && ln[0] == ' ' { + ln = ln[1:] + } + colon := bytes.IndexByte(ln, ':') + if colon == -1 { + return 0 + } + pc, err := strconv.ParseUint(string(ln[:colon]), 16, 64) + if err != nil { + return 0 + } + return pc +} + +func archCallInsn(target *targets.Target) ([][]byte, [][]byte) { + callName := [][]byte{[]byte(" <__sanitizer_cov_trace_pc>")} + switch target.Arch { + case targets.I386: + // c1000102: call c10001f0 <__sanitizer_cov_trace_pc> + return [][]byte{[]byte("\tcall ")}, callName + case targets.ARM64: + // ffff0000080d9cc0: bl ffff00000820f478 <__sanitizer_cov_trace_pc> + return [][]byte{[]byte("\tbl\t")}, callName + case targets.ARM: + // 8010252c: bl 801c3280 <__sanitizer_cov_trace_pc> + return [][]byte{[]byte("\tbl\t")}, callName + case targets.PPC64LE: + // c00000000006d904: bl c000000000350780 <.__sanitizer_cov_trace_pc> + // This is only known to occur in the test: + // 838: bl 824 <__sanitizer_cov_trace_pc+0x8> + // This occurs on PPC64LE: + // c0000000001c21a8: bl c0000000002df4a0 <__sanitizer_cov_trace_pc> + return [][]byte{[]byte("\tbl ")}, [][]byte{ + []byte("<__sanitizer_cov_trace_pc>"), + []byte("<__sanitizer_cov_trace_pc+0x8>"), + []byte(" <.__sanitizer_cov_trace_pc>"), + } + case targets.MIPS64LE: + // ffffffff80100420: jal ffffffff80205880 <__sanitizer_cov_trace_pc> + // This is only known to occur in the test: + // b58: bal b30 <__sanitizer_cov_trace_pc> + return [][]byte{[]byte("\tjal\t"), []byte("\tbal\t")}, callName + case targets.S390x: + // 1001de: brasl %r14,2bc090 <__sanitizer_cov_trace_pc> + return [][]byte{[]byte("\tbrasl\t")}, callName + case targets.RiscV64: + // ffffffe000200018: jal ra,ffffffe0002935b0 <__sanitizer_cov_trace_pc> + // ffffffe0000010da: jalr 1242(ra) # ffffffe0002935b0 <__sanitizer_cov_trace_pc> + return [][]byte{[]byte("\tjal\t"), []byte("\tjalr\t")}, callName + default: + panic(fmt.Sprintf("unknown arch %q", target.Arch)) + } +} diff --git a/pkg/cover/html.go b/pkg/cover/html.go new file mode 100644 index 000000000..c04f3988b --- /dev/null +++ b/pkg/cover/html.go @@ -0,0 +1,448 @@ +// Copyright 2018 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package cover + +import ( + "bytes" + "encoding/csv" + "fmt" + "html" + "html/template" + "io" + "io/ioutil" + "math" + "path/filepath" + "sort" + "strconv" + "strings" +) + +func (rg *ReportGenerator) DoHTML(w io.Writer, progs []Prog) error { + files, err := rg.prepareFileMap(progs) + if err != nil { + return err + } + d := &templateData{ + Root: new(templateDir), + } + for fname, file := range files { + pos := d.Root + path := "" + for { + if path != "" { + path += "/" + } + sep := strings.IndexByte(fname, filepath.Separator) + if sep == -1 { + path += fname + break + } + dir := fname[:sep] + path += dir + if pos.Dirs == nil { + pos.Dirs = make(map[string]*templateDir) + } + if pos.Dirs[dir] == nil { + pos.Dirs[dir] = &templateDir{ + templateBase: templateBase{ + Path: path, + Name: dir, + }, + } + } + pos = pos.Dirs[dir] + fname = fname[sep+1:] + } + f := &templateFile{ + templateBase: templateBase{ + Path: path, + Name: fname, + Total: file.pcs, + Covered: file.covered, + }, + } + pos.Files = append(pos.Files, f) + if file.covered == 0 { + continue + } + lines, err := parseFile(file.filename) + if err != nil { + return err + } + var buf bytes.Buffer + for i, ln := range lines { + cov, ok := file.lines[i+1] + prog, class, count := "", "", " " + if ok { + if len(cov.count) != 0 { + if cov.prog != -1 { + prog = fmt.Sprintf("onclick='onProgClick(%v)'", cov.prog) + } + count = fmt.Sprintf("% 5v", len(cov.count)) + class = "covered" + if cov.uncovered { + class = "both" + } + } else { + class = "uncovered" + } + } + buf.WriteString(fmt.Sprintf("%v", prog, count)) + if class == "" { + buf.WriteByte(' ') + buf.Write(ln) + buf.WriteByte('\n') + } else { + buf.WriteString(fmt.Sprintf(" ", class)) + buf.Write(ln) + buf.WriteString("\n") + } + } + d.Contents = append(d.Contents, template.HTML(buf.String())) + f.Index = len(d.Contents) - 1 + + addFunctionCoverage(file, d) + } + for _, prog := range progs { + d.Progs = append(d.Progs, template.HTML(html.EscapeString(prog.Data))) + } + + processDir(d.Root) + return coverTemplate.Execute(w, d) +} + +var csvHeader = []string{ + "Filename", + "Function", + "Covered PCs", + "Total PCs", +} + +func (rg *ReportGenerator) DoCSV(w io.Writer, progs []Prog) error { + files, err := rg.prepareFileMap(progs) + if err != nil { + return err + } + var data [][]string + for fname, file := range files { + for _, function := range file.functions { + data = append(data, []string{ + fname, + function.name, + strconv.Itoa(function.covered), + strconv.Itoa(function.pcs), + }) + } + } + sort.Slice(data, func(i, j int) bool { + if data[i][0] != data[j][0] { + return data[i][0] < data[j][0] + } + return data[i][1] < data[j][1] + }) + writer := csv.NewWriter(w) + defer writer.Flush() + if err := writer.Write(csvHeader); err != nil { + return err + } + return writer.WriteAll(data) +} + +func addFunctionCoverage(file *file, data *templateData) { + var buf bytes.Buffer + for _, function := range file.functions { + percentage := "" + if function.covered > 0 { + percentage = fmt.Sprintf("%v%%", percent(function.covered, function.pcs)) + } else { + percentage = "---" + } + buf.WriteString(fmt.Sprintf("%v", function.name)) + buf.WriteString(fmt.Sprintf("%v", percentage)) + buf.WriteString(fmt.Sprintf("of %v", strconv.Itoa(function.pcs))) + buf.WriteString("
\n") + } + data.Functions = append(data.Functions, template.HTML(buf.String())) +} + +func processDir(dir *templateDir) { + for len(dir.Dirs) == 1 && len(dir.Files) == 0 { + for _, child := range dir.Dirs { + dir.Name += "/" + child.Name + dir.Files = child.Files + dir.Dirs = child.Dirs + } + } + sort.Slice(dir.Files, func(i, j int) bool { + return dir.Files[i].Name < dir.Files[j].Name + }) + for _, f := range dir.Files { + dir.Total += f.Total + dir.Covered += f.Covered + f.Percent = percent(f.Covered, f.Total) + } + for _, child := range dir.Dirs { + processDir(child) + dir.Total += child.Total + dir.Covered += child.Covered + } + dir.Percent = percent(dir.Covered, dir.Total) + if dir.Covered == 0 { + dir.Dirs = nil + dir.Files = nil + } +} + +func percent(covered, total int) int { + f := math.Ceil(float64(covered) / float64(total) * 100) + if f == 100 && covered < total { + f = 99 + } + return int(f) +} + +func parseFile(fn string) ([][]byte, error) { + data, err := ioutil.ReadFile(fn) + if err != nil { + return nil, err + } + htmlReplacer := strings.NewReplacer(">", ">", "<", "<", "&", "&", "\t", " ") + var lines [][]byte + for { + idx := bytes.IndexByte(data, '\n') + if idx == -1 { + break + } + lines = append(lines, []byte(htmlReplacer.Replace(string(data[:idx])))) + data = data[idx+1:] + } + if len(data) != 0 { + lines = append(lines, data) + } + return lines, nil +} + +type templateData struct { + Root *templateDir + Contents []template.HTML + Progs []template.HTML + Functions []template.HTML +} + +type templateBase struct { + Name string + Path string + Total int + Covered int + Percent int +} + +type templateDir struct { + templateBase + Dirs map[string]*templateDir + Files []*templateFile +} + +type templateFile struct { + templateBase + Index int +} + +var coverTemplate = template.Must(template.New("").Parse(` + + + + + + + +
+ +
+
+ {{range $i, $f := .Contents}} +
{{$f}}
+ {{end}} + {{range $i, $p := .Progs}} +
{{$p}}
+ {{end}} + {{range $i, $p := .Functions}} +
{{$p}}
+ {{end}} +
+ + + + +{{define "dir"}} + {{range $dir := .Dirs}} +
  • + + {{$dir.Name}} + + {{if $dir.Covered}}{{$dir.Percent}}%{{else}}---{{end}} + of {{$dir.Total}} + + + +
  • + {{end}} + {{range $file := .Files}} +
  • + {{if $file.Covered}} + + {{$file.Name}} + + + + {{$file.Percent}}% + + of {{$file.Total}} + + {{else}} + {{$file.Name}}--- + of {{$file.Total}} + {{end}} +
  • + {{end}} +{{end}} +`)) diff --git a/pkg/cover/pc.go b/pkg/cover/pc.go new file mode 100644 index 000000000..5987e8846 --- /dev/null +++ b/pkg/cover/pc.go @@ -0,0 +1,55 @@ +// Copyright 2020 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package cover + +import ( + "fmt" + + "github.com/google/syzkaller/sys/targets" +) + +func PreviousInstructionPC(target *targets.Target, pc uint64) uint64 { + offset := instructionLen(target.Arch) + pc -= offset + // THUMB instructions are 2 or 4 bytes with low bit set. + // ARM instructions are always 4 bytes. + if target.Arch == targets.ARM { + return pc & ^uint64(1) + } + return pc +} + +func NextInstructionPC(target *targets.Target, pc uint64) uint64 { + offset := instructionLen(target.Arch) + pc += offset + // THUMB instructions are 2 or 4 bytes with low bit set. + // ARM instructions are always 4 bytes. + if target.Arch == targets.ARM { + return pc & ^uint64(1) + } + return pc +} + +func instructionLen(arch string) uint64 { + switch arch { + case targets.AMD64: + return 5 + case targets.I386: + return 5 + case targets.ARM64: + return 4 + case targets.ARM: + return 3 + case targets.PPC64LE: + return 4 + case targets.MIPS64LE: + return 8 + case targets.S390x: + return 6 + case targets.RiscV64: + return 4 + default: + panic(fmt.Sprintf("unknown arch %q", arch)) + } +} diff --git a/pkg/cover/report.go b/pkg/cover/report.go index 6f711f11a..4c9e9d9d0 100644 --- a/pkg/cover/report.go +++ b/pkg/cover/report.go @@ -4,38 +4,21 @@ package cover import ( - "bufio" - "bytes" - "debug/dwarf" - "debug/elf" - "encoding/binary" - "encoding/csv" "fmt" - "html" - "html/template" - "io" - "io/ioutil" - "math" "path/filepath" - "runtime" "sort" - "strconv" "strings" - "github.com/google/syzkaller/pkg/osutil" - "github.com/google/syzkaller/pkg/symbolizer" + "github.com/google/syzkaller/pkg/cover/backend" "github.com/google/syzkaller/sys/targets" ) type ReportGenerator struct { - target *targets.Target - kernelObject string - srcDir string - objDir string - buildDir string - units []*compileUnit - symbols []*symbol - pcs map[uint64][]pcFrame + target *targets.Target + srcDir string + objDir string + buildDir string + *backend.Impl } type Prog struct { @@ -43,135 +26,22 @@ type Prog struct { PCs []uint64 } -type symbol struct { - name string - unit *compileUnit - start uint64 - end uint64 - pcs []uint64 - symbolized bool -} - -type compileUnit struct { - name string - filename string - pcs int -} - -type pcFrame struct { - symbolizer.Frame - filename string -} - -type pcRange struct { - start uint64 - end uint64 - cu *compileUnit -} - func MakeReportGenerator(target *targets.Target, kernelObject, srcDir, buildDir string) (*ReportGenerator, error) { - file, err := elf.Open(kernelObject) + impl, err := backend.Make(target, kernelObject, srcDir, buildDir) if err != nil { return nil, err } - var coverPoints []uint64 - var symbols []*symbol - errc := make(chan error, 1) - go func() { - var err error - var tracePC uint64 - symbols, tracePC, err = readSymbols(file) - if err != nil { - errc <- err - return - } - if target.Arch == targets.AMD64 { - coverPoints, err = readCoverPoints(file, tracePC) - } else { - coverPoints, err = objdump(target, kernelObject) - } - errc <- err - }() - ranges, units, err := readTextRanges(file) - if err != nil { - return nil, err - } - if err := <-errc; err != nil { - return nil, err - } - if len(coverPoints) == 0 { - return nil, fmt.Errorf("%v doesn't contain coverage callbacks (set CONFIG_KCOV=y)", kernelObject) - } - symbols = buildSymbols(symbols, ranges, coverPoints) - objDir := filepath.Dir(kernelObject) - nunit := 0 - for _, unit := range units { - if unit.pcs == 0 { - continue // drop the unit - } - unit.name, unit.filename = cleanPath(unit.name, srcDir, objDir, buildDir) - units[nunit] = unit - nunit++ - } - units = units[:nunit] - if len(symbols) == 0 || len(units) == 0 { - return nil, fmt.Errorf("failed to parse DWARF (set CONFIG_DEBUG_INFO=y?)") - } rg := &ReportGenerator{ - target: target, - kernelObject: kernelObject, - srcDir: srcDir, - objDir: objDir, - buildDir: buildDir, - units: units, - symbols: symbols, - pcs: make(map[uint64][]pcFrame), - } - return rg, nil -} - -func buildSymbols(symbols []*symbol, ranges []pcRange, coverPoints []uint64) []*symbol { - // Assign coverage point PCs to symbols. - // Both symbols and coverage points are sorted, so we do it one pass over both. - var curSymbol *symbol - firstSymbolPC, symbolIdx := -1, 0 - for i := 0; i < len(coverPoints); i++ { - pc := coverPoints[i] - for ; symbolIdx < len(symbols) && pc >= symbols[symbolIdx].end; symbolIdx++ { - } - var symb *symbol - if symbolIdx < len(symbols) && pc >= symbols[symbolIdx].start && pc < symbols[symbolIdx].end { - symb = symbols[symbolIdx] - } - if curSymbol != nil && curSymbol != symb { - curSymbol.pcs = coverPoints[firstSymbolPC:i] - firstSymbolPC = -1 - } - curSymbol = symb - if symb != nil && firstSymbolPC == -1 { - firstSymbolPC = i - } + target: target, + srcDir: srcDir, + objDir: filepath.Dir(kernelObject), + buildDir: buildDir, + Impl: impl, } - if curSymbol != nil { - curSymbol.pcs = coverPoints[firstSymbolPC:] + for _, unit := range rg.Units { + unit.Name, unit.Path = rg.cleanPath(unit.Name) } - // Assign compile units to symbols based on unit pc ranges. - // Do it one pass as both are sorted. - nsymbol := 0 - rangeIndex := 0 - for _, s := range symbols { - for ; rangeIndex < len(ranges) && ranges[rangeIndex].end <= s.start; rangeIndex++ { - } - if rangeIndex == len(ranges) || s.start < ranges[rangeIndex].start || len(s.pcs) == 0 { - continue // drop the symbol - } - unit := ranges[rangeIndex].cu - s.unit = unit - unit.pcs += len(s.pcs) - symbols[nsymbol] = s - nsymbol++ - } - return symbols[:nsymbol] + return rg, nil } type file struct { @@ -194,88 +64,79 @@ type line struct { uncovered bool } -func (rg *ReportGenerator) DoHTML(buf io.Writer, progs []Prog) error { - files, err := rg.prepareFileMap(progs) - if err != nil { - return err - } - return rg.generateHTML(buf, progs, files) -} - -func (rg *ReportGenerator) DoCSV(buf io.Writer, progs []Prog) error { - files, err := rg.prepareFileMap(progs) - if err != nil { - return err - } - return rg.generateCSV(buf, progs, files) -} - func (rg *ReportGenerator) prepareFileMap(progs []Prog) (map[string]*file, error) { + if err := rg.lazySymbolize(progs); err != nil { + return nil, err + } files := make(map[string]*file) - for _, unit := range rg.units { - f := &file{ - filename: unit.filename, + for _, unit := range rg.Units { + files[unit.Name] = &file{ + filename: unit.Path, lines: make(map[int]line), - pcs: unit.pcs, + pcs: len(unit.PCs), } - files[unit.name] = f } - if err := rg.lazySymbolize(files, progs); err != nil { - return nil, err - } - coveredPCs := make(map[uint64]bool) - for progIdx, prog := range progs { + progPCs := make(map[uint64]map[int]bool) + for i, prog := range progs { for _, pc := range prog.PCs { - frames, ok := rg.pcs[pc] - if !ok { - continue - } - coveredPCs[pc] = true - for _, frame := range frames { - f := getFile(files, frame.File, frame.filename) - ln := f.lines[frame.Line] - if ln.count == nil { - ln.count = make(map[int]bool) - ln.prog = -1 - } + if progPCs[pc] == nil { + progPCs[pc] = make(map[int]bool) + } + progPCs[pc][i] = true + } + } + matchedPC := false + for _, frame := range rg.Frames { + name, path := rg.cleanPath(frame.File) + f := getFile(files, name, path) + ln := f.lines[frame.Line] + coveredBy := progPCs[frame.PC] + if len(coveredBy) != 0 { + // Covered frame. + matchedPC = true + if ln.count == nil { + ln.count = make(map[int]bool) + ln.prog = -1 + } + for progIdx := range coveredBy { ln.count[progIdx] = true - if ln.prog == -1 || len(prog.Data) < len(progs[ln.prog].Data) { + if ln.prog == -1 || len(progs[progIdx].Data) < len(progs[ln.prog].Data) { ln.prog = progIdx } - f.lines[frame.Line] = ln + } + } else { + // Uncovered frame. + if !frame.Inline || len(ln.count) == 0 { + ln.uncovered = true } } + f.lines[frame.Line] = ln } - if len(coveredPCs) == 0 { + if !matchedPC { return nil, fmt.Errorf("coverage doesn't match any coverage callbacks") } - for pc, frames := range rg.pcs { - if coveredPCs[pc] { - continue - } - for _, frame := range frames { - f := getFile(files, frame.File, frame.filename) - ln := f.lines[frame.Line] - if !frame.Inline || len(ln.count) == 0 { - ln.uncovered = true - f.lines[frame.Line] = ln + for _, unit := range rg.Units { + f := files[unit.Name] + for _, pc := range unit.PCs { + if progPCs[pc] != nil { + f.covered++ } } } - for _, s := range rg.symbols { - f := files[s.unit.name] - covered := 0 - for _, pc := range s.pcs { - if coveredPCs[pc] { - covered++ - f.covered++ + for _, s := range rg.Symbols { + fun := &function{ + name: s.Name, + pcs: len(s.PCs), + } + for _, pc := range s.PCs { + if progPCs[pc] != nil { + fun.covered++ } } - f.functions = append(f.functions, &function{ - name: s.name, - pcs: len(s.pcs), - covered: covered, - }) + f := files[s.Unit.Name] + f.functions = append(f.functions, fun) + } + for _, f := range files { sort.Slice(f.functions, func(i, j int) bool { return f.functions[i].name < f.functions[j].name }) @@ -283,72 +144,51 @@ func (rg *ReportGenerator) prepareFileMap(progs []Prog) (map[string]*file, error return files, nil } -func (rg *ReportGenerator) lazySymbolize(files map[string]*file, progs []Prog) error { +func (rg *ReportGenerator) lazySymbolize(progs []Prog) error { + if len(rg.Symbols) == 0 { + return nil + } + symbolize := make(map[*backend.Symbol]bool) uniquePCs := make(map[uint64]bool) - symbolizeSymbols := make(map[*symbol]bool) - var symbolizePCs []uint64 - anyPCs := false + var pcs []uint64 for _, prog := range progs { for _, pc := range prog.PCs { - anyPCs = true if uniquePCs[pc] { continue } uniquePCs[pc] = true - s := findSymbol(rg.symbols, pc) - if s == nil { + sym := rg.findSymbol(pc) + if sym == nil { continue } - if !s.symbolized && !symbolizeSymbols[s] { - symbolizeSymbols[s] = true - symbolizePCs = append(symbolizePCs, s.pcs...) + if !sym.Symbolized && !symbolize[sym] { + symbolize[sym] = true + pcs = append(pcs, sym.PCs...) } } } - if !anyPCs { + if len(uniquePCs) == 0 { return fmt.Errorf("no coverage collected so far") } - if len(symbolizeSymbols) == 0 { + if len(pcs) == 0 { return nil } - frames, err := symbolize(rg.target, rg.kernelObject, symbolizePCs) + frames, err := rg.Symbolize(pcs) if err != nil { return err } - for _, frame := range frames { - f := pcFrame{frame, ""} - f.File, f.filename = cleanPath(frame.File, rg.srcDir, rg.objDir, rg.buildDir) - rg.pcs[frame.PC] = append(rg.pcs[frame.PC], f) - } - for s := range symbolizeSymbols { - s.symbolized = true + rg.Frames = append(rg.Frames, frames...) + for sym := range symbolize { + sym.Symbolized = true } return nil } -type Symbol struct { - Name string - File string - PCs []uint64 -} - -func (rg *ReportGenerator) GetSymbols() []Symbol { - var ret []Symbol - for _, sym := range rg.symbols { - ret = append(ret, Symbol{ - Name: sym.name, - File: sym.unit.name, - PCs: sym.pcs, - }) - } - return ret -} - -func getFile(files map[string]*file, name, filename string) *file { +func getFile(files map[string]*file, name, path string) *file { f := files[name] if f == nil { f = &file{ - filename: filename, + filename: path, lines: make(map[int]line), // Special mark for header files, if a file does not have coverage at all it is not shown. pcs: 1, @@ -359,798 +199,34 @@ func getFile(files map[string]*file, name, filename string) *file { return f } -var csvHeader = []string{ - "Filename", - "Function", - "Covered PCs", - "Total PCs", -} - -func (rg *ReportGenerator) generateCSV(w io.Writer, progs []Prog, files map[string]*file) error { - var data [][]string - for fname, file := range files { - for _, function := range file.functions { - data = append(data, []string{ - fname, - function.name, - strconv.Itoa(function.covered), - strconv.Itoa(function.pcs), - }) - } - } - sort.Slice(data, func(i, j int) bool { - if data[i][0] != data[j][0] { - return data[i][0] < data[j][0] - } - return data[i][1] < data[j][1] - }) - writer := csv.NewWriter(w) - defer writer.Flush() - if err := writer.Write(csvHeader); err != nil { - return err - } - return writer.WriteAll(data) -} - -func (rg *ReportGenerator) generateHTML(w io.Writer, progs []Prog, files map[string]*file) error { - d := &templateData{ - Root: new(templateDir), - } - for fname, file := range files { - pos := d.Root - path := "" - for { - if path != "" { - path += "/" - } - sep := strings.IndexByte(fname, filepath.Separator) - if sep == -1 { - path += fname - break - } - dir := fname[:sep] - path += dir - if pos.Dirs == nil { - pos.Dirs = make(map[string]*templateDir) - } - if pos.Dirs[dir] == nil { - pos.Dirs[dir] = &templateDir{ - templateBase: templateBase{ - Path: path, - Name: dir, - }, - } - } - pos = pos.Dirs[dir] - fname = fname[sep+1:] - } - f := &templateFile{ - templateBase: templateBase{ - Path: path, - Name: fname, - Total: file.pcs, - Covered: file.covered, - }, - } - pos.Files = append(pos.Files, f) - if file.covered == 0 { - continue - } - lines, err := parseFile(file.filename) - if err != nil { - return err - } - var buf bytes.Buffer - for i, ln := range lines { - cov, ok := file.lines[i+1] - prog, class, count := "", "", " " - if ok { - if len(cov.count) != 0 { - if cov.prog != -1 { - prog = fmt.Sprintf("onclick='onProgClick(%v)'", cov.prog) - } - count = fmt.Sprintf("% 5v", len(cov.count)) - class = "covered" - if cov.uncovered { - class = "both" - } - } else { - class = "uncovered" - } - } - buf.WriteString(fmt.Sprintf("%v", prog, count)) - if class == "" { - buf.WriteByte(' ') - buf.Write(ln) - buf.WriteByte('\n') - } else { - buf.WriteString(fmt.Sprintf(" ", class)) - buf.Write(ln) - buf.WriteString("\n") - } - } - d.Contents = append(d.Contents, template.HTML(buf.String())) - f.Index = len(d.Contents) - 1 - - addFunctionCoverage(file, d) - } - for _, prog := range progs { - d.Progs = append(d.Progs, template.HTML(html.EscapeString(prog.Data))) - } - - processDir(d.Root) - return coverTemplate.Execute(w, d) -} - -func addFunctionCoverage(file *file, data *templateData) { - var buf bytes.Buffer - for _, function := range file.functions { - percentage := "" - if function.covered > 0 { - percentage = fmt.Sprintf("%v%%", percent(function.covered, function.pcs)) - } else { - percentage = "---" - } - buf.WriteString(fmt.Sprintf("%v", function.name)) - buf.WriteString(fmt.Sprintf("%v", percentage)) - buf.WriteString(fmt.Sprintf("of %v", strconv.Itoa(function.pcs))) - buf.WriteString("
    \n") - } - data.Functions = append(data.Functions, template.HTML(buf.String())) -} - -func processDir(dir *templateDir) { - for len(dir.Dirs) == 1 && len(dir.Files) == 0 { - for _, child := range dir.Dirs { - dir.Name += "/" + child.Name - dir.Files = child.Files - dir.Dirs = child.Dirs - } - } - sort.Slice(dir.Files, func(i, j int) bool { - return dir.Files[i].Name < dir.Files[j].Name - }) - for _, f := range dir.Files { - dir.Total += f.Total - dir.Covered += f.Covered - f.Percent = percent(f.Covered, f.Total) - } - for _, child := range dir.Dirs { - processDir(child) - dir.Total += child.Total - dir.Covered += child.Covered - } - dir.Percent = percent(dir.Covered, dir.Total) - if dir.Covered == 0 { - dir.Dirs = nil - dir.Files = nil - } -} - -func cleanPath(path, srcDir, objDir, buildDir string) (string, string) { +func (rg *ReportGenerator) cleanPath(path string) (string, string) { filename := "" switch { - case strings.HasPrefix(path, objDir): + case strings.HasPrefix(path, rg.objDir): // Assume the file was built there. - path = strings.TrimPrefix(path, objDir) - filename = filepath.Join(objDir, path) - case strings.HasPrefix(path, buildDir): + path = strings.TrimPrefix(path, rg.objDir) + filename = filepath.Join(rg.objDir, path) + case strings.HasPrefix(path, rg.buildDir): // Assume the file was moved from buildDir to srcDir. - path = strings.TrimPrefix(path, buildDir) - filename = filepath.Join(srcDir, path) + path = strings.TrimPrefix(path, rg.buildDir) + filename = filepath.Join(rg.srcDir, path) default: // Assume this is relative path. - filename = filepath.Join(srcDir, path) + filename = filepath.Join(rg.srcDir, path) } return strings.TrimLeft(filepath.Clean(path), "/\\"), filename } -func percent(covered, total int) int { - f := math.Ceil(float64(covered) / float64(total) * 100) - if f == 100 && covered < total { - f = 99 - } - return int(f) -} - -func findSymbol(symbols []*symbol, pc uint64) *symbol { - idx := sort.Search(len(symbols), func(i int) bool { - return pc < symbols[i].end +func (rg *ReportGenerator) findSymbol(pc uint64) *backend.Symbol { + idx := sort.Search(len(rg.Symbols), func(i int) bool { + return pc < rg.Symbols[i].End }) - if idx == len(symbols) { + if idx == len(rg.Symbols) { return nil } - s := symbols[idx] - if pc < s.start || pc > s.end { + s := rg.Symbols[idx] + if pc < s.Start || pc > s.End { return nil } return s } - -func readSymbols(file *elf.File) ([]*symbol, uint64, error) { - text := file.Section(".text") - if text == nil { - return nil, 0, fmt.Errorf("no .text section in the object file") - } - allSymbols, err := file.Symbols() - if err != nil { - return nil, 0, fmt.Errorf("failed to read ELF symbols: %v", err) - } - var tracePC uint64 - var symbols []*symbol - for _, symb := range allSymbols { - if symb.Value < text.Addr || symb.Value+symb.Size > text.Addr+text.Size { - continue - } - symbols = append(symbols, &symbol{ - name: symb.Name, - start: symb.Value, - end: symb.Value + symb.Size, - }) - if tracePC == 0 && symb.Name == "__sanitizer_cov_trace_pc" { - tracePC = symb.Value - } - } - if tracePC == 0 { - return nil, 0, fmt.Errorf("no __sanitizer_cov_trace_pc symbol in the object file") - } - sort.Slice(symbols, func(i, j int) bool { - return symbols[i].start < symbols[j].start - }) - return symbols, tracePC, nil -} - -func readTextRanges(file *elf.File) ([]pcRange, []*compileUnit, error) { - text := file.Section(".text") - if text == nil { - return nil, nil, fmt.Errorf("no .text section in the object file") - } - kaslr := file.Section(".rela.text") != nil - debugInfo, err := file.DWARF() - if err != nil { - return nil, nil, fmt.Errorf("failed to parse DWARF: %v (set CONFIG_DEBUG_INFO=y?)", err) - } - var ranges []pcRange - var units []*compileUnit - for r := debugInfo.Reader(); ; { - ent, err := r.Next() - if err != nil { - return nil, nil, err - } - if ent == nil { - break - } - if ent.Tag != dwarf.TagCompileUnit { - return nil, nil, fmt.Errorf("found unexpected tag %v on top level", ent.Tag) - } - attrName := ent.Val(dwarf.AttrName) - if attrName == nil { - continue - } - unit := &compileUnit{ - name: attrName.(string), - } - units = append(units, unit) - ranges1, err := debugInfo.Ranges(ent) - if err != nil { - return nil, nil, err - } - for _, r := range ranges1 { - if r[0] >= r[1] || r[0] < text.Addr || r[1] > text.Addr+text.Size { - if kaslr { - // Linux kernel binaries with CONFIG_RANDOMIZE_BASE=y are strange. - // .text starts at 0xffffffff81000000 and symbols point there as well, - // but PC ranges point to addresses around 0. - // So try to add text offset and retry the check. - // It's unclear if we also need some offset on top of text.Addr, - // it gives approximately correct addresses, but not necessary precisely - // correct addresses. - r[0] += text.Addr - r[1] += text.Addr - if r[0] >= r[1] || r[0] < text.Addr || r[1] > text.Addr+text.Size { - continue - } - } - } - ranges = append(ranges, pcRange{r[0], r[1], unit}) - } - r.SkipChildren() - } - sort.Slice(ranges, func(i, j int) bool { - return ranges[i].start < ranges[j].start - }) - return ranges, units, nil -} - -func symbolize(target *targets.Target, obj string, pcs []uint64) ([]symbolizer.Frame, error) { - procs := runtime.GOMAXPROCS(0) / 2 - const ( - minProcs = 1 - maxProcs = 4 - ) - // addr2line on a beefy vmlinux takes up to 1.6GB of RAM, so don't create too many of them. - if procs > maxProcs { - procs = maxProcs - } - if procs < minProcs { - procs = minProcs - } - type symbolizerResult struct { - frames []symbolizer.Frame - err error - } - symbolizerC := make(chan symbolizerResult, procs) - pcchan := make(chan []uint64, procs) - for p := 0; p < procs; p++ { - go func() { - symb := symbolizer.NewSymbolizer(target) - defer symb.Close() - var res symbolizerResult - for pcs := range pcchan { - frames, err := symb.SymbolizeArray(obj, pcs) - if err != nil { - res.err = fmt.Errorf("failed to symbolize: %v", err) - } - res.frames = append(res.frames, frames...) - } - symbolizerC <- res - }() - } - for i := 0; i < len(pcs); { - end := i + 100 - if end > len(pcs) { - end = len(pcs) - } - pcchan <- pcs[i:end] - i = end - } - close(pcchan) - var err0 error - var frames []symbolizer.Frame - for p := 0; p < procs; p++ { - res := <-symbolizerC - if res.err != nil { - err0 = res.err - } - frames = append(frames, res.frames...) - } - if err0 != nil { - return nil, err0 - } - return frames, nil -} - -// readCoverPoints finds all coverage points (calls of __sanitizer_cov_trace_pc) in the object file. -// Currently it is amd64-specific: looks for e8 opcode and correct offset. -// Running objdump on the whole object file is too slow. -func readCoverPoints(file *elf.File, tracePC uint64) ([]uint64, error) { - text := file.Section(".text") - if text == nil { - return nil, fmt.Errorf("no .text section in the object file") - } - data, err := text.Data() - if err != nil { - return nil, fmt.Errorf("failed to read .text: %v", err) - } - var pcs []uint64 - const callLen = 5 - end := len(data) - callLen + 1 - for i := 0; i < end; i++ { - pos := bytes.IndexByte(data[i:end], 0xe8) - if pos == -1 { - break - } - pos += i - i = pos - off := uint64(int64(int32(binary.LittleEndian.Uint32(data[pos+1:])))) - pc := text.Addr + uint64(pos) - target := pc + off + callLen - if target == tracePC { - pcs = append(pcs, pc) - } - } - return pcs, nil -} - -// objdump is an old, slow way of finding coverage points. -// amd64 uses faster option of parsing binary directly (readCoverPoints). -// TODO: use the faster approach for all other arches and drop this. -func objdump(target *targets.Target, obj string) ([]uint64, error) { - cmd := osutil.Command(target.Objdump, "-d", "--no-show-raw-insn", obj) - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, err - } - defer stdout.Close() - stderr, err := cmd.StderrPipe() - if err != nil { - return nil, err - } - defer stderr.Close() - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("failed to run objdump on %v: %v", obj, err) - } - defer func() { - cmd.Process.Kill() - cmd.Wait() - }() - s := bufio.NewScanner(stdout) - callInsns, traceFuncs := archCallInsn(target) - var pcs []uint64 - for s.Scan() { - if pc := parseLine(callInsns, traceFuncs, s.Bytes()); pc != 0 { - pcs = append(pcs, pc) - } - } - stderrOut, _ := ioutil.ReadAll(stderr) - if err := cmd.Wait(); err != nil { - return nil, fmt.Errorf("failed to run objdump on %v: %v\n%s", obj, err, stderrOut) - } - if err := s.Err(); err != nil { - return nil, fmt.Errorf("failed to run objdump on %v: %v\n%s", obj, err, stderrOut) - } - return pcs, nil -} - -func parseLine(callInsns, traceFuncs [][]byte, ln []byte) uint64 { - pos := -1 - for _, callInsn := range callInsns { - if pos = bytes.Index(ln, callInsn); pos != -1 { - break - } - } - if pos == -1 { - return 0 - } - hasCall := false - for _, traceFunc := range traceFuncs { - if hasCall = bytes.Contains(ln[pos:], traceFunc); hasCall { - break - } - } - if !hasCall { - return 0 - } - for len(ln) != 0 && ln[0] == ' ' { - ln = ln[1:] - } - colon := bytes.IndexByte(ln, ':') - if colon == -1 { - return 0 - } - pc, err := strconv.ParseUint(string(ln[:colon]), 16, 64) - if err != nil { - return 0 - } - return pc -} - -func parseFile(fn string) ([][]byte, error) { - data, err := ioutil.ReadFile(fn) - if err != nil { - return nil, err - } - htmlReplacer := strings.NewReplacer(">", ">", "<", "<", "&", "&", "\t", " ") - var lines [][]byte - for { - idx := bytes.IndexByte(data, '\n') - if idx == -1 { - break - } - lines = append(lines, []byte(htmlReplacer.Replace(string(data[:idx])))) - data = data[idx+1:] - } - if len(data) != 0 { - lines = append(lines, data) - } - return lines, nil -} - -func PreviousInstructionPC(target *targets.Target, pc uint64) uint64 { - offset := instructionLen(target.Arch) - pc -= offset - // THUMB instructions are 2 or 4 bytes with low bit set. - // ARM instructions are always 4 bytes. - if target.Arch == targets.ARM { - return pc & ^uint64(1) - } - return pc -} - -func NextInstructionPC(target *targets.Target, pc uint64) uint64 { - offset := instructionLen(target.Arch) - pc += offset - // THUMB instructions are 2 or 4 bytes with low bit set. - // ARM instructions are always 4 bytes. - if target.Arch == targets.ARM { - return pc & ^uint64(1) - } - return pc -} - -func instructionLen(arch string) uint64 { - switch arch { - case targets.AMD64: - return 5 - case targets.I386: - return 5 - case targets.ARM64: - return 4 - case targets.ARM: - return 3 - case targets.PPC64LE: - return 4 - case targets.MIPS64LE: - return 8 - case targets.S390x: - return 6 - case targets.RiscV64: - return 4 - default: - panic(fmt.Sprintf("unknown arch %q", arch)) - } -} - -func archCallInsn(target *targets.Target) ([][]byte, [][]byte) { - callName := [][]byte{[]byte(" <__sanitizer_cov_trace_pc>")} - switch target.Arch { - case targets.I386: - // c1000102: call c10001f0 <__sanitizer_cov_trace_pc> - return [][]byte{[]byte("\tcall ")}, callName - case targets.ARM64: - // ffff0000080d9cc0: bl ffff00000820f478 <__sanitizer_cov_trace_pc> - return [][]byte{[]byte("\tbl\t")}, callName - case targets.ARM: - // 8010252c: bl 801c3280 <__sanitizer_cov_trace_pc> - return [][]byte{[]byte("\tbl\t")}, callName - case targets.PPC64LE: - // c00000000006d904: bl c000000000350780 <.__sanitizer_cov_trace_pc> - // This is only known to occur in the test: - // 838: bl 824 <__sanitizer_cov_trace_pc+0x8> - // This occurs on PPC64LE: - // c0000000001c21a8: bl c0000000002df4a0 <__sanitizer_cov_trace_pc> - return [][]byte{[]byte("\tbl ")}, [][]byte{ - []byte("<__sanitizer_cov_trace_pc>"), - []byte("<__sanitizer_cov_trace_pc+0x8>"), - []byte(" <.__sanitizer_cov_trace_pc>"), - } - case targets.MIPS64LE: - // ffffffff80100420: jal ffffffff80205880 <__sanitizer_cov_trace_pc> - // This is only known to occur in the test: - // b58: bal b30 <__sanitizer_cov_trace_pc> - return [][]byte{[]byte("\tjal\t"), []byte("\tbal\t")}, callName - case targets.S390x: - // 1001de: brasl %r14,2bc090 <__sanitizer_cov_trace_pc> - return [][]byte{[]byte("\tbrasl\t")}, callName - case targets.RiscV64: - // ffffffe000200018: jal ra,ffffffe0002935b0 <__sanitizer_cov_trace_pc> - // ffffffe0000010da: jalr 1242(ra) # ffffffe0002935b0 <__sanitizer_cov_trace_pc> - return [][]byte{[]byte("\tjal\t"), []byte("\tjalr\t")}, callName - default: - panic(fmt.Sprintf("unknown arch %q", target.Arch)) - } -} - -type templateData struct { - Root *templateDir - Contents []template.HTML - Progs []template.HTML - Functions []template.HTML -} - -type templateBase struct { - Name string - Path string - Total int - Covered int - Percent int -} - -type templateDir struct { - templateBase - Dirs map[string]*templateDir - Files []*templateFile -} - -type templateFile struct { - templateBase - Index int -} - -var coverTemplate = template.Must(template.New("").Parse(` - - - - - - - -
    - -
    -
    - {{range $i, $f := .Contents}} -
    {{$f}}
    - {{end}} - {{range $i, $p := .Progs}} -
    {{$p}}
    - {{end}} - {{range $i, $p := .Functions}} -
    {{$p}}
    - {{end}} -
    - - - - -{{define "dir"}} - {{range $dir := .Dirs}} -
  • - - {{$dir.Name}} - - {{if $dir.Covered}}{{$dir.Percent}}%{{else}}---{{end}} - of {{$dir.Total}} - - - -
  • - {{end}} - {{range $file := .Files}} -
  • - {{if $file.Covered}} - - {{$file.Name}} - - - - {{$file.Percent}}% - - of {{$file.Total}} - - {{else}} - {{$file.Name}}--- - of {{$file.Total}} - {{end}} -
  • - {{end}} -{{end}} -`)) -- cgit mrf-deployment