From f2892e4f599bff233e34e337c3b486241f38b3a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Wed, 7 Oct 2026 13:19:12 +0200 Subject: [PATCH] feat(asm): emit the riscv64 local-exec TLS sequence Assisted-by: GLM 5.3 --- asm/elfriscv.go | 11 ++++++++ asm/goobjriscv.go | 6 +++- asm/link.go | 36 +++++++++++++++++++++++- asm/riscv_assemble.go | 54 ++++++++++++++++++++++++++++++------ asm/riscv_compressed_test.go | 2 +- asm/riscv_encode_test.go | 34 +++++++++++------------ 6 files changed, 114 insertions(+), 29 deletions(-) diff --git a/asm/elfriscv.go b/asm/elfriscv.go index 2126922..475540f 100644 --- a/asm/elfriscv.go +++ b/asm/elfriscv.go @@ -24,6 +24,8 @@ const ( rRISCVPCRELHI20 = 23 // R_RISCV_PCREL_HI20 rRISCVPCRELLO12I = 24 // R_RISCV_PCREL_LO12_I rRISCVPCRELLO12S = 25 // R_RISCV_PCREL_LO12_S + rRISCVTPRELHI20 = 29 // R_RISCV_TPREL_HI20 + rRISCVTPRELLO12I = 30 // R_RISCV_TPREL_LO12_I // R_RISCV_32 (debug/elf 1): the absolute 32-bit address of a symbol, // the R_ADDR shape a 4-byte DATA field carries. R_RISCV_64 (2) lives // with the DWARF fixup constants as rRISCVAbs64. @@ -128,6 +130,15 @@ func (img *Image) ELFRISCVObject() ([]byte, error) { ) case RelRISCVJal: relas = append(relas, elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVJAL, sym: idx, addend: r.Addend}) + case RelRISCVTLSLE: + // The local-exec pair splits into the TPREL HI20 on the LUI + // and the TPREL LO12_I on the ADDIW, both against the symbol + // (a thread offset, not PC-relative, so the LO12 needs no + // label indirection). + relas = append(relas, + elfRela{off: uint64(fn.Offset + r.Off), typ: rRISCVTPRELHI20, sym: idx, addend: r.Addend}, + elfRela{off: uint64(fn.Offset + r.Off + 4), typ: rRISCVTPRELLO12I, sym: idx, addend: r.Addend}, + ) default: return nil, fmt.Errorf("relocation kind %v unsupported in ELF emission", r.Kind) } diff --git a/asm/goobjriscv.go b/asm/goobjriscv.go index 01e0c08..fc9eb37 100644 --- a/asm/goobjriscv.go +++ b/asm/goobjriscv.go @@ -30,6 +30,8 @@ func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) { return relocRISCVPcrelStype, 8 case RelRISCVJal: return relocRISCVJal, 4 + case RelRISCVTLSLE: + return relocRISCVTLSLE, 8 default: return relocRISCVPcrelItype, 8 } @@ -38,11 +40,13 @@ func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) { // RISC-V relocation types (cmd/internal/objabi). The Go linker applies // R_RISCV_PCREL_ITYPE/STYPE to an AUIPC + I/S-type instruction pair as a -// single 8-byte field; R_RISCV_JAL covers a single 4-byte J-type instruction. +// single 8-byte field; R_RISCV_JAL covers a single 4-byte J-type instruction; +// R_RISCV_TLS_LE covers the LUI + I-type pair of a local-exec TLS reference. const ( relocRISCVJal = 59 // R_RISCV_JAL relocRISCVPcrelItype = 62 // R_RISCV_PCREL_ITYPE relocRISCVPcrelStype = 63 // R_RISCV_PCREL_STYPE + relocRISCVTLSLE = 65 // R_RISCV_TLS_LE ) // toolchainObjectPreambleRISCV returns the "go object ...\n!\n" header diff --git a/asm/link.go b/asm/link.go index 78434c2..97f3172 100644 --- a/asm/link.go +++ b/asm/link.go @@ -98,6 +98,7 @@ const ( 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) + RelRISCVTLSLE // R_RISCV_TLS_LE (LUI + I-type local-exec pair) RelLoong64AddrHi // R_LOONG64_ADDR_HI (pcalau12i) RelLoong64AddrLo // R_LOONG64_ADDR_LO (addi.d/ld/st) RelArm64Addr // R_ADDRARM64 (ADRP + ADD pair) @@ -335,6 +336,35 @@ func AssembleFile(f *ast.File, opts ...AssembleOption) (*Image, error) { return img, nil } +// riscvTLSSymbols names the symbols the file declares with the TLSBSS flag +// (or its legacy numeric constant 256 from textflag.h): the assembler gives +// their SB references the local-exec TLS sequence. +func riscvTLSSymbols(f *ast.File) map[string]bool { + var tls map[string]bool + for _, d := range f.Decls { + gd, ok := d.(*ast.Globl) + if !ok || gd.Name == nil || gd.Name.Pseudo != "SB" { + continue + } + for _, fl := range gd.Flags { + isTLS := fl == "TLSBSS" + if !isTLS { + if n, err := strconv.Atoi(fl); err == nil && n&256 != 0 { + isTLS = true + } + } + if isTLS { + if tls == nil { + tls = map[string]bool{} + } + tls[gd.Name.Name] = true + break + } + } + } + return tls +} + // AssembleFileRISCV assembles every TEXT function of a parsed RISC-V file // and lays out its static symbols (GLOBL/DATA) in a data section behind the // code. SB references in the code are encoded as AUIPC pairs with zero @@ -344,6 +374,10 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) { if err != nil { return nil, err } + // The symbols the file declares TLSBSS resolve through the local-exec + // sequence (LUI + ADDIW + ADD of TP), exactly as the toolchain routes + // every SB reference whose symbol carries the STLSBSS type. + tlsSyms := riscvTLSSymbols(f) // The pooled $i64 constants the wide MOV immediate loads refer to join // the declared data as read-only symbols, deduplicated across the file // (the toolchain synthesises the same symbols into its rodata). @@ -355,7 +389,7 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) { if !ok { continue } - code, labels, relocs, lines, spadj, lits, err := assembleRISCV(t) + code, labels, relocs, lines, spadj, lits, err := assembleRISCV(t, tlsSyms) if err != nil { return nil, fmt.Errorf("%s: %w", t.Name.Name, err) } diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index d073e68..3b01b54 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -17,7 +17,7 @@ import ( // assembleRISCV assembles a RISC-V TEXT function body into machine code. // It handles the full RV64IMAFDC instruction set including RVC compression. -func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, []RiscvLiteral, error) { +func assembleRISCV(t *ast.Text, tlsSyms map[string]bool) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, []RiscvLiteral, error) { fi := riscvComputeFrame(t) prologue := riscvPrologue(fi) guardLen, err := riscvGuardLen(fi) @@ -66,7 +66,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [ } recs = append(recs, instrRec{instr: s, pad: pendingPad}) pendingPad = 0 - pos += riscvInstrSize(s, fi) + pos += riscvInstrSize(s, fi, tlsSyms) } } @@ -78,12 +78,12 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [ pc := len(prologue) for i := range recs { branchLike := isBranchLike(recs[i].instr.Mnemonic.Text) || riscvIsCondBranch(recs[i].instr.Mnemonic.Text) - code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi, nil, nil, lits) // no relocs in Pass 2 + code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi, nil, nil, lits, tlsSyms) // no relocs in Pass 2 if err != nil && !(branchLike && riscvIsRangeError(err)) { return nil, nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err) } if err != nil { - code = make([]byte, riscvInstrSize(recs[i].instr, fi)) + code = make([]byte, riscvInstrSize(recs[i].instr, fi, tlsSyms)) } recs[i].code = code pc += len(code) @@ -252,7 +252,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [ code = r.code default: var err error - code, err = encodeRISCVInstr(r.instr, pc, offsets, fi, &relocs, pcRelPcs, lits) + code, err = encodeRISCVInstr(r.instr, pc, offsets, fi, &relocs, pcRelPcs, lits, tlsSyms) if err != nil { return nil, nil, nil, nil, nil, nil, err } @@ -376,7 +376,7 @@ func riscvFenceFlag(op *ast.Operand) (uint32, bool) { // Most instructions are 4 bytes; MOV with a large immediate and I-type // arithmetic with a large immediate expand to several (possibly compressed) // instructions. -func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int { +func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo, tlsSyms map[string]bool) int { mnem := instr.Mnemonic.Text ops := instr.Operands mnem = riscvNormalisePseudo(mnem) @@ -401,6 +401,14 @@ func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int { return 8 } } + // A TLSBSS symbol's memory reference takes the 16-byte local-exec + // sequence (LUI + ADDIW + ADD of TP + the access). + tlsRef := func(op *ast.Operand) bool { + return op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "SB" && tlsSyms[op.Addr.Sym.Name] + } + if tlsRef(ops[0]) || tlsRef(ops[1]) { + return 16 + } // MOV $sym(SB), rd → 8 bytes (AUIPC + ADDI). if isImmOperand(ops[0]) && ops[0].Imm.Sym != nil && ops[0].Imm.Sym.Pseudo == "SB" { return 8 @@ -1088,7 +1096,7 @@ func riscvCheckJumpOffset(target string, off int32) error { } // encodeRISCVInstr encodes a single RISC-V instruction. -func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc, pcRelPcs map[*ast.Instr]int, lits *riscvLiterals) ([]byte, error) { +func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc, pcRelPcs map[*ast.Instr]int, lits *riscvLiterals, tlsSyms map[string]bool) ([]byte, error) { mnem := instr.Mnemonic.Text ops := instr.Operands mnem = riscvNormalisePseudo(mnem) @@ -1269,7 +1277,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv // (MOVB/MOVH/MOVW and unsigned forms) select the access width, and // MOVD/MOVF address the FP registers. case "MOV", "MOVB", "MOVBU", "MOVH", "MOVHU", "MOVW", "MOVWU", "MOVF", "MOVD": - return encodeRISCVMov(instr, fi, relocs, lits) + return encodeRISCVMov(instr, fi, relocs, lits, tlsSyms) // JALR: indirect jump/call. Plan 9: JALR rs1, rd or JALR offset(rs1). case "JALR": @@ -1915,7 +1923,7 @@ func isImmOperand(op *ast.Operand) bool { // - MOV Rs, (Rd) register-relative store // - MOV Rs, Rd register-to-register move (ADDI $0) // - MOV $imm, Rd load immediate (ADDI or LUI+ADDIW) -func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc, lits *riscvLiterals) ([]byte, error) { +func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc, lits *riscvLiterals, tlsSyms map[string]bool) ([]byte, error) { ops := instr.Operands if len(ops) != 2 { return nil, fmt.Errorf("MOV expects 2 operands, got %d", len(ops)) @@ -2006,10 +2014,16 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc, lits * // Memory → register (load). if isMemOperand(src) && !isMemOperand(dst) { rd := regFromOperand(dst) + // MOV sym(SB), rd, load from static data. A TLSBSS symbol takes + // the local-exec sequence: LUI + ADDIW carry the offset against TP, + // the ADD folds the thread pointer in, the access reads through TMP. if src.Addr.Sym != nil && src.Addr.Sym.Pseudo == "SB" { if rd < 0 { return nil, fmt.Errorf("MOV sym(SB): invalid destination register") } + if tlsSyms[src.Addr.Sym.Name] { + return riscvTLSBytes(riscvMovEnc(strings.ToUpper(instr.Mnemonic.Text), false), false, rd, src.Addr.Sym, relocs), nil + } return encodeRISCVSBLoad(src.Addr.Sym, rd, relocs), nil } rs1, off := memFromOperandWithFrame(src, fi) @@ -2022,10 +2036,15 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc, lits * // Register → memory (store). if !isMemOperand(src) && isMemOperand(dst) { rs2 := regFromOperand(src) + // MOV rd, sym(SB), store to static data. A TLSBSS symbol takes + // the local-exec sequence with the store through TMP. if dst.Addr.Sym != nil && dst.Addr.Sym.Pseudo == "SB" { if rs2 < 0 { return nil, fmt.Errorf("MOV rd, sym(SB): invalid source register") } + if tlsSyms[dst.Addr.Sym.Name] { + return riscvTLSBytes(riscvMovEnc(strings.ToUpper(instr.Mnemonic.Text), true), true, rs2, dst.Addr.Sym, relocs), nil + } return encodeRISCVSBStore(dst.Addr.Sym, rs2, relocs), nil } rs1, off := memFromOperandWithFrame(dst, fi) @@ -2497,6 +2516,23 @@ func encodeRISCVSBFPLoad(name string, rd int, double bool, relocs *[]Reloc) []by return append(wordLE(auipc), wordLE(fl)...) } +// riscvTLSBytes emits the toolchain's local-exec TLS sequence for an SB +// reference to a TLSBSS symbol: LUI TMP + ADDIW TMP (the 8-byte +// R_RISCV_TLS_LE field the linker patches as the offset from the thread +// pointer), ADD TMP, TP, TMP, then the access at zero offset through TMP. +func riscvTLSBytes(enc riscvEnc, store bool, reg int, sym *ast.Symbol, relocs *[]Reloc) []byte { + if relocs != nil { + *relocs = append(*relocs, Reloc{Off: 0, After: 8, Name: sym.Name, Kind: RelRISCVTLSLE, Addend: sym.Offset}) + } + out := wordLE(riscvUType(riscvEnc{0x37, 0x0, 0x00}, 31, 0)) // LUI X31, hi + out = append(out, wordLE(riscvIType(riscvEnc{0x1b, 0x0, 0x00}, 31, 31, 0))...) // ADDIW X31, X31, lo + out = append(out, wordLE(riscvRType(riscvInstrTable["ADD"], 31, 31, 4))...) // ADD X31, X31, X4(TP) + if store { + return append(out, wordLE(riscvSType(enc, 31, reg, 0))...) + } + return append(out, wordLE(riscvIType(enc, reg, 31, 0))...) +} + // RiscvLiteral is one pooled 64-bit constant: a MOV whose immediate sits // beyond both the 32-bit span and the shift sequences loads its bits from a // read-only data symbol named like the toolchain's $i64 pool. diff --git a/asm/riscv_compressed_test.go b/asm/riscv_compressed_test.go index bfdea4e..2c3fd3f 100644 --- a/asm/riscv_compressed_test.go +++ b/asm/riscv_compressed_test.go @@ -88,7 +88,7 @@ func TestRISCVCompressedRange(t *testing.T) { asmOne := func(t *testing.T, stmt string) error { t.Helper() fn := firstTextRISCV(t, "#include \"textflag.h\"\nTEXT ·c(SB), NOSPLIT, $0\n\t"+stmt+"\n\tRET\n") - _, _, _, _, _, _, err := assembleRISCV(fn) + _, _, _, _, _, _, err := assembleRISCV(fn, nil) return err } for _, s := range []string{ diff --git a/asm/riscv_encode_test.go b/asm/riscv_encode_test.go index 7bad1a1..6f89efe 100644 --- a/asm/riscv_encode_test.go +++ b/asm/riscv_encode_test.go @@ -35,7 +35,7 @@ func firstTextRISCV(t *testing.T, src string) *ast.Text { // assembleRISCVHelper assembles one TEXT function and returns its code bytes. func assembleRISCVHelper(t *testing.T, fn *ast.Text) []byte { t.Helper() - code, _, _, _, _, _, err := assembleRISCV(fn) + code, _, _, _, _, _, err := assembleRISCV(fn, nil) if err != nil { t.Fatalf("assemble: %v", err) } @@ -132,7 +132,7 @@ TEXT ·wide(SB), NOSPLIT, $0 AUIPC $524288, X10 RET `) - if _, _, _, _, _, _, err := assembleRISCV(fn); err == nil { + if _, _, _, _, _, _, err := assembleRISCV(fn, nil); err == nil { t.Error("AUIPC $524288: expected the 20-bit range error, got none") } } @@ -868,7 +868,7 @@ TEXT ·argfp(SB), NOSPLIT, $0 MOV $arg(FP), X10 RET `) - code, _, _, _, _, _, err := assembleRISCV(fn) + code, _, _, _, _, _, err := assembleRISCV(fn, nil) if err != nil { t.Fatalf("assemble: %v", err) } @@ -891,7 +891,7 @@ TEXT ·book(SB), NOSPLIT, $0-8 MOV X10, ret+0(FP) RET `) - code, _, _, _, _, _, err := assembleRISCV(fn) + code, _, _, _, _, _, err := assembleRISCV(fn, nil) if err != nil { t.Fatalf("assemble: %v", err) } @@ -915,7 +915,7 @@ TEXT ·slots(SB), NOSPLIT, $0-0 JMP -3(PC) RET `) - code, _, _, _, _, _, err := assembleRISCV(fn) + code, _, _, _, _, _, err := assembleRISCV(fn, nil) if err != nil { t.Fatalf("assemble: %v", err) } @@ -943,7 +943,7 @@ TEXT ·wide(SB), NOSPLIT, $0-0 MOV $0x000fffffffffffda, X5 RET `) - code, _, _, _, _, _, err := assembleRISCV(fn) + code, _, _, _, _, _, err := assembleRISCV(fn, nil) if err != nil { t.Fatalf("assemble: %v", err) } @@ -1003,7 +1003,7 @@ TEXT ·calltest(SB), NOSPLIT, $0 CALL ext(SB) RET `) - code, _, relocs, _, _, _, err := assembleRISCV(fn) + code, _, relocs, _, _, _, err := assembleRISCV(fn, nil) if err != nil { t.Fatalf("assemble: %v", err) } @@ -1032,7 +1032,7 @@ TEXT ·calllocal(SB), NOSPLIT, $0 sub: RET `) - _, _, _, _, _, _, err := assembleRISCV(fn) + _, _, _, _, _, _, err := assembleRISCV(fn, nil) if err == nil { t.Error("expected error for CALL to local label, got nil") } @@ -1066,7 +1066,7 @@ func encodeOneInstrRISCV(t *testing.T, src string, pc int, offsets map[string]in t.Helper() fn := firstTextRISCV(t, "#include \"textflag.h\"\n"+src) instr := fn.Body[0].(*ast.Instr) - return encodeRISCVInstr(instr, pc, offsets, riscvFrameInfo{}, nil, nil, nil) + return encodeRISCVInstr(instr, pc, offsets, riscvFrameInfo{}, nil, nil, nil, nil) } // TestRISCVBranchJumpRange checks that displacements beyond the B-type span @@ -1115,7 +1115,7 @@ func TestRISCVBranchFarBody(t *testing.T) { } sb.WriteString("done:\n\tRET\n") fn := firstTextRISCV(t, sb.String()) - out, _, _, _, _, _, err := assembleRISCV(fn) + out, _, _, _, _, _, err := assembleRISCV(fn, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -1141,7 +1141,7 @@ TEXT ·csrhi(SB), NOSPLIT, $0 CSRRW $4096, X10, X11 RET `) - if _, _, _, _, _, _, err := assembleRISCV(fn); err == nil { + if _, _, _, _, _, _, err := assembleRISCV(fn, nil); err == nil { t.Error("expected an out-of-range error for CSR $4096, got none") } fn = firstTextRISCV(t, `#include "textflag.h" @@ -1149,7 +1149,7 @@ TEXT ·csrmax(SB), NOSPLIT, $0 CSRRW $4095, X10, X11 RET `) - if _, _, _, _, _, _, err := assembleRISCV(fn); err != nil { + if _, _, _, _, _, _, err := assembleRISCV(fn, nil); err != nil { t.Errorf("CSR $4095 must assemble: %v", err) } } @@ -1166,7 +1166,7 @@ func TestRISCV_Imm64Rejected(t *testing.T) { } for _, src := range cases { fn := firstTextRISCV(t, "#include \"textflag.h\"\nTEXT ·wide(SB), NOSPLIT, $0\n\t"+src+"\n\tRET\n") - if _, _, _, _, _, _, err := assembleRISCV(fn); err == nil { + if _, _, _, _, _, _, err := assembleRISCV(fn, nil); err == nil { t.Errorf("%s: expected an out-of-range error, got none", src) } } @@ -1179,7 +1179,7 @@ TEXT ·edge(SB), NOSPLIT, $0 SUB $0x80000000, X12, X13 RET `) - if _, _, _, _, _, _, err := assembleRISCV(fn); err != nil { + if _, _, _, _, _, _, err := assembleRISCV(fn, nil); err != nil { t.Errorf("int32-span immediates must assemble: %v", err) } // Beyond the span the MOV forms materialise the constant like the @@ -1189,7 +1189,7 @@ TEXT ·pool(SB), NOSPLIT, $0 MOV $0x123456789, X10 RET `) - if _, _, _, _, _, _, err := assembleRISCV(fn); err != nil { + if _, _, _, _, _, _, err := assembleRISCV(fn, nil); err != nil { t.Errorf("MOV with a 64-bit immediate must assemble: %v", err) } } @@ -1424,7 +1424,7 @@ func TestRISCV_rawDataRange(t *testing.T) { asmOne := func(t *testing.T, stmt string) ([]byte, error) { t.Helper() fn := firstTextRISCV(t, "#include \"textflag.h\"\nTEXT ·w(SB), NOSPLIT, $0\n\t"+stmt+"\n\tRET\n") - code, _, _, _, _, _, err := assembleRISCV(fn) + code, _, _, _, _, _, err := assembleRISCV(fn, nil) return code, err } t.Run("word bounds", func(t *testing.T) { @@ -1471,7 +1471,7 @@ func TestRISCV_shiftImmediateRange(t *testing.T) { asmOne := func(t *testing.T, stmt string) error { t.Helper() fn := firstTextRISCV(t, "#include \"textflag.h\"\nTEXT ·s(SB), NOSPLIT, $0\n\t"+stmt+"\n\tRET\n") - _, _, _, _, _, _, err := assembleRISCV(fn) + _, _, _, _, _, _, err := assembleRISCV(fn, nil) return err } for _, s := range []string{