1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
|
// Copyright 2024 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 golang
// This file produces the "Browse GOARCH assembly of f" HTML report.
//
// See also:
// - ./codeaction.go - computes the symbol and offers the CodeAction command.
// - ../server/command.go - handles the command by opening a web page.
// - ../server/server.go - handles the HTTP request and calls this function.
import (
"bytes"
"context"
"fmt"
"html"
"path/filepath"
"regexp"
"strconv"
"strings"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/internal/gocommand"
)
// AssemblyHTML returns an HTML document containing an assembly listing of the selected function.
//
// TODO(adonovan):
// - display a "Compiling..." message as a cold build can be slow.
// - cross-link jumps and block labels, like github.com/aclements/objbrowse.
func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, symbol string, web Web) ([]byte, error) {
// Compile the package with -S, and capture its stderr stream.
inv, cleanupInvocation, err := snapshot.GoCommandInvocation(false, &gocommand.Invocation{
Verb: "build",
Args: []string{"-gcflags=-S", "."},
WorkingDir: filepath.Dir(pkg.Metadata().CompiledGoFiles[0].Path()),
})
if err != nil {
return nil, err // e.g. failed to write overlays (rare)
}
defer cleanupInvocation()
_, stderr, err, _ := snapshot.View().GoCommandRunner().RunRaw(ctx, *inv)
if err != nil {
return nil, err // e.g. won't compile
}
content := stderr.String()
escape := html.EscapeString
// Produce the report.
title := fmt.Sprintf("%s assembly for %s",
escape(snapshot.View().GOARCH()),
escape(symbol))
var buf bytes.Buffer
buf.WriteString(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>` + escape(title) + `</title>
<link rel="stylesheet" href="/assets/common.css">
<script src="/assets/common.js"></script>
</head>
<body>
<h1>` + title + `</h1>
<p>
<a href='https://go.dev/doc/asm'>A Quick Guide to Go's Assembler</a>
</p>
<p>
Experimental. <a href='https://github.com/golang/go/issues/67478'>Contributions welcome!</a>
</p>
<p>
Click on a source line marker <code>L1234</code> to navigate your editor there.
(VS Code users: please upvote <a href='https://github.com/microsoft/vscode/issues/208093'>#208093</a>)
</p>
<p>
Reload the page to recompile.
</p>
<pre>
`)
// insnRx matches an assembly instruction line.
// Submatch groups are: (offset-hex-dec, file-line-column, instruction).
insnRx := regexp.MustCompile(`^(\s+0x[0-9a-f ]+)\(([^)]*)\)\s+(.*)$`)
// Parse the functions of interest out of the listing.
// Each function is of the form:
//
// symbol STEXT k=v...
// 0x0000 00000 (/file.go:123) NOP...
// ...
//
// Allow matches of symbol, symbol.func1, symbol.deferwrap, etc.
on := false
for _, line := range strings.Split(content, "\n") {
// start of function symbol?
if strings.Contains(line, " STEXT ") {
on = strings.HasPrefix(line, symbol) &&
(line[len(symbol)] == ' ' || line[len(symbol)] == '.')
}
if !on {
continue // within uninteresting symbol
}
// In lines of the form
// "\t0x0000 00000 (/file.go:123) NOP..."
// replace the "(/file.go:123)" portion with an "L0123" source link.
// Skip filenames of the form "<foo>".
if parts := insnRx.FindStringSubmatch(line); parts != nil {
link := " " // if unknown
if file, linenum, ok := cutLast(parts[2], ":"); ok && !strings.HasPrefix(file, "<") {
if linenum, err := strconv.Atoi(linenum); err == nil {
text := fmt.Sprintf("L%04d", linenum)
link = sourceLink(text, web.SrcURL(file, linenum, 1))
}
}
fmt.Fprintf(&buf, "%s\t%s\t%s", escape(parts[1]), link, escape(parts[3]))
} else {
buf.WriteString(escape(line))
}
buf.WriteByte('\n')
}
return buf.Bytes(), nil
}
// cutLast is the "last" analogue of [strings.Cut].
func cutLast(s, sep string) (before, after string, ok bool) {
if i := strings.LastIndex(s, sep); i >= 0 {
return s[:i], s[i+len(sep):], true
}
return s, "", false
}
|