feat(asm): emit the amd64 stack-split guard and morestack block
This commit is contained in:
+283
-8
@@ -51,6 +51,7 @@ type sbPatch struct {
|
||||
after int
|
||||
name string
|
||||
addend int64
|
||||
kind RelocKind
|
||||
}
|
||||
|
||||
// spadjStep is one stack-adjustment boundary within a function: Value is the
|
||||
@@ -75,13 +76,18 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
||||
return name
|
||||
}
|
||||
|
||||
// Layout: iterate jump sizes to a fixed point.
|
||||
// Layout: iterate jump sizes to a fixed point. The stack-split guard
|
||||
// prefix and the trailing morestack block participate in the iteration:
|
||||
// their conditional branches relax from rel8 to rel32 when the body
|
||||
// outgrows the short form.
|
||||
long := make([]bool, len(t.Body))
|
||||
sizes := make([]int, len(t.Body))
|
||||
offsets := map[string]int{}
|
||||
pcs := make([]int, len(t.Body))
|
||||
var guardJBlong, guardJBElong, moreJMPlong bool
|
||||
for {
|
||||
pos := len(fi.prologue)
|
||||
guard := fi.guardLen(guardJBlong, guardJBElong)
|
||||
pos := guard + len(fi.prologue)
|
||||
for i, stmt := range t.Body {
|
||||
switch s := stmt.(type) {
|
||||
case *ast.Label:
|
||||
@@ -96,6 +102,7 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
||||
pos += sz
|
||||
}
|
||||
}
|
||||
bodyLen := pos - (guard + len(fi.prologue))
|
||||
// Expand any short jump whose displacement no longer fits rel8.
|
||||
changed := false
|
||||
for i, stmt := range t.Body {
|
||||
@@ -121,25 +128,75 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
// The guard's conditional branches target the morestack block, which
|
||||
// starts right after the body: the JBE measures from the end of the
|
||||
// guard, so its displacement is the prologue plus the body.
|
||||
if !guardJBElong && !fits8(int64(len(fi.prologue)+bodyLen)) {
|
||||
guardJBElong = true
|
||||
changed = true
|
||||
}
|
||||
if fi.splitClass == 2 && !guardJBlong {
|
||||
// The underflow JB sits before the CMPQ; its displacement spans
|
||||
// the rest of the guard plus the prologue and the body.
|
||||
jbLen := 2
|
||||
if guardJBlong {
|
||||
jbLen = 6
|
||||
}
|
||||
rest := fi.guardLen(guardJBlong, guardJBElong) - (9 + 3 + 7 + jbLen)
|
||||
if !fits8(int64(rest + len(fi.prologue) + bodyLen)) {
|
||||
guardJBlong = true
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
// The morestack JMP returns to the function start, so its
|
||||
// displacement is the negated distance from its own end.
|
||||
if !moreJMPlong {
|
||||
jmpLen := 2
|
||||
if moreJMPlong {
|
||||
jmpLen = 5
|
||||
}
|
||||
if !fits8(-int64(guard + len(fi.prologue) + bodyLen + 5 + jmpLen)) {
|
||||
moreJMPlong = true
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: emit.
|
||||
out := append([]byte(nil), fi.prologue...)
|
||||
// Pass 2: emit. The guard comes first, then the prologue, the body and
|
||||
// the morestack block.
|
||||
guardLen := fi.guardLen(guardJBlong, guardJBElong)
|
||||
bodyLen := 0
|
||||
{
|
||||
pos := guardLen + len(fi.prologue)
|
||||
for i, stmt := range t.Body {
|
||||
if _, ok := stmt.(*ast.Instr); ok {
|
||||
pos += sizes[i]
|
||||
}
|
||||
}
|
||||
bodyLen = pos - (guardLen + len(fi.prologue))
|
||||
}
|
||||
var out []byte
|
||||
var patches []sbPatch
|
||||
if fi.needSplit {
|
||||
guard, tlsPatch := buildGuard(fi, int32(len(fi.prologue)+bodyLen), int32(fi.guardLen(guardJBlong, guardJBElong)-(9+3+7+2)+len(fi.prologue)+bodyLen))
|
||||
out = append(out, guard...)
|
||||
patches = append(patches, tlsPatch)
|
||||
}
|
||||
out = append(out, fi.prologue...)
|
||||
var steps []spadjStep
|
||||
var lines []LineEntry
|
||||
if fi.useFP {
|
||||
// PUSHQ BP saves the return-address-relative base (+8); the MOVQ
|
||||
// changes nothing; SUBQ $size, SP completes the frame.
|
||||
steps = append(steps,
|
||||
spadjStep{1, 8},
|
||||
spadjStep{len(fi.prologue), 8 + fi.size},
|
||||
spadjStep{guardLen + 1, 8},
|
||||
spadjStep{guardLen + len(fi.prologue), 8 + fi.size},
|
||||
)
|
||||
}
|
||||
pos := len(fi.prologue)
|
||||
pos := guardLen + len(fi.prologue)
|
||||
for i, stmt := range t.Body {
|
||||
s, ok := stmt.(*ast.Instr)
|
||||
if !ok {
|
||||
@@ -161,11 +218,32 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
||||
if len(code) != sizes[i] {
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i])
|
||||
}
|
||||
if strings.ToUpper(s.Mnemonic.Text) == "CALL" {
|
||||
for k := range ps {
|
||||
ps[k].kind = RelCall
|
||||
}
|
||||
}
|
||||
patches = append(patches, ps...)
|
||||
lines = append(lines, LineEntry{Offset: pos, Line: s.Pos().Line})
|
||||
out = append(out, code...)
|
||||
pos += len(code)
|
||||
}
|
||||
if fi.needSplit {
|
||||
// The morestack block: CALL runtime.morestack_noctxt, then a JMP
|
||||
// back to the function entry.
|
||||
jmpLen := 2
|
||||
if moreJMPlong {
|
||||
jmpLen = 5
|
||||
}
|
||||
jmpDisp := -int64(pos + 5 + jmpLen)
|
||||
suffix, callPatch := buildMoreStack(int32(jmpDisp))
|
||||
callPatch.off += pos
|
||||
callPatch.after = pos + 5
|
||||
patches = append(patches, callPatch)
|
||||
out = append(out, suffix...)
|
||||
pos += len(suffix)
|
||||
}
|
||||
_ = pos
|
||||
return out, patches, offsets, steps, lines, nil
|
||||
}
|
||||
|
||||
@@ -229,10 +307,31 @@ type frameInfo struct {
|
||||
spAdjust int64 // x-N(SP) becomes (spAdjust - N)(SP)
|
||||
prologue []byte
|
||||
epilogue []byte
|
||||
|
||||
// Stack-split guard state (matching the toolchain's stacksplit): needSplit
|
||||
// is false for NOSPLIT functions and for leaf functions whose frame is
|
||||
// below StackSmall, which the toolchain auto-marks NOSPLIT.
|
||||
needSplit bool
|
||||
splitClass int // 0: <=StackSmall, 1: <=StackBig, 2: >StackBig
|
||||
framesize int // the size the guard checks: frame+8 for framed functions
|
||||
}
|
||||
|
||||
// Stack-frame size classes from runtime/stack.go.
|
||||
const (
|
||||
stackSmall = 128
|
||||
stackBig = 4096
|
||||
)
|
||||
|
||||
// sbPatch gains a kind so the emitters can tell CALL and TLS patches from
|
||||
// plain PC-relative displacements.
|
||||
|
||||
// computeFrame derives the frame layout, matching the Go assembler's default
|
||||
// (a frame pointer is used whenever the function has a non-zero frame).
|
||||
// (a frame pointer is used whenever the function has a non-zero frame). It
|
||||
// also decides whether the function needs the stack-split guard, mirroring
|
||||
// obj6: a NOSPLIT function never splits, and a leaf function whose frame is
|
||||
// below StackSmall is auto-marked NOSPLIT. One deliberate deviation: the
|
||||
// toolchain treats zero-argument runtime calls (duffcopy and friends) as
|
||||
// leaf-compatible; here any CALL makes the function a non-leaf.
|
||||
func computeFrame(t *ast.Text) frameInfo {
|
||||
fi := frameInfo{}
|
||||
if t.Frame != nil && t.Frame.Imm.HasVal {
|
||||
@@ -247,9 +346,141 @@ func computeFrame(t *ast.Text) frameInfo {
|
||||
} else {
|
||||
fi.fpAdjust = 8 // return address only
|
||||
}
|
||||
|
||||
noSplit := false
|
||||
for _, f := range t.Flags {
|
||||
if strings.EqualFold(f, "NOSPLIT") {
|
||||
noSplit = true
|
||||
}
|
||||
}
|
||||
// The toolchain's autoffset: the frame plus the saved base pointer.
|
||||
framesize := fi.size
|
||||
if framesize > 0 {
|
||||
framesize += 8
|
||||
}
|
||||
switch {
|
||||
case noSplit:
|
||||
case framesize < stackSmall && !hasCall(t):
|
||||
// Auto-NOSPLIT, as the toolchain's leaf search concludes.
|
||||
default:
|
||||
fi.needSplit = true
|
||||
fi.framesize = framesize
|
||||
switch {
|
||||
case framesize <= stackSmall:
|
||||
fi.splitClass = 0
|
||||
case framesize <= stackBig:
|
||||
fi.splitClass = 1
|
||||
default:
|
||||
fi.splitClass = 2
|
||||
}
|
||||
}
|
||||
return fi
|
||||
}
|
||||
|
||||
// hasCall reports whether the function body contains a CALL instruction.
|
||||
func hasCall(t *ast.Text) bool {
|
||||
for _, stmt := range t.Body {
|
||||
in, ok := stmt.(*ast.Instr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if strings.ToUpper(in.Mnemonic.Text) == "CALL" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// guardLen returns the byte length of the stack-split guard prefix. The
|
||||
// final conditional branch (JBE, and JB in the big class) is 2 bytes in the
|
||||
// short form and 6 in the long form.
|
||||
func (fi frameInfo) guardLen(jbLong, jbeLong bool) int {
|
||||
if !fi.needSplit {
|
||||
return 0
|
||||
}
|
||||
jb, jbe := 2, 2
|
||||
if jbLong {
|
||||
jb = 6
|
||||
}
|
||||
if jbeLong {
|
||||
jbe = 6
|
||||
}
|
||||
switch fi.splitClass {
|
||||
case 0:
|
||||
return 9 + 4 + jbe
|
||||
case 1:
|
||||
return 9 + 8 + 4 + jbe
|
||||
default:
|
||||
return 9 + 3 + 7 + jb + 4 + jbe
|
||||
}
|
||||
}
|
||||
|
||||
// moreLen returns the byte length of the trailing morestack block: the CALL
|
||||
// (always rel32) plus the JMP back to the function start.
|
||||
func moreLen(jmpLong bool) int {
|
||||
jmp := 2
|
||||
if jmpLong {
|
||||
jmp = 5
|
||||
}
|
||||
return 5 + jmp
|
||||
}
|
||||
|
||||
// buildGuard emits the stack-split guard prefix. jbeDisp and jbDisp are the
|
||||
// already-computed displacements of the conditional branches that jump to the
|
||||
// morestack block (unused in classes without them). The TLS load carries a
|
||||
// R_TLS_LE patch site at offset 5.
|
||||
func buildGuard(fi frameInfo, jbeDisp, jbDisp int32) ([]byte, sbPatch) {
|
||||
out := []byte{
|
||||
0x64, 0x4c, 0x8b, 0x34, 0x25, // MOVQ FS:0, R14
|
||||
0, 0, 0, 0, // TLS slot offset, filled by the linker
|
||||
}
|
||||
tls := sbPatch{off: 5, after: 9, kind: RelTLSLE}
|
||||
jmp := func(op8, op32 byte, disp int32) []byte {
|
||||
if disp >= -128 && disp <= 127 {
|
||||
return []byte{op8, byte(disp)}
|
||||
}
|
||||
return append([]byte{0x0F, op32}, le32(int64(disp))...)
|
||||
}
|
||||
switch fi.splitClass {
|
||||
case 0:
|
||||
// CMPQ SP, 16(R14)
|
||||
out = append(out, 0x49, 0x3b, 0x66, 0x10)
|
||||
out = append(out, jmp(0x76, 0x86, jbeDisp)...)
|
||||
case 1:
|
||||
// LEAQ -(framesize-StackSmall)(SP), R12; CMPQ R12, 16(R14)
|
||||
out = append(out, 0x4c, 0x8d, 0xa4, 0x24)
|
||||
out = append(out, le32(-int64(fi.framesize-stackSmall))...)
|
||||
out = append(out, 0x4d, 0x3b, 0x66, 0x10)
|
||||
out = append(out, jmp(0x76, 0x86, jbeDisp)...)
|
||||
default:
|
||||
// MOVQ SP, R12; SUBQ $(framesize-StackSmall), R12; JB; CMPQ R12, 16(R14)
|
||||
out = append(out, 0x49, 0x89, 0xe4)
|
||||
out = append(out, 0x49, 0x81, 0xec)
|
||||
out = append(out, le32(int64(fi.framesize-stackSmall))...)
|
||||
out = append(out, jmp(0x72, 0x82, jbDisp)...)
|
||||
out = append(out, 0x4d, 0x3b, 0x66, 0x10)
|
||||
out = append(out, jmp(0x76, 0x86, jbeDisp)...)
|
||||
}
|
||||
return out, tls
|
||||
}
|
||||
|
||||
// buildMoreStack emits the trailing block: CALL runtime.morestack_noctxt
|
||||
// (patched by the linker) and a JMP back to the function start.
|
||||
func buildMoreStack(jmpDisp int32) ([]byte, sbPatch) {
|
||||
out := []byte{0xE8, 0, 0, 0, 0}
|
||||
call := sbPatch{off: 1, after: 5, name: "runtime\u00b7morestack_noctxt", kind: RelCall}
|
||||
out = append(out, jmpBytes(jmpDisp)...)
|
||||
return out, call
|
||||
}
|
||||
|
||||
// jmpBytes encodes a near JMP in the short or long form.
|
||||
func jmpBytes(disp int32) []byte {
|
||||
if disp >= -128 && disp <= 127 {
|
||||
return []byte{0xEB, byte(disp)}
|
||||
}
|
||||
return append([]byte{0xE9}, le32(int64(disp))...)
|
||||
}
|
||||
|
||||
// prologueBytes emits: PUSHQ BP; MOVQ SP, BP; SUBQ $size, SP.
|
||||
func prologueBytes(size int) []byte {
|
||||
out := []byte{0x55, 0x48, 0x89, 0xE5} // PUSHQ BP; MOVQ SP, BP
|
||||
@@ -333,6 +564,23 @@ func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, lon
|
||||
var ps []sbPatch
|
||||
var err error
|
||||
if isJumpMnemonic(mnem) {
|
||||
if mnem == "CALL" && isSBCall(s) {
|
||||
// CALL sym(SB): a rel32 call against a static or external
|
||||
// symbol, resolved by the file-level layout or the linker.
|
||||
code, ps, err = encodeSBCall(s, link)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for i := range ps {
|
||||
ps[i].kind = RelCall
|
||||
}
|
||||
body := pc + len(prefix)
|
||||
for i := range ps {
|
||||
ps[i].off += body
|
||||
ps[i].after = body + len(code)
|
||||
}
|
||||
return append(prefix, code...), ps, nil
|
||||
}
|
||||
code, err = encodeJump(s, mnem, pc+len(prefix), offsets, long, resolve)
|
||||
} else {
|
||||
code, ps, err = encodeNormal(s, fi, link)
|
||||
@@ -414,6 +662,33 @@ func encodeJump(s *ast.Instr, mnem string, pc int, offsets map[string]int, long
|
||||
}
|
||||
}
|
||||
|
||||
// isSBCall reports whether the CALL operand is a symbol reference.
|
||||
func isSBCall(s *ast.Instr) bool {
|
||||
return len(s.Operands) == 1 && s.Operands[0].Kind == ast.OpAddr &&
|
||||
s.Operands[0].Addr.Sym != nil && s.Operands[0].Addr.Sym.Pseudo == "SB"
|
||||
}
|
||||
|
||||
// encodeSBCall encodes CALL sym(SB) as E8 rel32 with a patch site.
|
||||
func encodeSBCall(s *ast.Instr, link *linkInfo) ([]byte, []sbPatch, error) {
|
||||
o, err := operandFromAST(s.Operands[0], 8, frameInfo{}, link)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
m, ok := o.(sbMem)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("CALL: unsupported operand")
|
||||
}
|
||||
e := &enc{}
|
||||
if err := e.emit(&instr{opcode: []byte{0xE8}, modrm: -1, sib: -1, disp: le32(0), sb: &sbRef{name: m.name, addend: m.addend}}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ps := make([]sbPatch, len(e.patches))
|
||||
for i, p := range e.patches {
|
||||
ps[i] = sbPatch{off: p.off, name: p.name, addend: p.addend, kind: RelCall}
|
||||
}
|
||||
return e.out, ps, nil
|
||||
}
|
||||
|
||||
// labelName extracts a local-label name from a jump operand.
|
||||
func labelName(op *ast.Operand) (string, bool) {
|
||||
if op.Kind == ast.OpAddr && op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "" &&
|
||||
|
||||
+10
@@ -43,6 +43,7 @@ const (
|
||||
stInfoShift = 4
|
||||
|
||||
rX8664PC32 = 2
|
||||
rX8664TPOFF32 = 20
|
||||
)
|
||||
|
||||
// elfSym is one symbol-table entry in construction.
|
||||
@@ -125,11 +126,19 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
type elfRela struct {
|
||||
off uint64
|
||||
sym int
|
||||
typ uint32
|
||||
addend int64
|
||||
}
|
||||
var relas []elfRela
|
||||
for _, fn := range img.Funcs {
|
||||
for _, r := range fn.Relocs {
|
||||
var typ uint32 = rX8664PC32
|
||||
if r.Kind == RelTLSLE {
|
||||
// R_X86_64_TPOFF32 resolves to the local-exec TLS offset and
|
||||
// carries no symbol.
|
||||
relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), sym: 0, typ: rX8664TPOFF32})
|
||||
continue
|
||||
}
|
||||
idx, ok := symIdx[r.Name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
||||
@@ -137,6 +146,7 @@ func (img *Image) ELFObject() ([]byte, error) {
|
||||
relas = append(relas, elfRela{
|
||||
off: uint64(fn.Offset + r.Off),
|
||||
sym: idx,
|
||||
typ: typ,
|
||||
// R_X86_64_PC32 computes S + A − P with P the patch site; the
|
||||
// assembler measures the symbol from the instruction end,
|
||||
// After − Off bytes past the field, so the addend carries
|
||||
|
||||
+79
-7
@@ -94,10 +94,12 @@ const (
|
||||
)
|
||||
|
||||
// Relocation types (cmd/internal/objabi).
|
||||
// R_PCREL and R_ADDR are stable across Go versions.
|
||||
// R_ADDR, R_CALL, R_PCREL and R_TLS_LE are stable across Go versions.
|
||||
const (
|
||||
relocPCRel = 14 // R_PCREL
|
||||
relocAddr = 1 // R_ADDR
|
||||
relocCall = 7 // R_CALL
|
||||
relocPCRel = 14 // R_PCREL
|
||||
relocTLSLE = 15 // R_TLS_LE
|
||||
)
|
||||
|
||||
// relocDWTXTADDRU4 returns the R_DWTXTADDR_U4 relocation type for the
|
||||
@@ -142,8 +144,18 @@ func isGo127OrLater() bool {
|
||||
const (
|
||||
pkgIdxNone = 0x7fffffff
|
||||
pkgIdxSelf = 0x7ffffffb
|
||||
pkgIdxBuiltin = 0x7ffffffc
|
||||
)
|
||||
|
||||
// goobjBuiltinMorestackNoctxt is the index of runtime.morestack_noctxt in
|
||||
// cmd/internal/goobj/builtinlist.go of the toolchain the object targets
|
||||
// (246 since Go 1.25; the list is append-only).
|
||||
const goobjBuiltinMorestackNoctxt = 246
|
||||
|
||||
// goobjBuiltinMorestack is the builtin reference the toolchain emits for the
|
||||
// stack-guard call.
|
||||
var goobjBuiltinMorestack = "runtime\u00b7morestack_noctxt"
|
||||
|
||||
const goobjMagic = "\x00go120ld"
|
||||
|
||||
// goSym is one symbol definition under construction.
|
||||
@@ -184,8 +196,18 @@ func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// amd64: MinLC 1, R_PCREL for the code relocations.
|
||||
return img.emitGOObject(pkgPath, srcPath, pre, 1, func(Reloc) (uint16, uint8) { return relocPCRel, 4 })
|
||||
// amd64: MinLC 1, R_PCREL for displacements, R_CALL for calls and
|
||||
// R_TLS_LE for the stack-guard TLS load.
|
||||
return img.emitGOObject(pkgPath, srcPath, pre, 1, func(r Reloc) (uint16, uint8) {
|
||||
switch r.Kind {
|
||||
case RelCall:
|
||||
return relocCall, 4
|
||||
case RelTLSLE:
|
||||
return relocTLSLE, 4
|
||||
default:
|
||||
return relocPCRel, 4
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// emitGOObject assembles the GOOBJ payload for any architecture. pre is
|
||||
@@ -247,7 +269,7 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
// their relocations cover whole AUIPC/pcalau12i pairs, so
|
||||
// zeroing r.Off would erase the opcode/register bits the linker
|
||||
// preserves when it patches only the immediate.
|
||||
if r.Kind != RelPCRel32 {
|
||||
if r.Kind != RelPCRel32 && r.Kind != RelCall {
|
||||
continue
|
||||
}
|
||||
if r.Off >= 0 && r.Off+4 <= len(code) {
|
||||
@@ -320,6 +342,13 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
)
|
||||
}
|
||||
|
||||
// Index the non-package TEXT definitions by short name for the internal
|
||||
// call references.
|
||||
textNpIdx := map[string]int{}
|
||||
for i, fn := range img.Funcs {
|
||||
textNpIdx[fn.Name] = fnNpIdx[i]
|
||||
}
|
||||
|
||||
// Resolve external symbol references (cross-package). Build the
|
||||
// package index table and determine each external symbol's SymIdx
|
||||
// by reading the target package's export data.
|
||||
@@ -327,12 +356,22 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
var extPkgIdx map[string]int
|
||||
var extSymIdx map[string]int
|
||||
if len(img.Externals) > 0 {
|
||||
// The morestack call is a builtin reference, not a resolved external.
|
||||
var need []string
|
||||
for _, n := range img.Externals {
|
||||
if n == goobjBuiltinMorestack {
|
||||
continue
|
||||
}
|
||||
need = append(need, n)
|
||||
}
|
||||
if len(need) > 0 {
|
||||
var err error
|
||||
extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(img.Externals)
|
||||
extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(need)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Relocations, per defined symbol in definition order (package defs,
|
||||
// then non-package defs).
|
||||
@@ -342,6 +381,31 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
si := len(defs) + fnNpIdx[i]
|
||||
for _, r := range fn.Relocs {
|
||||
typ, size := relocField(r)
|
||||
if r.Kind == RelTLSLE {
|
||||
// The TLS load has no symbol: {0, 0} is the nil ref.
|
||||
var rec [23]byte
|
||||
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||
rec[4] = size
|
||||
binary.LittleEndian.PutUint16(rec[5:], typ)
|
||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||
binary.LittleEndian.PutUint32(rec[15:], 0)
|
||||
binary.LittleEndian.PutUint32(rec[19:], 0)
|
||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||
continue
|
||||
}
|
||||
if r.External && r.Kind == RelCall && r.Name == goobjBuiltinMorestack {
|
||||
// The stack-guard morestack call uses the toolchain's
|
||||
// builtin reference.
|
||||
var rec [23]byte
|
||||
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||
rec[4] = size
|
||||
binary.LittleEndian.PutUint16(rec[5:], typ)
|
||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkgIdxBuiltin)
|
||||
binary.LittleEndian.PutUint32(rec[19:], goobjBuiltinMorestackNoctxt)
|
||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||
continue
|
||||
}
|
||||
if r.External {
|
||||
// Split package-qualified name: "runtime·morestack" → runtime, morestack.
|
||||
pkg, name := splitQualified(r.Name)
|
||||
@@ -366,16 +430,24 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r
|
||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||
continue
|
||||
}
|
||||
pkg := uint32(pkgIdxSelf)
|
||||
di, ok := defIdx[r.Name]
|
||||
if !ok {
|
||||
// A call to a TEXT function of the same file references the
|
||||
// non-package definition table.
|
||||
ni, isText := textNpIdx[r.Name]
|
||||
if !isText || r.Kind != RelCall {
|
||||
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
|
||||
}
|
||||
pkg = pkgIdxNone
|
||||
di = ni
|
||||
}
|
||||
var rec [23]byte
|
||||
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||
rec[4] = size // field width
|
||||
binary.LittleEndian.PutUint16(rec[5:], typ)
|
||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf)
|
||||
binary.LittleEndian.PutUint32(rec[15:], pkg)
|
||||
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
|
||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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")
|
||||
}
|
||||
}
|
||||
+19
-1
@@ -91,6 +91,8 @@ type RelocKind int
|
||||
|
||||
const (
|
||||
RelPCRel32 RelocKind = iota // 32-bit PC-relative (amd64)
|
||||
RelCall // R_CALL: CALL to a function symbol (amd64)
|
||||
RelTLSLE // R_TLS_LE: local-exec TLS load, no symbol (amd64 guard)
|
||||
RelRISCVPCRELIType // R_RISCV_PCREL_ITYPE (AUIPC + I-type pair)
|
||||
RelRISCVPCRELSType // R_RISCV_PCREL_STYPE (AUIPC + S-type pair)
|
||||
RelRISCVJal // R_RISCV_JAL (J-type call)
|
||||
@@ -148,6 +150,7 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
link := &linkInfo{symbols: known, allowExternal: true}
|
||||
|
||||
img := &Image{Symbols: map[string]int{}}
|
||||
textOff := map[string]int{}
|
||||
type asmFunc struct {
|
||||
name string
|
||||
patches []sbPatch
|
||||
@@ -185,6 +188,7 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
for _, s := range steps {
|
||||
fl.Spadj = append(fl.Spadj, SpadjStep{PC: s.pc, Value: s.value})
|
||||
}
|
||||
textOff[t.Name.Name] = len(img.Code)
|
||||
img.Funcs = append(img.Funcs, fl)
|
||||
img.Code = append(img.Code, code...)
|
||||
funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches})
|
||||
@@ -217,13 +221,27 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
||||
base := img.Funcs[i].Offset
|
||||
code := img.Code[base : base+img.Funcs[i].Size]
|
||||
for _, p := range fn.patches {
|
||||
reloc := Reloc{Off: p.off, After: p.after, Name: p.name, Addend: p.addend}
|
||||
reloc := Reloc{Off: p.off, After: p.after, Name: p.name, Addend: p.addend, Kind: p.kind}
|
||||
if p.kind == RelTLSLE {
|
||||
// The TLS slot has no symbol: the linker fills the offset
|
||||
// from the runtime's TLS layout.
|
||||
img.Funcs[i].Relocs = append(img.Funcs[i].Relocs, reloc)
|
||||
continue
|
||||
}
|
||||
if imgOff, ok := img.Symbols[p.name]; ok {
|
||||
rel := int64(imgOff) + p.addend - int64(base+p.after)
|
||||
if rel < -1<<31 || rel >= 1<<31 {
|
||||
return nil, fmt.Errorf("%s: displacement to %q out of rel32 range", fn.name, p.name)
|
||||
}
|
||||
copy(code[p.off:p.off+4], le32(rel))
|
||||
} else if imgOff, ok := textOff[p.name]; ok {
|
||||
// A CALL to a TEXT function of the same file: resolve the
|
||||
// displacement against the function's layout position.
|
||||
rel := int64(imgOff) + p.addend - int64(base+p.after)
|
||||
if rel < -1<<31 || rel >= 1<<31 {
|
||||
return nil, fmt.Errorf("%s: displacement to %q out of rel32 range", fn.name, p.name)
|
||||
}
|
||||
copy(code[p.off:p.off+4], le32(rel))
|
||||
} else {
|
||||
reloc.External = true
|
||||
externals[p.name] = true
|
||||
|
||||
Reference in New Issue
Block a user