diff options
Diffstat (limited to 'src/runtime/align_test.go')
-rw-r--r-- | src/runtime/align_test.go | 200 |
1 files changed, 200 insertions, 0 deletions
diff --git a/src/runtime/align_test.go b/src/runtime/align_test.go new file mode 100644 index 0000000..5f225d6 --- /dev/null +++ b/src/runtime/align_test.go @@ -0,0 +1,200 @@ +// 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 runtime_test + +import ( + "go/ast" + "go/build" + "go/importer" + "go/parser" + "go/printer" + "go/token" + "go/types" + "internal/testenv" + "os" + "regexp" + "runtime" + "strings" + "testing" +) + +// Check that 64-bit fields on which we apply atomic operations +// are aligned to 8 bytes. This can be a problem on 32-bit systems. +func TestAtomicAlignment(t *testing.T) { + testenv.MustHaveGoBuild(t) // go command needed to resolve std .a files for importer.Default(). + + // Read the code making the tables above, to see which fields and + // variables we are currently checking. + checked := map[string]bool{} + x, err := os.ReadFile("./align_runtime_test.go") + if err != nil { + t.Fatalf("read failed: %v", err) + } + fieldDesc := map[int]string{} + r := regexp.MustCompile(`unsafe[.]Offsetof[(](\w+){}[.](\w+)[)]`) + matches := r.FindAllStringSubmatch(string(x), -1) + for i, v := range matches { + checked["field runtime."+v[1]+"."+v[2]] = true + fieldDesc[i] = v[1] + "." + v[2] + } + varDesc := map[int]string{} + r = regexp.MustCompile(`unsafe[.]Pointer[(]&(\w+)[)]`) + matches = r.FindAllStringSubmatch(string(x), -1) + for i, v := range matches { + checked["var "+v[1]] = true + varDesc[i] = v[1] + } + + // Check all of our alignemnts. This is the actual core of the test. + for i, d := range runtime.AtomicFields { + if d%8 != 0 { + t.Errorf("field alignment of %s failed: offset is %d", fieldDesc[i], d) + } + } + for i, p := range runtime.AtomicVariables { + if uintptr(p)%8 != 0 { + t.Errorf("variable alignment of %s failed: address is %x", varDesc[i], p) + } + } + + // The code above is the actual test. The code below attempts to check + // that the tables used by the code above are exhaustive. + + // Parse the whole runtime package, checking that arguments of + // appropriate atomic operations are in the list above. + fset := token.NewFileSet() + m, err := parser.ParseDir(fset, ".", nil, 0) + if err != nil { + t.Fatalf("parsing runtime failed: %v", err) + } + pkg := m["runtime"] // Note: ignore runtime_test and main packages + + // Filter files by those for the current architecture/os being tested. + fileMap := map[string]bool{} + for _, f := range buildableFiles(t, ".") { + fileMap[f] = true + } + var files []*ast.File + for fname, f := range pkg.Files { + if fileMap[fname] { + files = append(files, f) + } + } + + // Call go/types to analyze the runtime package. + var info types.Info + info.Types = map[ast.Expr]types.TypeAndValue{} + conf := types.Config{Importer: importer.Default()} + _, err = conf.Check("runtime", fset, files, &info) + if err != nil { + t.Fatalf("typechecking runtime failed: %v", err) + } + + // Analyze all atomic.*64 callsites. + v := Visitor{t: t, fset: fset, types: info.Types, checked: checked} + ast.Walk(&v, pkg) +} + +type Visitor struct { + fset *token.FileSet + types map[ast.Expr]types.TypeAndValue + checked map[string]bool + t *testing.T +} + +func (v *Visitor) Visit(n ast.Node) ast.Visitor { + c, ok := n.(*ast.CallExpr) + if !ok { + return v + } + f, ok := c.Fun.(*ast.SelectorExpr) + if !ok { + return v + } + p, ok := f.X.(*ast.Ident) + if !ok { + return v + } + if p.Name != "atomic" { + return v + } + if !strings.HasSuffix(f.Sel.Name, "64") { + return v + } + + a := c.Args[0] + + // This is a call to atomic.XXX64(a, ...). Make sure a is aligned to 8 bytes. + // XXX = one of Load, Store, Cas, etc. + // The arg we care about the alignment of is always the first one. + + if u, ok := a.(*ast.UnaryExpr); ok && u.Op == token.AND { + v.checkAddr(u.X) + return v + } + + // Other cases there's nothing we can check. Assume we're ok. + v.t.Logf("unchecked atomic operation %s %v", v.fset.Position(n.Pos()), v.print(n)) + + return v +} + +// checkAddr checks to make sure n is a properly aligned address for a 64-bit atomic operation. +func (v *Visitor) checkAddr(n ast.Node) { + switch n := n.(type) { + case *ast.IndexExpr: + // Alignment of an array element is the same as the whole array. + v.checkAddr(n.X) + return + case *ast.Ident: + key := "var " + v.print(n) + if !v.checked[key] { + v.t.Errorf("unchecked variable %s %s", v.fset.Position(n.Pos()), key) + } + return + case *ast.SelectorExpr: + t := v.types[n.X].Type + if t == nil { + // Not sure what is happening here, go/types fails to + // type the selector arg on some platforms. + return + } + if p, ok := t.(*types.Pointer); ok { + // Note: we assume here that the pointer p in p.foo is properly + // aligned. We just check that foo is at a properly aligned offset. + t = p.Elem() + } else { + v.checkAddr(n.X) + } + if t.Underlying() == t { + v.t.Errorf("analysis can't handle unnamed type %s %v", v.fset.Position(n.Pos()), t) + } + key := "field " + t.String() + "." + n.Sel.Name + if !v.checked[key] { + v.t.Errorf("unchecked field %s %s", v.fset.Position(n.Pos()), key) + } + default: + v.t.Errorf("unchecked atomic address %s %v", v.fset.Position(n.Pos()), v.print(n)) + + } +} + +func (v *Visitor) print(n ast.Node) string { + var b strings.Builder + printer.Fprint(&b, v.fset, n) + return b.String() +} + +// buildableFiles returns the list of files in the given directory +// that are actually used for the build, given GOOS/GOARCH restrictions. +func buildableFiles(t *testing.T, dir string) []string { + ctxt := build.Default + ctxt.CgoEnabled = true + pkg, err := ctxt.ImportDir(dir, 0) + if err != nil { + t.Fatalf("can't find buildable files: %v", err) + } + return pkg.GoFiles +} |