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
|
after int
|
||||||
name string
|
name string
|
||||||
addend int64
|
addend int64
|
||||||
|
kind RelocKind
|
||||||
}
|
}
|
||||||
|
|
||||||
// spadjStep is one stack-adjustment boundary within a function: Value is the
|
// 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
|
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))
|
long := make([]bool, len(t.Body))
|
||||||
sizes := make([]int, len(t.Body))
|
sizes := make([]int, len(t.Body))
|
||||||
offsets := map[string]int{}
|
offsets := map[string]int{}
|
||||||
pcs := make([]int, len(t.Body))
|
pcs := make([]int, len(t.Body))
|
||||||
|
var guardJBlong, guardJBElong, moreJMPlong bool
|
||||||
for {
|
for {
|
||||||
pos := len(fi.prologue)
|
guard := fi.guardLen(guardJBlong, guardJBElong)
|
||||||
|
pos := guard + len(fi.prologue)
|
||||||
for i, stmt := range t.Body {
|
for i, stmt := range t.Body {
|
||||||
switch s := stmt.(type) {
|
switch s := stmt.(type) {
|
||||||
case *ast.Label:
|
case *ast.Label:
|
||||||
@@ -96,6 +102,7 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
|||||||
pos += sz
|
pos += sz
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bodyLen := pos - (guard + len(fi.prologue))
|
||||||
// Expand any short jump whose displacement no longer fits rel8.
|
// Expand any short jump whose displacement no longer fits rel8.
|
||||||
changed := false
|
changed := false
|
||||||
for i, stmt := range t.Body {
|
for i, stmt := range t.Body {
|
||||||
@@ -121,25 +128,75 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
|||||||
changed = true
|
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 {
|
if !changed {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 2: emit.
|
// Pass 2: emit. The guard comes first, then the prologue, the body and
|
||||||
out := append([]byte(nil), fi.prologue...)
|
// 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
|
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 steps []spadjStep
|
||||||
var lines []LineEntry
|
var lines []LineEntry
|
||||||
if fi.useFP {
|
if fi.useFP {
|
||||||
// PUSHQ BP saves the return-address-relative base (+8); the MOVQ
|
// PUSHQ BP saves the return-address-relative base (+8); the MOVQ
|
||||||
// changes nothing; SUBQ $size, SP completes the frame.
|
// changes nothing; SUBQ $size, SP completes the frame.
|
||||||
steps = append(steps,
|
steps = append(steps,
|
||||||
spadjStep{1, 8},
|
spadjStep{guardLen + 1, 8},
|
||||||
spadjStep{len(fi.prologue), 8 + fi.size},
|
spadjStep{guardLen + len(fi.prologue), 8 + fi.size},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
pos := len(fi.prologue)
|
pos := guardLen + len(fi.prologue)
|
||||||
for i, stmt := range t.Body {
|
for i, stmt := range t.Body {
|
||||||
s, ok := stmt.(*ast.Instr)
|
s, ok := stmt.(*ast.Instr)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -161,11 +218,32 @@ func assemble(t *ast.Text, link *linkInfo) ([]byte, []sbPatch, map[string]int, [
|
|||||||
if len(code) != sizes[i] {
|
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])
|
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...)
|
patches = append(patches, ps...)
|
||||||
lines = append(lines, LineEntry{Offset: pos, Line: s.Pos().Line})
|
lines = append(lines, LineEntry{Offset: pos, Line: s.Pos().Line})
|
||||||
out = append(out, code...)
|
out = append(out, code...)
|
||||||
pos += len(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
|
return out, patches, offsets, steps, lines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,10 +307,31 @@ type frameInfo struct {
|
|||||||
spAdjust int64 // x-N(SP) becomes (spAdjust - N)(SP)
|
spAdjust int64 // x-N(SP) becomes (spAdjust - N)(SP)
|
||||||
prologue []byte
|
prologue []byte
|
||||||
epilogue []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
|
// 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 {
|
func computeFrame(t *ast.Text) frameInfo {
|
||||||
fi := frameInfo{}
|
fi := frameInfo{}
|
||||||
if t.Frame != nil && t.Frame.Imm.HasVal {
|
if t.Frame != nil && t.Frame.Imm.HasVal {
|
||||||
@@ -247,9 +346,141 @@ func computeFrame(t *ast.Text) frameInfo {
|
|||||||
} else {
|
} else {
|
||||||
fi.fpAdjust = 8 // return address only
|
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
|
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.
|
// prologueBytes emits: PUSHQ BP; MOVQ SP, BP; SUBQ $size, SP.
|
||||||
func prologueBytes(size int) []byte {
|
func prologueBytes(size int) []byte {
|
||||||
out := []byte{0x55, 0x48, 0x89, 0xE5} // PUSHQ BP; MOVQ SP, BP
|
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 ps []sbPatch
|
||||||
var err error
|
var err error
|
||||||
if isJumpMnemonic(mnem) {
|
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)
|
code, err = encodeJump(s, mnem, pc+len(prefix), offsets, long, resolve)
|
||||||
} else {
|
} else {
|
||||||
code, ps, err = encodeNormal(s, fi, link)
|
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.
|
// labelName extracts a local-label name from a jump operand.
|
||||||
func labelName(op *ast.Operand) (string, bool) {
|
func labelName(op *ast.Operand) (string, bool) {
|
||||||
if op.Kind == ast.OpAddr && op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "" &&
|
if op.Kind == ast.OpAddr && op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "" &&
|
||||||
|
|||||||
+10
@@ -43,6 +43,7 @@ const (
|
|||||||
stInfoShift = 4
|
stInfoShift = 4
|
||||||
|
|
||||||
rX8664PC32 = 2
|
rX8664PC32 = 2
|
||||||
|
rX8664TPOFF32 = 20
|
||||||
)
|
)
|
||||||
|
|
||||||
// elfSym is one symbol-table entry in construction.
|
// elfSym is one symbol-table entry in construction.
|
||||||
@@ -125,11 +126,19 @@ func (img *Image) ELFObject() ([]byte, error) {
|
|||||||
type elfRela struct {
|
type elfRela struct {
|
||||||
off uint64
|
off uint64
|
||||||
sym int
|
sym int
|
||||||
|
typ uint32
|
||||||
addend int64
|
addend int64
|
||||||
}
|
}
|
||||||
var relas []elfRela
|
var relas []elfRela
|
||||||
for _, fn := range img.Funcs {
|
for _, fn := range img.Funcs {
|
||||||
for _, r := range fn.Relocs {
|
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]
|
idx, ok := symIdx[r.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("relocation references unknown symbol %q", r.Name)
|
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{
|
relas = append(relas, elfRela{
|
||||||
off: uint64(fn.Offset + r.Off),
|
off: uint64(fn.Offset + r.Off),
|
||||||
sym: idx,
|
sym: idx,
|
||||||
|
typ: typ,
|
||||||
// R_X86_64_PC32 computes S + A − P with P the patch site; the
|
// R_X86_64_PC32 computes S + A − P with P the patch site; the
|
||||||
// assembler measures the symbol from the instruction end,
|
// assembler measures the symbol from the instruction end,
|
||||||
// After − Off bytes past the field, so the addend carries
|
// After − Off bytes past the field, so the addend carries
|
||||||
|
|||||||
+79
-7
@@ -94,10 +94,12 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Relocation types (cmd/internal/objabi).
|
// 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 (
|
const (
|
||||||
relocPCRel = 14 // R_PCREL
|
|
||||||
relocAddr = 1 // R_ADDR
|
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
|
// relocDWTXTADDRU4 returns the R_DWTXTADDR_U4 relocation type for the
|
||||||
@@ -142,8 +144,18 @@ func isGo127OrLater() bool {
|
|||||||
const (
|
const (
|
||||||
pkgIdxNone = 0x7fffffff
|
pkgIdxNone = 0x7fffffff
|
||||||
pkgIdxSelf = 0x7ffffffb
|
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"
|
const goobjMagic = "\x00go120ld"
|
||||||
|
|
||||||
// goSym is one symbol definition under construction.
|
// goSym is one symbol definition under construction.
|
||||||
@@ -184,8 +196,18 @@ func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// amd64: MinLC 1, R_PCREL for the code relocations.
|
// amd64: MinLC 1, R_PCREL for displacements, R_CALL for calls and
|
||||||
return img.emitGOObject(pkgPath, srcPath, pre, 1, func(Reloc) (uint16, uint8) { return relocPCRel, 4 })
|
// 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
|
// 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
|
// their relocations cover whole AUIPC/pcalau12i pairs, so
|
||||||
// zeroing r.Off would erase the opcode/register bits the linker
|
// zeroing r.Off would erase the opcode/register bits the linker
|
||||||
// preserves when it patches only the immediate.
|
// preserves when it patches only the immediate.
|
||||||
if r.Kind != RelPCRel32 {
|
if r.Kind != RelPCRel32 && r.Kind != RelCall {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if r.Off >= 0 && r.Off+4 <= len(code) {
|
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
|
// Resolve external symbol references (cross-package). Build the
|
||||||
// package index table and determine each external symbol's SymIdx
|
// package index table and determine each external symbol's SymIdx
|
||||||
// by reading the target package's export data.
|
// 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 extPkgIdx map[string]int
|
||||||
var extSymIdx map[string]int
|
var extSymIdx map[string]int
|
||||||
if len(img.Externals) > 0 {
|
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
|
var err error
|
||||||
extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(img.Externals)
|
extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(need)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err)
|
return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Relocations, per defined symbol in definition order (package defs,
|
// Relocations, per defined symbol in definition order (package defs,
|
||||||
// then non-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]
|
si := len(defs) + fnNpIdx[i]
|
||||||
for _, r := range fn.Relocs {
|
for _, r := range fn.Relocs {
|
||||||
typ, size := relocField(r)
|
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 {
|
if r.External {
|
||||||
// Split package-qualified name: "runtime·morestack" → runtime, morestack.
|
// Split package-qualified name: "runtime·morestack" → runtime, morestack.
|
||||||
pkg, name := splitQualified(r.Name)
|
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[:]...)
|
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
pkg := uint32(pkgIdxSelf)
|
||||||
di, ok := defIdx[r.Name]
|
di, ok := defIdx[r.Name]
|
||||||
if !ok {
|
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)
|
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
|
||||||
}
|
}
|
||||||
|
pkg = pkgIdxNone
|
||||||
|
di = ni
|
||||||
|
}
|
||||||
var rec [23]byte
|
var rec [23]byte
|
||||||
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||||
rec[4] = size // field width
|
rec[4] = size // field width
|
||||||
binary.LittleEndian.PutUint16(rec[5:], typ)
|
binary.LittleEndian.PutUint16(rec[5:], typ)
|
||||||
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
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))
|
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
|
||||||
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
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 (
|
const (
|
||||||
RelPCRel32 RelocKind = iota // 32-bit PC-relative (amd64)
|
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)
|
RelRISCVPCRELIType // R_RISCV_PCREL_ITYPE (AUIPC + I-type pair)
|
||||||
RelRISCVPCRELSType // R_RISCV_PCREL_STYPE (AUIPC + S-type pair)
|
RelRISCVPCRELSType // R_RISCV_PCREL_STYPE (AUIPC + S-type pair)
|
||||||
RelRISCVJal // R_RISCV_JAL (J-type call)
|
RelRISCVJal // R_RISCV_JAL (J-type call)
|
||||||
@@ -148,6 +150,7 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
|||||||
link := &linkInfo{symbols: known, allowExternal: true}
|
link := &linkInfo{symbols: known, allowExternal: true}
|
||||||
|
|
||||||
img := &Image{Symbols: map[string]int{}}
|
img := &Image{Symbols: map[string]int{}}
|
||||||
|
textOff := map[string]int{}
|
||||||
type asmFunc struct {
|
type asmFunc struct {
|
||||||
name string
|
name string
|
||||||
patches []sbPatch
|
patches []sbPatch
|
||||||
@@ -185,6 +188,7 @@ func AssembleFile(f *ast.File) (*Image, error) {
|
|||||||
for _, s := range steps {
|
for _, s := range steps {
|
||||||
fl.Spadj = append(fl.Spadj, SpadjStep{PC: s.pc, Value: s.value})
|
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.Funcs = append(img.Funcs, fl)
|
||||||
img.Code = append(img.Code, code...)
|
img.Code = append(img.Code, code...)
|
||||||
funcs = append(funcs, asmFunc{name: t.Name.Name, patches: patches})
|
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
|
base := img.Funcs[i].Offset
|
||||||
code := img.Code[base : base+img.Funcs[i].Size]
|
code := img.Code[base : base+img.Funcs[i].Size]
|
||||||
for _, p := range fn.patches {
|
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 {
|
if imgOff, ok := img.Symbols[p.name]; ok {
|
||||||
rel := int64(imgOff) + p.addend - int64(base+p.after)
|
rel := int64(imgOff) + p.addend - int64(base+p.after)
|
||||||
if rel < -1<<31 || rel >= 1<<31 {
|
if rel < -1<<31 || rel >= 1<<31 {
|
||||||
return nil, fmt.Errorf("%s: displacement to %q out of rel32 range", fn.name, p.name)
|
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))
|
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 {
|
} else {
|
||||||
reloc.External = true
|
reloc.External = true
|
||||||
externals[p.name] = true
|
externals[p.name] = true
|
||||||
|
|||||||
Reference in New Issue
Block a user