// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "bytes" "encoding/hex" "testing" "sourcedock.dev/petrbalvin/gasm-devkit/parser" ) // The expected bytes are pinned from `go tool asm` output (Go 1.27, amd64, // verified with go tool objdump): the stack-split guard classes, the morestack // block and the auto-NOSPLIT leaf behaviour. func TestStackGuardBytes(t *testing.T) { for _, tt := range []struct { name string src string want string }{ {"leafsmall", "TEXT \u00b7leafsmall(SB), $16-0\n\tRET\n", "554889e54883ec104883c4105dc3"}, {"leafmed", "TEXT \u00b7leafmed(SB), $256-0\n\tRET\n", "644c8b3425000000004c8da42478ffffff4d3b66107614554889e54881ec000100004881c4000100005dc3e800000000ebce"}, {"leafbig", "TEXT \u00b7leafbig(SB), $8192-0\n\tRET\n", "644c8b3425000000004989e44981ec881f0000721a4d3b66107614554889e54881ec002000004881c4002000005dc3e800000000ebca"}, {"callsmall", "TEXT \u00b7callsmall(SB), $16-0\n\tCALL \u00b7other(SB)\n\tRET\nTEXT \u00b7other(SB), NOSPLIT, $0\n\tRET\n", "644c8b342500000000493b66107613554889e54883ec10e8000000004883c4105dc3e800000000ebd7"}, {"nosplit", "TEXT \u00b7nosplit(SB), NOSPLIT, $16-0\n\tRET\n", "554889e54883ec104883c4105dc3"}, } { f, errs := parser.Parse("g_amd64.s", tt.src) if len(errs) > 0 { t.Fatalf("%s: parse: %v", tt.name, errs) } img, err := AssembleFile(f) if err != nil { t.Fatalf("%s: assemble: %v", tt.name, err) } fn := img.Funcs[0] // The toolchain's object leaves every relocation field zero for the // linker, while the gasm image resolves file-internal references, so // the comparison masks the patch sites the way verify's ground truth // does. code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...) for _, r := range fn.Relocs { for j := r.Off; j < r.Off+4 && j < len(code); j++ { code[j] = 0 } } got := hex.EncodeToString(code) if got != tt.want { t.Errorf("%s:\n got %s\n want %s", tt.name, got, tt.want) } } } // TestStackGuardRelocs checks the guard's patch sites: the TLS slot and the // morestack call. func TestStackGuardRelocs(t *testing.T) { f, errs := parser.Parse("g_amd64.s", "TEXT \u00b7f(SB), $256-0\n\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFile(f) if err != nil { t.Fatalf("assemble: %v", err) } relocs := img.Funcs[0].Relocs if len(relocs) != 2 { t.Fatalf("relocs = %d, want 2", len(relocs)) } tls, call := relocs[0], relocs[1] if tls.Kind != RelTLSLE || tls.Off != 5 || tls.Name != "" || tls.External { t.Errorf("tls reloc = %+v, want RelTLSLE at 5 with no symbol", tls) } if call.Kind != RelCall || call.Name != "runtime\u00b7morestack_noctxt" || !call.External { t.Errorf("call reloc = %+v, want RelCall to runtime.morestack_noctxt", call) } } // TestStackGuardGOObj emissions succeed with the guard's TLS and builtin // references in play. func TestStackGuardGOObj(t *testing.T) { f, errs := parser.Parse("g_amd64.s", "TEXT \u00b7f(SB), $256-0\n\tCALL \u00b7helper(SB)\n\tRET\nTEXT \u00b7helper(SB), NOSPLIT, $0\n\tRET\n") if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFile(f) if err != nil { t.Fatalf("assemble: %v", err) } obj, err := img.GOObject("testpkg", "g_amd64.s") if err != nil { t.Fatalf("GOObject: %v", err) } if !bytes.Contains(obj, []byte("go120ld")) { t.Fatal("object lacks the GOOBJ magic") } }