| // 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 base |
| |
| import ( |
| "bytes" |
| "cmd/internal/notsha256" |
| "cmd/internal/obj" |
| "cmd/internal/src" |
| "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| "strconv" |
| "strings" |
| "sync" |
| ) |
| |
| type writeSyncer interface { |
| io.Writer |
| Sync() error |
| } |
| |
| type hashAndMask struct { |
| // a hash h matches if (h^hash)&mask == 0 |
| hash uint64 |
| mask uint64 |
| name string // base name, or base name + "0", "1", etc. |
| } |
| |
| type HashDebug struct { |
| mu sync.Mutex // for logfile, posTmp, bytesTmp |
| name string // base name of the flag/variable. |
| // what file (if any) receives the yes/no logging? |
| // default is os.Stdout |
| logfile writeSyncer |
| posTmp []src.Pos |
| bytesTmp bytes.Buffer |
| matches []hashAndMask // A hash matches if one of these matches. |
| yes, no bool |
| fileSuffixOnly bool // for Pos hashes, remove the directory prefix. |
| inlineSuffixOnly bool // for Pos hashes, remove all but the most inline position. |
| } |
| |
| // SetFileSuffixOnly controls whether hashing and reporting use the entire |
| // file path name, just the basename. This makes hashing more consistent, |
| // at the expense of being able to certainly locate the file. |
| func (d *HashDebug) SetFileSuffixOnly(b bool) *HashDebug { |
| d.fileSuffixOnly = b |
| return d |
| } |
| |
| // SetInlineSuffixOnly controls whether hashing and reporting use the entire |
| // inline position, or just the most-inline suffix. Compiler debugging tends |
| // to want the whole inlining, debugging user problems (loopvarhash, e.g.) |
| // typically does not need to see the entire inline tree, there is just one |
| // copy of the source code. |
| func (d *HashDebug) SetInlineSuffixOnly(b bool) *HashDebug { |
| d.inlineSuffixOnly = b |
| return d |
| } |
| |
| // The default compiler-debugging HashDebug, for "-d=gossahash=..." |
| var hashDebug *HashDebug |
| |
| var FmaHash *HashDebug // for debugging fused-multiply-add floating point changes |
| var LoopVarHash *HashDebug // for debugging shared/private loop variable changes |
| |
| // DebugHashMatch reports whether debug variable Gossahash |
| // |
| // 1. is empty (returns true; this is a special more-quickly implemented case of 4 below) |
| // |
| // 2. is "y" or "Y" (returns true) |
| // |
| // 3. is "n" or "N" (returns false) |
| // |
| // 4. is a suffix of the sha1 hash of pkgAndName (returns true) |
| // |
| // 5. OR |
| // if the value is in the regular language "[01]+(/[01]+)+" |
| // test the [01]+ substrings after in order returning true |
| // for the first one that suffix-matches. The substrings AFTER |
| // the first slash are numbered 0,1, etc and are named |
| // fmt.Sprintf("%s%d", varname, number) |
| // Clause 5 is not really intended for human use and only |
| // matters for failures that require multiple triggers. |
| // |
| // Otherwise it returns false. |
| // |
| // Unless Flags.Gossahash is empty, when DebugHashMatch returns true the message |
| // |
| // "%s triggered %s\n", varname, pkgAndName |
| // |
| // is printed on the file named in environment variable GSHS_LOGFILE, |
| // or standard out if that is empty. "Varname" is either the name of |
| // the variable or the name of the substring, depending on which matched. |
| // |
| // Typical use: |
| // |
| // 1. you make a change to the compiler, say, adding a new phase |
| // |
| // 2. it is broken in some mystifying way, for example, make.bash builds a broken |
| // compiler that almost works, but crashes compiling a test in run.bash. |
| // |
| // 3. add this guard to the code, which by default leaves it broken, but does not |
| // run the broken new code if Flags.Gossahash is non-empty and non-matching: |
| // |
| // if !base.DebugHashMatch(ir.PkgFuncName(fn)) { |
| // return nil // early exit, do nothing |
| // } |
| // |
| // 4. rebuild w/o the bad code, |
| // GOCOMPILEDEBUG=gossahash=n ./all.bash |
| // to verify that you put the guard in the right place with the right sense of the test. |
| // |
| // 5. use github.com/dr2chase/gossahash to search for the error: |
| // |
| // go install github.com/dr2chase/gossahash@latest |
| // |
| // gossahash -- <the thing that fails> |
| // |
| // for example: GOMAXPROCS=1 gossahash -- ./all.bash |
| // |
| // 6. gossahash should return a single function whose miscompilation |
| // causes the problem, and you can focus on that. |
| func DebugHashMatch(pkgAndName string) bool { |
| return hashDebug.DebugHashMatch(pkgAndName) |
| } |
| |
| // HasDebugHash returns true if Flags.Gossahash is non-empty, which |
| // results in hashDebug being not-nil. I.e., if !HasDebugHash(), |
| // there is no need to create the string for hashing and testing. |
| func HasDebugHash() bool { |
| return hashDebug != nil |
| } |
| |
| func toHashAndMask(s, varname string) hashAndMask { |
| l := len(s) |
| if l > 64 { |
| s = s[l-64:] |
| l = 64 |
| } |
| m := ^(^uint64(0) << l) |
| h, err := strconv.ParseUint(s, 2, 64) |
| if err != nil { |
| Fatalf("Could not parse %s (=%s) as a binary number", varname, s) |
| } |
| |
| return hashAndMask{name: varname, hash: h, mask: m} |
| } |
| |
| // NewHashDebug returns a new hash-debug tester for the |
| // environment variable ev. If ev is not set, it returns |
| // nil, allowing a lightweight check for normal-case behavior. |
| func NewHashDebug(ev, s string, file writeSyncer) *HashDebug { |
| if s == "" { |
| return nil |
| } |
| |
| hd := &HashDebug{name: ev, logfile: file} |
| switch s[0] { |
| case 'y', 'Y': |
| hd.yes = true |
| return hd |
| case 'n', 'N': |
| hd.no = true |
| return hd |
| } |
| ss := strings.Split(s, "/") |
| hd.matches = append(hd.matches, toHashAndMask(ss[0], ev)) |
| // hash searches may use additional EVs with 0, 1, 2, ... suffixes. |
| for i := 1; i < len(ss); i++ { |
| evi := fmt.Sprintf("%s%d", ev, i-1) // convention is extras begin indexing at zero |
| hd.matches = append(hd.matches, toHashAndMask(ss[i], evi)) |
| } |
| return hd |
| |
| } |
| |
| func hashOf(pkgAndName string, param uint64) uint64 { |
| return hashOfBytes([]byte(pkgAndName), param) |
| } |
| |
| func hashOfBytes(sbytes []byte, param uint64) uint64 { |
| hbytes := notsha256.Sum256(sbytes) |
| hash := uint64(hbytes[7])<<56 + uint64(hbytes[6])<<48 + |
| uint64(hbytes[5])<<40 + uint64(hbytes[4])<<32 + |
| uint64(hbytes[3])<<24 + uint64(hbytes[2])<<16 + |
| uint64(hbytes[1])<<8 + uint64(hbytes[0]) |
| |
| if param != 0 { |
| // Because param is probably a line number, probably near zero, |
| // hash it up a little bit, but even so only the lower-order bits |
| // likely matter because search focuses on those. |
| p0 := param + uint64(hbytes[9]) + uint64(hbytes[10])<<8 + |
| uint64(hbytes[11])<<16 + uint64(hbytes[12])<<24 |
| |
| p1 := param + uint64(hbytes[13]) + uint64(hbytes[14])<<8 + |
| uint64(hbytes[15])<<16 + uint64(hbytes[16])<<24 |
| |
| param += p0 * p1 |
| param ^= param>>17 ^ param<<47 |
| } |
| |
| return hash ^ param |
| } |
| |
| // DebugHashMatch returns true if either the variable used to create d is |
| // unset, or if its value is y, or if it is a suffix of the base-two |
| // representation of the hash of pkgAndName. If the variable is not nil, |
| // then a true result is accompanied by stylized output to d.logfile, which |
| // is used for automated bug search. |
| func (d *HashDebug) DebugHashMatch(pkgAndName string) bool { |
| return d.DebugHashMatchParam(pkgAndName, 0) |
| } |
| |
| // DebugHashMatchParam returns true if either the variable used to create d is |
| // unset, or if its value is y, or if it is a suffix of the base-two |
| // representation of the hash of pkgAndName and param. If the variable is not |
| // nil, then a true result is accompanied by stylized output to d.logfile, |
| // which is used for automated bug search. |
| func (d *HashDebug) DebugHashMatchParam(pkgAndName string, param uint64) bool { |
| if d == nil { |
| return true |
| } |
| if d.no { |
| return false |
| } |
| |
| if d.yes { |
| d.logDebugHashMatch(d.name, pkgAndName, "y", param) |
| return true |
| } |
| |
| hash := hashOf(pkgAndName, param) |
| |
| for _, m := range d.matches { |
| if (m.hash^hash)&m.mask == 0 { |
| hstr := "" |
| if hash == 0 { |
| hstr = "0" |
| } else { |
| for ; hash != 0; hash = hash >> 1 { |
| hstr = string('0'+byte(hash&1)) + hstr |
| } |
| } |
| d.logDebugHashMatch(m.name, pkgAndName, hstr, param) |
| return true |
| } |
| } |
| return false |
| } |
| |
| // DebugHashMatchPos is similar to DebugHashMatchParam, but for hash computation |
| // it uses the source position including all inlining information instead of |
| // package name and path. The output trigger string is prefixed with "POS=" so |
| // that tools processing the output can reliably tell the difference. The mutex |
| // locking is also more frequent and more granular. |
| // Note that the default answer for no environment variable (d == nil) |
| // is "yes", do the thing. |
| func (d *HashDebug) DebugHashMatchPos(ctxt *obj.Link, pos src.XPos) bool { |
| if d == nil { |
| return true |
| } |
| if d.no { |
| return false |
| } |
| // Written this way to make inlining likely. |
| return d.debugHashMatchPos(ctxt, pos) |
| } |
| |
| func (d *HashDebug) debugHashMatchPos(ctxt *obj.Link, pos src.XPos) bool { |
| d.mu.Lock() |
| defer d.mu.Unlock() |
| |
| b := d.bytesForPos(ctxt, pos) |
| |
| if d.yes { |
| d.logDebugHashMatchLocked(d.name, string(b), "y", 0) |
| return true |
| } |
| |
| hash := hashOfBytes(b, 0) |
| |
| for _, m := range d.matches { |
| if (m.hash^hash)&m.mask == 0 { |
| hstr := "" |
| if hash == 0 { |
| hstr = "0" |
| } else { |
| for ; hash != 0; hash = hash >> 1 { |
| hstr = string('0'+byte(hash&1)) + hstr |
| } |
| } |
| d.logDebugHashMatchLocked(m.name, "POS="+string(b), hstr, 0) |
| return true |
| } |
| } |
| return false |
| } |
| |
| // bytesForPos renders a position, including inlining, into d.bytesTmp |
| // and returns the byte array. d.mu must be locked. |
| func (d *HashDebug) bytesForPos(ctxt *obj.Link, pos src.XPos) []byte { |
| d.posTmp = ctxt.AllPos(pos, d.posTmp) |
| // Reverse posTmp to put outermost first. |
| b := &d.bytesTmp |
| b.Reset() |
| start := len(d.posTmp) - 1 |
| if d.inlineSuffixOnly { |
| start = 0 |
| } |
| for i := start; i >= 0; i-- { |
| p := &d.posTmp[i] |
| f := p.Filename() |
| if d.fileSuffixOnly { |
| f = filepath.Base(f) |
| } |
| fmt.Fprintf(b, "%s:%d:%d", f, p.Line(), p.Col()) |
| if i != 0 { |
| b.WriteByte(';') |
| } |
| } |
| return b.Bytes() |
| } |
| |
| func (d *HashDebug) logDebugHashMatch(varname, name, hstr string, param uint64) { |
| d.mu.Lock() |
| defer d.mu.Unlock() |
| d.logDebugHashMatchLocked(varname, name, hstr, param) |
| } |
| |
| func (d *HashDebug) logDebugHashMatchLocked(varname, name, hstr string, param uint64) { |
| file := d.logfile |
| if file == nil { |
| if tmpfile := os.Getenv("GSHS_LOGFILE"); tmpfile != "" { |
| var err error |
| file, err = os.OpenFile(tmpfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) |
| if err != nil { |
| Fatalf("could not open hash-testing logfile %s", tmpfile) |
| return |
| } |
| } |
| if file == nil { |
| file = os.Stdout |
| } |
| d.logfile = file |
| } |
| if len(hstr) > 24 { |
| hstr = hstr[len(hstr)-24:] |
| } |
| // External tools depend on this string |
| if param == 0 { |
| fmt.Fprintf(file, "%s triggered %s %s\n", varname, name, hstr) |
| } else { |
| fmt.Fprintf(file, "%s triggered %s:%d %s\n", varname, name, param, hstr) |
| } |
| file.Sync() |
| } |