diff --git a/asm/riscv_assemble.go b/asm/riscv_assemble.go index b790bcd..c9a1bb8 100644 --- a/asm/riscv_assemble.go +++ b/asm/riscv_assemble.go @@ -6,6 +6,7 @@ package asm import ( "errors" "fmt" + "math/bits" "slices" "strings" @@ -14,13 +15,14 @@ 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, error) { +func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, []RiscvLiteral, error) { fi := riscvComputeFrame(t) prologue := riscvPrologue(fi) guardLen, err := riscvGuardLen(fi) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, err } + lits := &riscvLiterals{} var relocs []Reloc var spadj []SpadjStep @@ -74,9 +76,9 @@ 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) // no relocs in Pass 2 + code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi, nil, nil, lits) // no relocs in Pass 2 if err != nil && !(branchLike && riscvIsRangeError(err)) { - return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err) + return nil, nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err) } if err != nil { code = make([]byte, 4) @@ -178,13 +180,20 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [ } } if !changed { - // Capture the final pcs for the N(PC) branch forms: their target - // is the instruction N source slots away, resolved by index. + // Capture the final pcs for the N(PC) branch and jump forms: the + // target is the instruction N source slots away (N=0 the branch + // itself, N negative backwards), resolved by index against the + // final layout. pcRelPcs = map[*ast.Instr]int{} for i := range recs { - if _, ok := riscvPCRelOffset(recs[i].instr); ok { - pcRelPcs[recs[i].instr] = pcs[i] + n, ok := riscvPCRelOffset(recs[i].instr) + if !ok { + continue } + if i+n < 0 || i+n >= len(recs) { + continue + } + pcRelPcs[recs[i].instr] = pcs[i+n] } break } @@ -198,7 +207,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [ var out []byte guardBytes, guardReloc, err := riscvGuard(fi) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, err } if fi.needSplit { out = append(out, guardBytes...) @@ -220,11 +229,11 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [ // The JMP a relaxation inserted: JAL X0 to the original target. targetOff, ok := offsets[r.jmpTo] if !ok { - return nil, nil, nil, nil, nil, fmt.Errorf("undefined label %q", r.jmpTo) + return nil, nil, nil, nil, nil, nil, fmt.Errorf("undefined label %q", r.jmpTo) } offset := int32(targetOff - pc) if err := riscvCheckJumpOffset(r.jmpTo, offset); err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, err } word := riscvJType(0, offset) code = []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)} @@ -233,7 +242,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [ // JMP, always the very next instruction (offset 4). enc, rs1, rs2, ok := riscvInvertedBranchEnc(strings.ToUpper(r.instr.Mnemonic.Text), r.instr.Operands) if !ok { - return nil, nil, nil, nil, nil, fmt.Errorf("%s: cannot relax branch", r.instr.Mnemonic.Text) + return nil, nil, nil, nil, nil, nil, fmt.Errorf("%s: cannot relax branch", r.instr.Mnemonic.Text) } word := riscvBType(enc, rs1, rs2, 4) code = []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)} @@ -241,9 +250,9 @@ 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) + code, err = encodeRISCVInstr(r.instr, pc, offsets, fi, &relocs, pcRelPcs, lits) if err != nil { - return nil, nil, nil, nil, nil, err + return nil, nil, nil, nil, nil, nil, err } if c16, ok := tryCompressRVC(r.instr, fi); ok { code = []byte{byte(c16), byte(c16 >> 8)} @@ -270,7 +279,7 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [ if fi.needSplit { relocs = append(relocs, guardReloc) } - return out, offsets, relocs, lines, spadj, nil + return out, offsets, relocs, lines, spadj, lits.list(), nil } // riscvImmAlias maps the R-type ALU mnemonics onto their I-type immediate @@ -348,6 +357,11 @@ func riscvPadBytes(pad int) []byte { func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int { mnem := instr.Mnemonic.Text ops := instr.Operands + mnem = riscvNormalisePseudo(mnem) + if mnem == "FUNCDATA" || mnem == "PCDATA" { + // The bookkeeping statements contribute no bytes. + return 0 + } var immNeg bool mnem, immNeg = riscvNormaliseImmAlias(mnem, ops) if mnem == "RET" { @@ -368,7 +382,25 @@ func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int { } // MOV $imm, rd → size depends on the immediate and RVC compression. if isImmOperand(ops[0]) && ops[0].Imm.Sym == nil { - return riscvMovImmSize(regFromOperand(ops[1]), immFromOperand(ops[0])) + imm := riscvOperandImm64(ops[0]) + if int64(int32(imm)) != imm { + return riscvMovImm64Size(regFromOperand(ops[1]), imm) + } + return riscvMovImmSize(regFromOperand(ops[1]), int32(imm)) + } + // MOV $sym+off(FP|SP), rd → the frame-adjusted offset as an ADDI, + // compressed like riscvSPAddiBytes encodes it. + if isImmOperand(ops[0]) && ops[0].Imm.Sym != nil && + (ops[0].Imm.Sym.Pseudo == "FP" || ops[0].Imm.Sym.Pseudo == "SP") { + rd := regFromOperand(ops[1]) + _, off := riscvResolvePseudo(ops[0].Imm.Sym, fi) + if rd > 0 && off == 0 { + return 2 // C.MV rd, SP + } + if isRVCIntReg(rd) && off > 0 && off < 1024 && off%4 == 0 { + return 2 // C.ADDI4SPN + } + return riscvItypeImmediateSize("ADDI", off) } // Frame-relative loads and stores: a frame offset beyond the signed // 12-bit range materialises the address in X31 first. @@ -664,9 +696,10 @@ 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) ([]byte, error) { +func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc, pcRelPcs map[*ast.Instr]int, lits *riscvLiterals) ([]byte, error) { mnem := instr.Mnemonic.Text ops := instr.Operands + mnem = riscvNormalisePseudo(mnem) var immNeg bool mnem, immNeg = riscvNormaliseImmAlias(mnem, ops) var word uint32 @@ -677,6 +710,22 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv // RET = epilogue (restore LR and close the frame when present) + // uncompressed JALR X0, 0(X1) (the toolchain never compresses RET). return riscvReturn(fi), nil + case "FUNCDATA": + // The assembler's bookkeeping statement, the expanded form of the + // GO_ARGS and NO_LOCAL_POINTERS macros: FUNCDATA $n, sym(SB) + // contributes no bytes, exactly as the toolchain's listing shows + // (the FUNCDATA entries and the instruction after them share a PC). + if len(ops) != 2 || !isImmOperand(ops[0]) { + return nil, fmt.Errorf("FUNCDATA expects $n, sym(SB)") + } + return nil, nil + case "PCDATA": + // The other bookkeeping statement, the expanded form of + // GO_RESULTS_INITIALIZED: PCDATA $n, $m contributes no bytes too. + if len(ops) != 2 || !isImmOperand(ops[0]) || !isImmOperand(ops[1]) { + return nil, fmt.Errorf("PCDATA expects $n, $m") + } + return nil, nil case "WORD": // WORD $w lays down a raw 32-bit little-endian word. if len(ops) != 1 { @@ -739,6 +788,22 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil } target = labelFromOperand(ops[0]) + // JMP N(PC): the PC-relative slot form, resolved like the + // branches (the toolchain counts source instructions at a + // uniform 4 bytes, so JMP 0(PC) is a self-loop and JMP -3(PC) + // reaches twelve bytes back). It must be recognised before the + // indirect-register form, whose operand it resembles. + if off, isPCRel, err := riscvPCRelTargetOff(instr, pc, pcRelPcs); isPCRel { + if err != nil { + return nil, err + } + offset := int32(off - pc) + if err := riscvCheckJumpOffset("", offset); err != nil { + return nil, err + } + word = riscvJType(0, offset) + return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil + } // JMP (X5): an indirect branch, the toolchain's JALR X0, 0(X5). if ops[0].Addr.Sym == nil && ops[0].Addr.Base != "" { if ops[0].Addr.Offset != 0 || ops[0].Addr.Index != "" { @@ -751,17 +816,6 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, rs1, 0) return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil } - if off, isPCRel, err := riscvPCRelTargetOff(instr, pc, pcRelPcs); isPCRel { - if err != nil { - return nil, err - } - offset := int32(off - pc) - if err := riscvCheckJumpOffset("", offset); err != nil { - return nil, err - } - word = riscvJType(0, offset) - return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil - } } targetOff, ok := offsets[target] if !ok { @@ -810,7 +864,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) + return encodeRISCVMov(instr, fi, relocs, lits) // JALR: indirect jump/call. Plan 9: JALR rs1, rd or JALR offset(rs1). case "JALR": @@ -1316,7 +1370,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) ([]byte, error) { +func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc, lits *riscvLiterals) ([]byte, error) { ops := instr.Operands if len(ops) != 2 { return nil, fmt.Errorf("MOV expects 2 operands, got %d", len(ops)) @@ -1335,8 +1389,21 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc) ([]byt } return encodeRISCVSBAddr(src.Imm.Sym, rd, relocs), nil } + // MOV $sym+off(FP|SP), rd: the address of a frame slot as an + // immediate is the frame-adjusted offset against the hardware SP, + // the toolchain's ADDI $adj, SP, rd (argframe+0(FP) in the runtime's + // reflect trampolines is the spelling). + if src.Imm.Sym != nil && (src.Imm.Sym.Pseudo == "FP" || src.Imm.Sym.Pseudo == "SP") { + rd := regFromOperand(dst) + if rd < 0 { + return nil, fmt.Errorf("MOV $%s(%s): invalid destination register", src.Imm.Sym.Name, src.Imm.Sym.Pseudo) + } + _, off := riscvResolvePseudo(src.Imm.Sym, fi) + return riscvSPAddiBytes(rd, off), nil + } // MOV $sym(FP/SP), rd, not supported: immediate symbol references - // other than SB cannot be encoded as a simple immediate. + // other than the frame pseudos cannot be encoded as a simple + // immediate. if src.Imm.Sym != nil && src.Imm.Sym.Pseudo != "" { return nil, fmt.Errorf("MOV $%s(%s): unsupported immediate symbol reference (only SB is supported)", src.Imm.Sym.Name, src.Imm.Sym.Pseudo) } @@ -1344,11 +1411,14 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc) ([]byt if rd < 0 { return nil, fmt.Errorf("MOV $imm: invalid destination register") } - imm, err := riscvImm32FromOperand(src, false) - if err != nil { - return nil, err + imm := riscvOperandImm64(src) + if int64(int32(imm)) != imm { + // Beyond the signed 32-bit span the toolchain either builds the + // value from a shifted 32-bit part or loads it from the pooled + // $i64 constant it synthesises for the purpose. + return riscvLoadImm64(rd, imm, lits, relocs), nil } - return encodeRISCVLoadImm(rd, imm), nil + return encodeRISCVLoadImm(rd, int32(imm)), nil } // Memory → register (load). @@ -1557,6 +1627,186 @@ func splitRISCV32Imm(imm int32) (low, high int32) { return low, high } +// riscvNormalisePseudo rewrites the toolchain's UNDEF spelling onto EBREAK: +// the assembler accepts UNDEF where the hardware wants the trap instruction +// and emits ebreak (compressed to C.EBREAK under RVC), so every pass sees the +// canonical name. +func riscvNormalisePseudo(mnem string) string { + if strings.EqualFold(mnem, "UNDEF") { + return "EBREAK" + } + return mnem +} + +// riscvOperandImm64 reads an immediate operand as a full signed 64-bit value, +// where immFromOperand would truncate to int32; the MOV immediate path uses +// it to classify the wide constants. +func riscvOperandImm64(op *ast.Operand) int64 { + if !op.Imm.HasVal { + return 0 + } + v := op.Imm.Val + if op.Imm.Neg { + v = -v + } + return v +} + +// riscvSplitShiftConst mirrors cmd/internal/obj/riscv's splitShiftConst: it +// looks for the signed 32-bit integer a constant can be rebuilt from with a +// left shift, a left-and-right shift pair (a run of ones), or a zero-extended +// 32-bit pattern. A constant that fits none of the shapes is materialised +// from the pooled $i64 data symbol instead. +func riscvSplitShiftConst(v int64) (imm int64, lsh int, rsh int, ok bool) { + // Rebuild from a signed 32-bit integer shifted left. + lsh = bits.TrailingZeros64(uint64(v)) + c := v >> lsh + if int64(int32(c)) == c { + return c, lsh, 0, true + } + + // Rebuild from a small negative constant: shift left into place, then + // shift the sign-extended ones run right. + rsh = bits.LeadingZeros64(uint64(v)) + ones := bits.OnesCount64((uint64(v) >> lsh) >> 11) + if rsh+ones+lsh+11 == 64 { + c = (1<<11 | ((v >> lsh) & 0x7ff)) << 52 >> 52 // sign extend 12 bits + if lsh > 0 || c != -1 { + lsh += rsh + } + return c, lsh, rsh, true + } + + // Rebuild from a zero-extended signed 32-bit integer. + if int64(uint32(c)) == c { + c = int64(int32(c)) + lsh, rsh = 32, 32-lsh + return c, lsh, rsh, true + } + + return 0, 0, 0, false +} + +// riscvSPAddiBytes encodes ADDI rd, SP, imm for the frame-address immediates +// (the MOV $sym+off(FP|SP) form), using the compressed forms the toolchain +// picks under RVC: C.ADDI4SPN for a positive 4-byte multiple that fits, C.MV +// for the zero offset, the plain ADDI otherwise. +func riscvSPAddiBytes(rd int, imm int32) []byte { + if rd != 0 && imm == 0 { + return word16(rvcCR(0x8, uint32(rd), 2)) // C.MV rd, SP + } + if isRVCIntReg(rd) && imm > 0 && imm < 1024 && imm%4 == 0 { + return word16(rvcCIW(0x0, rvcReg3(rd), uint32(imm))) + } + return wordLE(riscvIType(riscvInstrTable["ADDI"], rd, 2, imm)) +} + +// riscvMovImm64Size returns the encoded byte length of MOV $imm, rd when the +// immediate sits outside the signed 32-bit span: the shifted-part sequences +// of riscvLoadImm64, or the 8-byte AUIPC+LD pool load. +func riscvMovImm64Size(rd int, imm int64) int { + c, lsh, rsh, ok := riscvSplitShiftConst(imm) + if !ok { + return 8 // AUIPC + LD against the $i64 pool symbol + } + size := riscvMovImmSize(rd, int32(c)) + if lsh > 0 { + size += riscvShiftImmSize(rd, true) + } + if rsh > 0 { + size += riscvShiftImmSize(rd, false) + } + return size +} + +// riscvShiftImmSize returns the encoded size of one SLLI/SRLI expansion +// part: two bytes under RVC when the destination can carry a compressed +// shift (C.SLLI admits every register but X0, C.SRLI only X8 to X15), four +// otherwise. +func riscvShiftImmSize(rd int, left bool) int { + if rd != 0 && (left || isRVCIntReg(rd)) { + return 2 + } + return 4 +} + +// riscvLoadImm64 encodes MOV $imm, rd for an immediate beyond the signed +// 32-bit span, mirroring the toolchain's instructionsForMOVConst: when a +// shifted 32-bit part rebuilds the value it emits that part (compressed like +// any written MOV) followed by the SLLI and SRLI shifts; otherwise it loads +// the constant from the pooled read-only $i64. symbol via AUIPC + LD +// and registers the literal so the data section carries its bytes. +func riscvLoadImm64(rd int, imm int64, lits *riscvLiterals, relocs *[]Reloc) []byte { + c, lsh, rsh, ok := riscvSplitShiftConst(imm) + if !ok { + name := fmt.Sprintf("$i64.%016x", uint64(imm)) + if lits != nil { + lits.add(name, riscvLiteralBytes(imm)) + } + return encodeRISCVSBLoad(&ast.Symbol{Name: name}, rd, relocs) + } + out := encodeRISCVLoadImm(rd, int32(c)) + if lsh > 0 { + out = append(out, riscvShiftImmBytes(rd, lsh, true)...) + } + if rsh > 0 { + out = append(out, riscvShiftImmBytes(rd, rsh, false)...) + } + return out +} + +// riscvShiftImmBytes encodes one SLLI (left) or SRLI expansion part, using +// the compressed form the toolchain picks under RVC: C.SLLI admits every +// register but X0, C.SRLI only X8 to X15. +func riscvShiftImmBytes(rd, shamt int, left bool) []byte { + if rd != 0 && shamt >= 1 && shamt <= 63 && (left || isRVCIntReg(rd)) { + if left { + return word16(rvcSLLI(uint32(rd), uint32(shamt)&0x3F)) + } + return word16(rvcCBShift(0x0, rvcReg3(rd), uint32(shamt)&0x3F)) + } + enc := riscvEnc{0x13, 0x1, 0x00} // SLLI + imm := int32(shamt) + if !left { + enc = riscvEnc{0x13, 0x5, 0x00} // SRLI: funct6 000000, funct3 101 + } + return wordLE(riscvIType(enc, rd, rd, imm)) +} + +// riscvLiteralBytes renders a 64-bit constant as the little-endian bytes the +// $i64 pool symbol holds. +func riscvLiteralBytes(v int64) []byte { + return []byte{byte(v), byte(v >> 8), byte(v >> 16), byte(v >> 24), + byte(v >> 32), byte(v >> 40), byte(v >> 48), byte(v >> 56)} +} + +// 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. +type RiscvLiteral struct { + Name string + Data []byte +} + +// riscvLiterals collects the pooled constants the MOV expansions refer to, +// deduplicated by name, in first-use order. +type riscvLiterals struct { + order []RiscvLiteral + seen map[string]bool +} + +func (l *riscvLiterals) add(name string, data []byte) { + if l.seen == nil { + l.seen = map[string]bool{} + } + if !l.seen[name] { + l.seen[name] = true + l.order = append(l.order, RiscvLiteral{Name: name, Data: data}) + } +} + +func (l *riscvLiterals) list() []RiscvLiteral { return l.order } + // encodeRISCVItypeImmediate encodes an I-type arithmetic instruction, expanding // large immediates for ADDI/ANDI/ORI/XORI into LUI+ADDIW+op (or two ADDIs for // ADDI), matching the Go assembler. @@ -1734,6 +1984,7 @@ func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) { // RVC form. It returns the compressed instruction word and true on success. func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) { mnem := riscvCompressMnem(instr) + mnem = riscvNormalisePseudo(mnem) ops := instr.Operands // The immediate aliases fold onto their I-type mnemonics before // compression: the toolchain compresses ADD $imm, rd as c.addi, exactly diff --git a/asm/riscv_encode.go b/asm/riscv_encode.go index 53ae999..084c23d 100644 --- a/asm/riscv_encode.go +++ b/asm/riscv_encode.go @@ -63,7 +63,7 @@ func riscvRegNum(name string) int { return 24 case "X25", "S9": return 25 - case "X26", "S10": + case "X26", "S10", "CTXT": return 26 case "X27", "S11", "g": return 27 diff --git a/asm/riscv_encode_test.go b/asm/riscv_encode_test.go index 757f440..230f4b2 100644 --- a/asm/riscv_encode_test.go +++ b/asm/riscv_encode_test.go @@ -33,7 +33,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) if err != nil { t.Fatalf("assemble: %v", err) } @@ -785,16 +785,140 @@ TEXT ·sys(SB), NOSPLIT, $0 } } -func TestRISCV_MOV_sym_FP_error(t *testing.T) { - // MOV $sym(FP), rd should return an error (unsupported). +func TestRISCV_MOV_sym_FP(t *testing.T) { + // MOV $sym(FP), rd lowers to the frame-adjusted ADDI against SP: the + // toolchain's argframe spelling. A zero frame leaves the offset at the + // 8-byte link slot, compressed to C.ADDI4SPN. fn := firstTextRISCV(t, `#include "textflag.h" -TEXT ·badfp(SB), NOSPLIT, $0 +TEXT ·argfp(SB), NOSPLIT, $0 MOV $arg(FP), X10 RET `) - _, _, _, _, _, err := assembleRISCV(fn) - if err == nil { - t.Error("expected error for MOV $arg(FP), got nil") + code, _, _, _, _, _, err := assembleRISCV(fn) + if err != nil { + t.Fatalf("assemble: %v", err) + } + // prologue (0: leaf, zero frame) + C.ADDI4SPN (2) + RET (4) = 6 + want := []byte{0x28, 0x00, 0x67, 0x80, 0x00, 0x00} + if string(code) != string(want) { + t.Errorf("got % x, want % x", code, want) + } +} + +func TestRISCV_Bookkeeping(t *testing.T) { + // FUNCDATA and PCDATA contribute no bytes; UNDEF is the toolchain's + // ebreak, compressed to C.EBREAK under RVC. + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·book(SB), NOSPLIT, $0-8 + FUNCDATA $0, marks<>(SB) + PCDATA $1, $1 + UNDEF + MOV $1, X10 + MOV X10, ret+0(FP) + RET +`) + code, _, _, _, _, _, err := assembleRISCV(fn) + if err != nil { + t.Fatalf("assemble: %v", err) + } + // C.EBREAK (2) + C.LI X10, 1 (2) + C.SWSP (2) + RET (4) = 10: the + // FUNCDATA and PCDATA statements contribute nothing. + want := []byte{0x02, 0x90, 0x05, 0x45, 0x2a, 0xe4, 0x67, 0x80, 0x00, 0x00} + if string(code) != string(want) { + t.Errorf("got % x, want % x", code, want) + } +} + +func TestRISCV_JMPPCRel(t *testing.T) { + // JMP N(PC): the displacement tracks the instruction N source slots + // away in the final layout (0 the jump itself, negative backwards). + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·slots(SB), NOSPLIT, $0-0 + JMP 2(PC) + MOV $1, X11 + MOV $2, X12 + MOV X12, X11 + JMP -3(PC) + RET +`) + code, _, _, _, _, _, err := assembleRISCV(fn) + if err != nil { + t.Fatalf("assemble: %v", err) + } + // JMP 2(PC) lands on the C.MV six bytes ahead; JMP -3(PC) lands back on + // the first C.LI, six bytes behind. + want := []byte{ + 0x6f, 0x00, 0x60, 0x00, // JAL X0, 6 + 0x85, 0x45, // C.LI X11, 1 + 0x09, 0x46, // C.LI X12, 2 + 0xb2, 0x85, // C.MV X11, X12 + 0x6f, 0xf0, 0xbf, 0xff, // JAL X0, -6 + 0x67, 0x80, 0x00, 0x00, // RET + } + if string(code) != string(want) { + t.Errorf("got % x, want % x", code, want) + } +} + +func TestRISCV_MOVWideImm(t *testing.T) { + // Shift-sequence constants compress like the toolchain's expansion. + fn := firstTextRISCV(t, `#include "textflag.h" +TEXT ·wide(SB), NOSPLIT, $0-0 + MOV $0x8000000000000000, X5 + MOV $0x100000000, X5 + MOV $0x000fffffffffffda, X5 + RET +`) + code, _, _, _, _, _, err := assembleRISCV(fn) + if err != nil { + t.Fatalf("assemble: %v", err) + } + // C.LI -1, C.SLLI 63; C.LI 1, C.SLLI 32; C.LI -19, C.SLLI 13, SRLI 12. + want := []byte{ + 0xfd, 0x52, 0xfe, 0x12, + 0x85, 0x42, 0x82, 0x12, + 0xb5, 0x52, 0xb6, 0x02, 0x93, 0xd2, 0xc2, 0x00, + 0x67, 0x80, 0x00, 0x00, + } + if string(code) != string(want) { + t.Errorf("got % x, want % x", code, want) + } +} + +func TestRISCV_MOVImmPool(t *testing.T) { + // A constant outside the shift shapes loads from the pooled $i64 data + // symbol via AUIPC+LD, named like the toolchain's pool. + src := `#include "textflag.h" +TEXT ·pool(SB), NOSPLIT, $0-8 + MOV $0x0101010101010101, X16 + MOV X16, ret+0(FP) + RET +` + f, errs := parser.Parse("pool_riscv64.s", src) + if len(errs) > 0 { + t.Fatalf("parse: %v", errs) + } + img, err := AssembleFileRISCV(f) + if err != nil { + t.Fatalf("AssembleFileRISCV: %v", err) + } + // AUIPC X16, 0 + LD X16, 0(X16): the relocation pair carries the symbol. + wantCode := []byte{0x17, 0x08, 0x00, 0x00, 0x03, 0x38, 0x08, 0x00} + if string(img.Code[0:8]) != string(wantCode) { + t.Errorf("pool load: got % x", img.Code[0:8]) + } + var lit *DataSymbol + for i := range img.DataSyms { + if img.DataSyms[i].Name == "$i64.0101010101010101" { + lit = &img.DataSyms[i] + } + } + if lit == nil { + t.Fatalf("pool symbol missing: %v", img.DataSyms) + } + wantData := []byte{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01} + if string(img.Data[lit.Offset:lit.Offset+8]) != string(wantData) { + t.Errorf("pool bytes: got % x", img.Data[lit.Offset:lit.Offset+8]) } } @@ -805,7 +929,7 @@ TEXT ·calltest(SB), NOSPLIT, $0 CALL ext(SB) RET `) - code, _, relocs, _, _, err := assembleRISCV(fn) + code, _, relocs, _, _, _, err := assembleRISCV(fn) if err != nil { t.Fatalf("assemble: %v", err) } @@ -834,7 +958,7 @@ TEXT ·calllocal(SB), NOSPLIT, $0 sub: RET `) - _, _, _, _, _, err := assembleRISCV(fn) + _, _, _, _, _, _, err := assembleRISCV(fn) if err == nil { t.Error("expected error for CALL to local label, got nil") } @@ -868,7 +992,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) + return encodeRISCVInstr(instr, pc, offsets, riscvFrameInfo{}, nil, nil, nil) } // TestRISCVBranchJumpRange checks that displacements beyond the B-type span @@ -917,7 +1041,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) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -943,7 +1067,7 @@ TEXT ·csrhi(SB), NOSPLIT, $0 CSRRW $4096, X10, X11 RET `) - if _, _, _, _, _, err := assembleRISCV(fn); err == nil { + if _, _, _, _, _, _, err := assembleRISCV(fn); err == nil { t.Error("expected an out-of-range error for CSR $4096, got none") } fn = firstTextRISCV(t, `#include "textflag.h" @@ -951,25 +1075,24 @@ TEXT ·csrmax(SB), NOSPLIT, $0 CSRRW $4095, X10, X11 RET `) - if _, _, _, _, _, err := assembleRISCV(fn); err != nil { + if _, _, _, _, _, _, err := assembleRISCV(fn); err != nil { t.Errorf("CSR $4095 must assemble: %v", err) } } // TestRISCV_Imm64Rejected checks that immediates outside the signed 32-bit -// span are diagnosed instead of silently truncated to their low 32 bits (the -// toolchain materialises such constants via SLLI expansion, which this -// assembler does not implement). +// span are diagnosed instead of silently truncated to their low 32 bits for +// the I-type arithmetic; the MOV forms materialise the wide constant instead +// (shift sequence or pooled load), like the toolchain. func TestRISCV_Imm64Rejected(t *testing.T) { cases := []string{ - "MOV $0x123456789, X10", "ADDI $0x100000000, X10, X11", "ANDI $-0x800000001, X10, X11", "SUB $0x100000000, X10, X11", } 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); err == nil { t.Errorf("%s: expected an out-of-range error, got none", src) } } @@ -982,9 +1105,19 @@ TEXT ·edge(SB), NOSPLIT, $0 SUB $0x80000000, X12, X13 RET `) - if _, _, _, _, _, err := assembleRISCV(fn); err != nil { + if _, _, _, _, _, _, err := assembleRISCV(fn); err != nil { t.Errorf("int32-span immediates must assemble: %v", err) } + // Beyond the span the MOV forms materialise the constant like the + // toolchain instead of diagnosing it. + fn = firstTextRISCV(t, `#include "textflag.h" +TEXT ·pool(SB), NOSPLIT, $0 + MOV $0x123456789, X10 + RET +`) + if _, _, _, _, _, _, err := assembleRISCV(fn); err != nil { + t.Errorf("MOV with a 64-bit immediate must assemble: %v", err) + } } // riscvWants decodes code as little-endian words and pins each one; the diff --git a/cmd/gasm/audit.go b/cmd/gasm/audit.go index 33ae9c1..f3e4b69 100644 --- a/cmd/gasm/audit.go +++ b/cmd/gasm/audit.go @@ -475,8 +475,8 @@ type corpusStats struct { // set, even when gasm does not support the architecture. var goPortSuffixes = []string{ "386", "amd64", "arm", "arm64", "loong64", "mips", "mips64", - "mips64le", "mipsle", "ppc64", "ppc64le", "riscv", "riscv64", - "s390x", "wasm", + "mips64le", "mipsle", "mips64x", "mipsx", "ppc64", "ppc64le", + "ppc64x", "riscv", "riscv64", "s390x", "wasm", } // otherPortFile reports whether the file belongs to a build no supported diff --git a/testdata/verify/bookkeep_riscv64.s b/testdata/verify/bookkeep_riscv64.s new file mode 100644 index 0000000..e2c0f3b --- /dev/null +++ b/testdata/verify/bookkeep_riscv64.s @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +// Differential kernel for the riscv64 bookkeeping statements and the +// slot-relative branches: FUNCDATA and PCDATA (the expanded forms of the +// funcdata.h macros, contributing no bytes), UNDEF (the toolchain's ebreak), +// and the JMP N(PC) slot jumps including the self-loop and the backward form. + +#include "textflag.h" + +TEXT ·bookkeep(SB), NOSPLIT, $8-8 + FUNCDATA $1, marks<>(SB) + PCDATA $1, $-1 + MOV ZERO, ret+0(FP) + PCDATA $1, $1 + UNDEF + MOV $1, X10 + RET + +TEXT ·slots(SB), NOSPLIT, $0-0 + MOV $1, X10 + JMP 2(PC) + MOV $64, X11 + MOV $128, X12 + MOV $2, X11 + MOV $3, X12 + BEQ X10, X11, skip + JMP -2(PC) + +skip: + JMP 0(PC) + +TEXT ·marksreader(SB), NOSPLIT, $0-8 + MOV $marks<>(SB), X10 + MOV (X10), X11 + MOV X11, ret+0(FP) + RET + +GLOBL marks<>(SB), RODATA, $8 +DATA marks<>+0(SB)/8, $1234605616436508552 diff --git a/testdata/verify/branch2_loong64.s b/testdata/verify/branch2_loong64.s new file mode 100644 index 0000000..df78506 --- /dev/null +++ b/testdata/verify/branch2_loong64.s @@ -0,0 +1,21 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +// Differential kernel for the loong64 two-operand BEQ/BNE spellings the +// msan trampolines use: BEQ Rj, target compares against R0 (the beqz form). + +#include "textflag.h" + +TEXT ·branch2(SB), NOSPLIT, $0-8 + MOVV arg+0(FP), R4 + BEQ R4, zero + ADDV $1, R4, R4 + +zero: + MOVV $16, R5 + BNE R4, done + ADDV $2, R4, R4 + +done: + MOVV R4, ret+0(FP) + RET diff --git a/testdata/verify/datainit_loong64.s b/testdata/verify/datainit_loong64.s new file mode 100644 index 0000000..9019240 --- /dev/null +++ b/testdata/verify/datainit_loong64.s @@ -0,0 +1,27 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +// Differential kernel for the loong64 DATA value forms the runtime's exp and +// asm files use: floating-point initialisers stored as IEEE-754 bits and +// string initialisers zero-padded within their declared width. + +#include "textflag.h" + +TEXT ·floatbits(SB), NOSPLIT, $0-8 + MOVV $floats<>(SB), R12 + MOVD 8(R12), F0 + MOVD F0, ret+0(FP) + RET + +TEXT ·stringhead(SB), NOSPLIT, $0-8 + MOVV $msg<>(SB), R12 + MOVV (R12), R13 + MOVV R13, ret+0(FP) + RET + +GLOBL floats<>(SB), RODATA, $16 +DATA floats<>+0(SB)/8, $0.0 +DATA floats<>+8(SB)/8, $0.5 + +GLOBL msg<>(SB), RODATA, $20 +DATA msg<>+0(SB)/20, $"call frame too large" diff --git a/testdata/verify/wideimm_riscv64.s b/testdata/verify/wideimm_riscv64.s new file mode 100644 index 0000000..722a5bf --- /dev/null +++ b/testdata/verify/wideimm_riscv64.s @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +// Differential kernel for the riscv64 wide MOV immediates: the 64-bit +// constants the toolchain materialises through shifted 32-bit parts or loads +// from its pooled $i64 data symbols, and the frame-address immediates +// ($name+off(FP)) lowered to ADDI against the hardware stack pointer. + +#include "textflag.h" + +TEXT ·poolload(SB), NOSPLIT, $0-16 + MOV arg+0(FP), X10 + MOV $0x0101010101010101, X16 + MUL X10, X16, X17 + MOV $0x123456789, X18 + ADD X18, X17, X17 + MOV X17, ret+8(FP) + RET + +TEXT ·shiftload(SB), NOSPLIT, $0-16 + MOV arg+0(FP), X10 + MOV $0x8000000000000000, X20 + MOV $0x100000000, X21 + ADD X21, X20, X20 + MOV $0x000fffffffffffda, X22 + XOR X22, X20, X20 + ADD X20, X10, X10 + MOV X10, ret+8(FP) + RET + +TEXT ·frameaddr(SB), NOSPLIT, $32-16 + MOV $ret+8(FP), X12 + MOV $scratch+0(FP), X13 + ADD X12, X13, X13 + MOV $slot+16(SP), X14 + SUB X14, X13, X13 + MOV X13, ret+8(FP) + RET + +GLOBL scratch<>(SB), NOPTR, $8 diff --git a/verify/l64_groundtruth_test.go b/verify/l64_groundtruth_test.go index 5ff8897..ac6bb7e 100644 --- a/verify/l64_groundtruth_test.go +++ b/verify/l64_groundtruth_test.go @@ -31,6 +31,8 @@ func TestGroundTruthLOONG64(t *testing.T) { "../testdata/verify/vector_loong64.s", "../testdata/verify/pcalign_loong64.s", "../testdata/verify/l64forms_loong64.s", + "../testdata/verify/datainit_loong64.s", + "../testdata/verify/branch2_loong64.s", "../testdata/verify/vector_arith_add_loong64.s", "../testdata/verify/vector_arith_sub_loong64.s", "../testdata/verify/vector_arith_sat_loong64.s", diff --git a/verify/riscv_groundtruth_test.go b/verify/riscv_groundtruth_test.go index 8e067ac..b13e94d 100644 --- a/verify/riscv_groundtruth_test.go +++ b/verify/riscv_groundtruth_test.go @@ -35,6 +35,8 @@ func TestGroundTruthRISCV(t *testing.T) { "../testdata/verify/bitmanip_riscv64.s", "../testdata/verify/pcalign_riscv64.s", "../testdata/verify/branch_far_riscv64.s", + "../testdata/verify/wideimm_riscv64.s", + "../testdata/verify/bookkeep_riscv64.s", "trampoline_riscv64.s", } { t.Run(path, func(t *testing.T) {