diff options
Diffstat (limited to 'src/cmd/compile/internal/amd64/versions_test.go')
-rw-r--r-- | src/cmd/compile/internal/amd64/versions_test.go | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/src/cmd/compile/internal/amd64/versions_test.go b/src/cmd/compile/internal/amd64/versions_test.go new file mode 100644 index 0000000..fc0046a --- /dev/null +++ b/src/cmd/compile/internal/amd64/versions_test.go @@ -0,0 +1,433 @@ +// Copyright 2021 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. + +// When using GOEXPERIMENT=boringcrypto, the test program links in the boringcrypto syso, +// which does not respect GOAMD64, so we skip the test if boringcrypto is enabled. +//go:build !boringcrypto + +package amd64_test + +import ( + "bufio" + "debug/elf" + "debug/macho" + "errors" + "fmt" + "go/build" + "internal/testenv" + "io" + "math" + "math/bits" + "os" + "os/exec" + "regexp" + "runtime" + "strconv" + "strings" + "testing" +) + +// Test to make sure that when building for GOAMD64=v1, we don't +// use any >v1 instructions. +func TestGoAMD64v1(t *testing.T) { + if runtime.GOARCH != "amd64" { + t.Skip("amd64-only test") + } + if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { + t.Skip("test only works on elf or macho platforms") + } + for _, tag := range build.Default.ToolTags { + if tag == "amd64.v2" { + t.Skip("compiling for GOAMD64=v2 or higher") + } + } + if os.Getenv("TESTGOAMD64V1") != "" { + t.Skip("recursive call") + } + + // Make a binary which will be a modified version of the + // currently running binary. + dst, err := os.CreateTemp("", "TestGoAMD64v1") + if err != nil { + t.Fatalf("failed to create temp file: %v", err) + } + defer os.Remove(dst.Name()) + dst.Chmod(0500) // make executable + + // Clobber all the non-v1 opcodes. + opcodes := map[string]bool{} + var features []string + for feature, opcodeList := range featureToOpcodes { + if runtimeFeatures[feature] { + features = append(features, fmt.Sprintf("cpu.%s=off", feature)) + } + for _, op := range opcodeList { + opcodes[op] = true + } + } + clobber(t, os.Args[0], dst, opcodes) + if err = dst.Close(); err != nil { + t.Fatalf("can't close binary: %v", err) + } + + // Run the resulting binary. + cmd := testenv.Command(t, dst.Name()) + testenv.CleanCmdEnv(cmd) + cmd.Env = append(cmd.Env, "TESTGOAMD64V1=yes") + cmd.Env = append(cmd.Env, fmt.Sprintf("GODEBUG=%s", strings.Join(features, ","))) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("couldn't execute test: %s", err) + } + // Expect to see output of the form "PASS\n", unless the test binary + // was compiled for coverage (in which case there will be an extra line). + success := false + lines := strings.Split(string(out), "\n") + if len(lines) == 2 { + success = lines[0] == "PASS" && lines[1] == "" + } else if len(lines) == 3 { + success = lines[0] == "PASS" && + strings.HasPrefix(lines[1], "coverage") && lines[2] == "" + } + if !success { + t.Fatalf("test reported error: %s lines=%+v", string(out), lines) + } +} + +// Clobber copies the binary src to dst, replacing all the instructions in opcodes with +// faulting instructions. +func clobber(t *testing.T, src string, dst *os.File, opcodes map[string]bool) { + // Run objdump to get disassembly. + var re *regexp.Regexp + var disasm io.Reader + if false { + // TODO: go tool objdump doesn't disassemble the bmi1 instructions + // in question correctly. See issue 48584. + cmd := testenv.Command(t, "go", "tool", "objdump", src) + var err error + disasm, err = cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := cmd.Wait(); err != nil { + t.Error(err) + } + }) + re = regexp.MustCompile(`^[^:]*:[-\d]+\s+0x([\da-f]+)\s+([\da-f]+)\s+([A-Z]+)`) + } else { + // TODO: we're depending on platform-native objdump here. Hence the Skipf + // below if it doesn't run for some reason. + cmd := testenv.Command(t, "objdump", "-d", src) + var err error + disasm, err = cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + if errors.Is(err, exec.ErrNotFound) { + t.Skipf("can't run test due to missing objdump: %s", err) + } + t.Fatal(err) + } + t.Cleanup(func() { + if err := cmd.Wait(); err != nil { + t.Error(err) + } + }) + re = regexp.MustCompile(`^\s*([\da-f]+):\s*((?:[\da-f][\da-f] )+)\s*([a-z\d]+)`) + } + + // Find all the instruction addresses we need to edit. + virtualEdits := map[uint64]bool{} + scanner := bufio.NewScanner(disasm) + for scanner.Scan() { + line := scanner.Text() + parts := re.FindStringSubmatch(line) + if len(parts) == 0 { + continue + } + addr, err := strconv.ParseUint(parts[1], 16, 64) + if err != nil { + continue // not a hex address + } + opcode := strings.ToLower(parts[3]) + if !opcodes[opcode] { + continue + } + t.Logf("clobbering instruction %s", line) + n := (len(parts[2]) - strings.Count(parts[2], " ")) / 2 // number of bytes in instruction encoding + for i := 0; i < n; i++ { + // Only really need to make the first byte faulting, but might + // as well make all the bytes faulting. + virtualEdits[addr+uint64(i)] = true + } + } + + // Figure out where in the binary the edits must be done. + physicalEdits := map[uint64]bool{} + if e, err := elf.Open(src); err == nil { + for _, sec := range e.Sections { + vaddr := sec.Addr + paddr := sec.Offset + size := sec.Size + for a := range virtualEdits { + if a >= vaddr && a < vaddr+size { + physicalEdits[paddr+(a-vaddr)] = true + } + } + } + } else if m, err2 := macho.Open(src); err2 == nil { + for _, sec := range m.Sections { + vaddr := sec.Addr + paddr := uint64(sec.Offset) + size := sec.Size + for a := range virtualEdits { + if a >= vaddr && a < vaddr+size { + physicalEdits[paddr+(a-vaddr)] = true + } + } + } + } else { + t.Log(err) + t.Log(err2) + t.Fatal("executable format not elf or macho") + } + if len(virtualEdits) != len(physicalEdits) { + t.Fatal("couldn't find an instruction in text sections") + } + + // Copy source to destination, making edits along the way. + f, err := os.Open(src) + if err != nil { + t.Fatal(err) + } + r := bufio.NewReader(f) + w := bufio.NewWriter(dst) + a := uint64(0) + done := 0 + for { + b, err := r.ReadByte() + if err == io.EOF { + break + } + if err != nil { + t.Fatal("can't read") + } + if physicalEdits[a] { + b = 0xcc // INT3 opcode + done++ + } + err = w.WriteByte(b) + if err != nil { + t.Fatal("can't write") + } + a++ + } + if done != len(physicalEdits) { + t.Fatal("physical edits remaining") + } + w.Flush() + f.Close() +} + +func setOf(keys ...string) map[string]bool { + m := make(map[string]bool, len(keys)) + for _, key := range keys { + m[key] = true + } + return m +} + +var runtimeFeatures = setOf( + "adx", "aes", "avx", "avx2", "bmi1", "bmi2", "erms", "fma", + "pclmulqdq", "popcnt", "rdtscp", "sse3", "sse41", "sse42", "ssse3", +) + +var featureToOpcodes = map[string][]string{ + // Note: we include *q, *l, and plain opcodes here. + // go tool objdump doesn't include a [QL] on popcnt instructions, until CL 351889 + // native objdump doesn't include [QL] on linux. + "popcnt": {"popcntq", "popcntl", "popcnt"}, + "bmi1": { + "andnq", "andnl", "andn", + "blsiq", "blsil", "blsi", + "blsmskq", "blsmskl", "blsmsk", + "blsrq", "blsrl", "blsr", + "tzcntq", "tzcntl", "tzcnt", + }, + "bmi2": { + "sarxq", "sarxl", "sarx", + "shlxq", "shlxl", "shlx", + "shrxq", "shrxl", "shrx", + }, + "sse41": { + "roundsd", + "pinsrq", "pinsrl", "pinsrd", "pinsrb", "pinsr", + "pextrq", "pextrl", "pextrd", "pextrb", "pextr", + "pminsb", "pminsd", "pminuw", "pminud", // Note: ub and sw are ok. + "pmaxsb", "pmaxsd", "pmaxuw", "pmaxud", + "pmovzxbw", "pmovzxbd", "pmovzxbq", "pmovzxwd", "pmovzxwq", "pmovzxdq", + "pmovsxbw", "pmovsxbd", "pmovsxbq", "pmovsxwd", "pmovsxwq", "pmovsxdq", + "pblendvb", + }, + "fma": {"vfmadd231sd"}, + "movbe": {"movbeqq", "movbeq", "movbell", "movbel", "movbe"}, + "lzcnt": {"lzcntq", "lzcntl", "lzcnt"}, +} + +// Test to use POPCNT instruction, if available +func TestPopCnt(t *testing.T) { + for _, tt := range []struct { + x uint64 + want int + }{ + {0b00001111, 4}, + {0b00001110, 3}, + {0b00001100, 2}, + {0b00000000, 0}, + } { + if got := bits.OnesCount64(tt.x); got != tt.want { + t.Errorf("OnesCount64(%#x) = %d, want %d", tt.x, got, tt.want) + } + if got := bits.OnesCount32(uint32(tt.x)); got != tt.want { + t.Errorf("OnesCount32(%#x) = %d, want %d", tt.x, got, tt.want) + } + } +} + +// Test to use ANDN, if available +func TestAndNot(t *testing.T) { + for _, tt := range []struct { + x, y, want uint64 + }{ + {0b00001111, 0b00000011, 0b1100}, + {0b00001111, 0b00001100, 0b0011}, + {0b00000000, 0b00000000, 0b0000}, + } { + if got := tt.x &^ tt.y; got != tt.want { + t.Errorf("%#x &^ %#x = %#x, want %#x", tt.x, tt.y, got, tt.want) + } + if got := uint32(tt.x) &^ uint32(tt.y); got != uint32(tt.want) { + t.Errorf("%#x &^ %#x = %#x, want %#x", tt.x, tt.y, got, tt.want) + } + } +} + +// Test to use BLSI, if available +func TestBLSI(t *testing.T) { + for _, tt := range []struct { + x, want uint64 + }{ + {0b00001111, 0b001}, + {0b00001110, 0b010}, + {0b00001100, 0b100}, + {0b11000110, 0b010}, + {0b00000000, 0b000}, + } { + if got := tt.x & -tt.x; got != tt.want { + t.Errorf("%#x & (-%#x) = %#x, want %#x", tt.x, tt.x, got, tt.want) + } + if got := uint32(tt.x) & -uint32(tt.x); got != uint32(tt.want) { + t.Errorf("%#x & (-%#x) = %#x, want %#x", tt.x, tt.x, got, tt.want) + } + } +} + +// Test to use BLSMSK, if available +func TestBLSMSK(t *testing.T) { + for _, tt := range []struct { + x, want uint64 + }{ + {0b00001111, 0b001}, + {0b00001110, 0b011}, + {0b00001100, 0b111}, + {0b11000110, 0b011}, + {0b00000000, 1<<64 - 1}, + } { + if got := tt.x ^ (tt.x - 1); got != tt.want { + t.Errorf("%#x ^ (%#x-1) = %#x, want %#x", tt.x, tt.x, got, tt.want) + } + if got := uint32(tt.x) ^ (uint32(tt.x) - 1); got != uint32(tt.want) { + t.Errorf("%#x ^ (%#x-1) = %#x, want %#x", tt.x, tt.x, got, uint32(tt.want)) + } + } +} + +// Test to use BLSR, if available +func TestBLSR(t *testing.T) { + for _, tt := range []struct { + x, want uint64 + }{ + {0b00001111, 0b00001110}, + {0b00001110, 0b00001100}, + {0b00001100, 0b00001000}, + {0b11000110, 0b11000100}, + {0b00000000, 0b00000000}, + } { + if got := tt.x & (tt.x - 1); got != tt.want { + t.Errorf("%#x & (%#x-1) = %#x, want %#x", tt.x, tt.x, got, tt.want) + } + if got := uint32(tt.x) & (uint32(tt.x) - 1); got != uint32(tt.want) { + t.Errorf("%#x & (%#x-1) = %#x, want %#x", tt.x, tt.x, got, tt.want) + } + } +} + +func TestTrailingZeros(t *testing.T) { + for _, tt := range []struct { + x uint64 + want int + }{ + {0b00001111, 0}, + {0b00001110, 1}, + {0b00001100, 2}, + {0b00001000, 3}, + {0b00000000, 64}, + } { + if got := bits.TrailingZeros64(tt.x); got != tt.want { + t.Errorf("TrailingZeros64(%#x) = %d, want %d", tt.x, got, tt.want) + } + want := tt.want + if want == 64 { + want = 32 + } + if got := bits.TrailingZeros32(uint32(tt.x)); got != want { + t.Errorf("TrailingZeros64(%#x) = %d, want %d", tt.x, got, want) + } + } +} + +func TestRound(t *testing.T) { + for _, tt := range []struct { + x, want float64 + }{ + {1.4, 1}, + {1.5, 2}, + {1.6, 2}, + {2.4, 2}, + {2.5, 2}, + {2.6, 3}, + } { + if got := math.RoundToEven(tt.x); got != tt.want { + t.Errorf("RoundToEven(%f) = %f, want %f", tt.x, got, tt.want) + } + } +} + +func TestFMA(t *testing.T) { + for _, tt := range []struct { + x, y, z, want float64 + }{ + {2, 3, 4, 10}, + {3, 4, 5, 17}, + } { + if got := math.FMA(tt.x, tt.y, tt.z); got != tt.want { + t.Errorf("FMA(%f,%f,%f) = %f, want %f", tt.x, tt.y, tt.z, got, tt.want) + } + } +} |