| // 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 | |
| } |