blob: c91482ffef780c8a84cea5e46f3f828ac371ab86 [file] [log] [blame]
// 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.
//
// For language-server behavior in Go assembly language files,
// see [golang.org/x/tools/gopls/internal/goasm].
import (
"bytes"
"context"
"fmt"
"html"
"io"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/util/morestrings"
)
// AssemblyHTML returns an HTML document containing an assembly listing of the selected function.
//
// TODO(adonovan): cross-link jumps and block labels, like github.com/aclements/objbrowse.
//
// See gopls/internal/test/integration/misc/webserver_test.go for tests.
func AssemblyHTML(ctx context.Context, snapshot *cache.Snapshot, w http.ResponseWriter, pkg *cache.Package, symbol string, web Web) {
// Prepare to compile the package with -S, and capture its stderr stream.
// We use "go test -c" not "go build" as it covers all three packages
// (p, "p [p.test]", "p_test [p.test]") in the directory, if they exist.
// (See also compileropt.go.)
inv, cleanupInvocation, err := snapshot.GoCommandInvocation(cache.NoNetwork, pkg.Metadata().CompiledGoFiles[0].DirPath(),
"test", []string{
"-c",
"-o", os.DevNull,
"-gcflags=-S",
".",
})
if err != nil {
// e.g. failed to write overlays (rare)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer cleanupInvocation()
escape := html.EscapeString
// Emit the start of the report.
titleHTML := fmt.Sprintf("%s assembly for %s",
escape(snapshot.View().GOARCH()),
escape(symbol))
io.WriteString(w, `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>`+titleHTML+`</title>
<link rel="stylesheet" href="/assets/common.css">
<script src="/assets/common.js"></script>
</head>
<body>
<h1>`+titleHTML+`</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 id='compiling'>Compiling...</p>
<pre>
`)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
// At this point errors must be reported by writing HTML.
// To do this, set "status" return early.
var buf bytes.Buffer
status := "Reload the page to recompile."
defer func() {
// Update the "Compiling..." message.
fmt.Fprintf(&buf, `
</pre>
<script>
document.getElementById('compiling').innerText = %q;
</script>
</body>`, status)
w.Write(buf.Bytes())
}()
// Compile the package.
_, stderr, err, _ := snapshot.View().GoCommandRunner().RunRaw(ctx, *inv)
if err != nil {
status = fmt.Sprintf("compilation failed: %v", err)
return
}
// Write the rest of the report.
content := stderr.String()
// 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.SplitSeq(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 := morestrings.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')
}
}