fix(asm): materialise large RISC-V immediates
Assisted-by: DeepSeek V4 Pro
This commit is contained in:
@@ -61,6 +61,13 @@ Unreleased changes on the `development` branch.
|
|||||||
`C.LDSP`/`C.SDSP`), plus `C.ADDI4SPN`, `C.ADDW` and `C.SUBW`. The
|
`C.LDSP`/`C.SDSP`), plus `C.ADDI4SPN`, `C.ADDW` and `C.SUBW`. The
|
||||||
byte-exact ground-truth test exercises these against `GOARCH=riscv64
|
byte-exact ground-truth test exercises these against `GOARCH=riscv64
|
||||||
go tool asm`.
|
go tool asm`.
|
||||||
|
- **RISC-V large-immediate materialisation.** `ADDI`/`ANDI`/`ORI`/`XORI`
|
||||||
|
with a 32-bit immediate that does not fit 12 bits now expand exactly as
|
||||||
|
`cmd/asm`: two `ADDI`s for the small `ADDI` split range, and
|
||||||
|
`LUI`+`ADDIW`+`<op>` otherwise, with the `LUI` and `ADDIW` compressed to
|
||||||
|
`C.LUI`/`C.ADDIW` when their immediate fits six signed bits. The
|
||||||
|
byte-exact ground-truth test covers positive, negative, and out-of-range
|
||||||
|
immediates against `GOARCH=riscv64 go tool asm`.
|
||||||
- **Debugger watchpoint slots.** `gasm debug`'s `watch` command always used
|
- **Debugger watchpoint slots.** `gasm debug`'s `watch` command always used
|
||||||
hardware watchpoint slot 0, so a second `watch` call silently overwrote
|
hardware watchpoint slot 0, so a second `watch` call silently overwrote
|
||||||
the first. Watchpoint slots are now tracked in the `Session` (DR0–DR3);
|
the first. Watchpoint slots are now tracked in the `Session` (DR0–DR3);
|
||||||
|
|||||||
+117
-2
@@ -150,6 +150,10 @@ func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// I-type arithmetic with a large immediate expands to several instructions.
|
||||||
|
if (mnem == "ADDI" || mnem == "ANDI" || mnem == "ORI" || mnem == "XORI") && len(ops) >= 1 && isImmOperand(ops[0]) {
|
||||||
|
return riscvItypeImmediateSize(mnem, immFromOperand(ops[0]))
|
||||||
|
}
|
||||||
return 4
|
return 4
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,7 +451,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
|||||||
if rd < 0 || rs1 < 0 {
|
if rd < 0 || rs1 < 0 {
|
||||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||||
}
|
}
|
||||||
word = riscvIType(enc, rd, rs1, imm)
|
return encodeRISCVItypeImmediate(mnem, enc, rd, rs1, imm)
|
||||||
|
|
||||||
case len(ops) == 2 && isITypeInstr(mnem):
|
case len(ops) == 2 && isITypeInstr(mnem):
|
||||||
imm := immFromOperand(ops[0])
|
imm := immFromOperand(ops[0])
|
||||||
@@ -455,7 +459,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
|||||||
if rd < 0 {
|
if rd < 0 {
|
||||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||||
}
|
}
|
||||||
word = riscvIType(enc, rd, rd, imm)
|
return encodeRISCVItypeImmediate(mnem, enc, rd, rd, imm)
|
||||||
|
|
||||||
// Loads: rd, offset(rs1) — Plan 9 order is LD src, dst.
|
// Loads: rd, offset(rs1) — Plan 9 order is LD src, dst.
|
||||||
case len(ops) == 2 && isLoadInstr(mnem):
|
case len(ops) == 2 && isLoadInstr(mnem):
|
||||||
@@ -654,6 +658,112 @@ func encodeRISCVLoadImm(rd int, imm int32) []byte {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// splitRISCV32Imm splits a signed 32-bit immediate into a signed 12-bit low
|
||||||
|
// part and a signed 20-bit high part, mirroring cmd/internal/obj/riscv's
|
||||||
|
// Split32BitImmediate. The high part is returned unshifted; callers place it
|
||||||
|
// in the upper bits of LUI (or its compressed C.LUI form).
|
||||||
|
func splitRISCV32Imm(imm int32) (low, high int32) {
|
||||||
|
if imm >= -2048 && imm <= 2047 {
|
||||||
|
return imm, 0
|
||||||
|
}
|
||||||
|
h := int64(imm) >> 12
|
||||||
|
if imm&(1<<11) != 0 {
|
||||||
|
h++
|
||||||
|
}
|
||||||
|
low = int32((int64(imm) << 52) >> 52) // sign extend 12 bits
|
||||||
|
high = int32((h << 44) >> 44) // sign extend 20 bits
|
||||||
|
return low, high
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func encodeRISCVItypeImmediate(mnem string, enc riscvEnc, rd, rs1 int, imm int32) ([]byte, error) {
|
||||||
|
if imm >= -2048 && imm <= 2047 {
|
||||||
|
return wordLE(riscvIType(enc, rd, rs1, imm)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var opMn string
|
||||||
|
switch mnem {
|
||||||
|
case "ADDI":
|
||||||
|
opMn = "ADD"
|
||||||
|
case "ANDI":
|
||||||
|
opMn = "AND"
|
||||||
|
case "ORI":
|
||||||
|
opMn = "OR"
|
||||||
|
case "XORI":
|
||||||
|
opMn = "XOR"
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%s: immediate %d does not fit 12 bits", mnem, imm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADDI with a small-ish immediate splits into two ADDIs.
|
||||||
|
if mnem == "ADDI" && imm >= -4096 && imm < 4095 {
|
||||||
|
imm0 := imm / 2
|
||||||
|
imm1 := imm - imm0
|
||||||
|
var out []byte
|
||||||
|
out = append(out, wordLE(riscvIType(enc, rd, rs1, imm0))...)
|
||||||
|
out = append(out, wordLE(riscvIType(enc, rd, rd, imm1))...)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LUI $high, TMP; [ADDIW $low, TMP, TMP]; op TMP, rs1, rd. The LUI and
|
||||||
|
// ADDIW compress to their RVC forms (C.LUI / C.ADDIW) when the immediate
|
||||||
|
// fits 6 signed bits, matching the toolchain's compress pass.
|
||||||
|
low, high := splitRISCV32Imm(imm)
|
||||||
|
tmp := 31 // X31 = T6 = TMP
|
||||||
|
var out []byte
|
||||||
|
if high != 0 && high >= -32 && high <= 31 {
|
||||||
|
out = append(out, word16(rvcCI(0x3, uint32(tmp), uint32(high)&0x3F))...)
|
||||||
|
} else {
|
||||||
|
out = append(out, wordLE(riscvUType(riscvEnc{0x37, 0x0, 0x00}, tmp, high<<12))...)
|
||||||
|
}
|
||||||
|
if low != 0 {
|
||||||
|
if low >= -32 && low <= 31 {
|
||||||
|
out = append(out, word16(rvcCI(0x1, uint32(tmp), uint32(low)&0x3F))...)
|
||||||
|
} else {
|
||||||
|
out = append(out, wordLE(riscvIType(riscvEnc{0x1B, 0x0, 0x00}, tmp, tmp, low))...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opEnc, ok := riscvInstrTable[opMn]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s: unsupported operation %q", mnem, opMn)
|
||||||
|
}
|
||||||
|
out = append(out, wordLE(riscvRType(opEnc, rd, rs1, tmp))...)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// riscvItypeImmediateSize returns the encoded byte length of an I-type
|
||||||
|
// immediate instruction, accounting for the large-immediate expansion.
|
||||||
|
func riscvItypeImmediateSize(mnem string, imm int32) int {
|
||||||
|
if imm >= -2048 && imm <= 2047 {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
switch mnem {
|
||||||
|
case "ADDI", "ANDI", "ORI", "XORI":
|
||||||
|
default:
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
if mnem == "ADDI" && imm >= -4096 && imm < 4095 {
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
low, high := splitRISCV32Imm(imm)
|
||||||
|
size := 4 // the R-type op (TMP is X31, never compressed)
|
||||||
|
if high != 0 && high >= -32 && high <= 31 {
|
||||||
|
size += 2 // C.LUI
|
||||||
|
} else {
|
||||||
|
size += 4 // LUI
|
||||||
|
}
|
||||||
|
if low != 0 {
|
||||||
|
if low >= -32 && low <= 31 {
|
||||||
|
size += 2 // C.ADDIW
|
||||||
|
} else {
|
||||||
|
size += 4 // ADDIW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
// encodeRISCVSBAddr emits AUIPC + ADDI to load the address of a static
|
// encodeRISCVSBAddr emits AUIPC + ADDI to load the address of a static
|
||||||
// symbol into rd, recording the single R_RISCV_PCREL_ITYPE relocation the Go
|
// symbol into rd, recording the single R_RISCV_PCREL_ITYPE relocation the Go
|
||||||
// toolchain uses for the pair (the object-file emitters expand or map it).
|
// toolchain uses for the pair (the object-file emitters expand or map it).
|
||||||
@@ -700,6 +810,11 @@ func wordLE(w uint32) []byte {
|
|||||||
return []byte{byte(w), byte(w >> 8), byte(w >> 16), byte(w >> 24)}
|
return []byte{byte(w), byte(w >> 8), byte(w >> 16), byte(w >> 24)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// word16 encodes a uint16 as 2 little-endian bytes.
|
||||||
|
func word16(w uint16) []byte {
|
||||||
|
return []byte{byte(w), byte(w >> 8)}
|
||||||
|
}
|
||||||
|
|
||||||
// encodeRISCVJALR encodes the JALR indirect jump/call instruction.
|
// encodeRISCVJALR encodes the JALR indirect jump/call instruction.
|
||||||
// Plan 9: JALR rs1, rd (2 regs) or JALR offset(rs1) (memory → rd=X1).
|
// Plan 9: JALR rs1, rd (2 regs) or JALR offset(rs1) (memory → rd=X1).
|
||||||
func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
|
func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
|
||||||
|
|||||||
Vendored
+12
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
TEXT ·largeimm(SB), NOSPLIT, $0
|
||||||
|
ADDI $2048, X5
|
||||||
|
ADDI $4095, X5, X6
|
||||||
|
ANDI $4095, X5, X6
|
||||||
|
ORI $-4096, X5, X6
|
||||||
|
XORI $0x12345, X5, X6
|
||||||
|
RET
|
||||||
@@ -21,6 +21,7 @@ func TestGroundTruthRISCV(t *testing.T) {
|
|||||||
"../testdata/verify/basic_riscv64.s",
|
"../testdata/verify/basic_riscv64.s",
|
||||||
"../testdata/verify/rvc_riscv64.s",
|
"../testdata/verify/rvc_riscv64.s",
|
||||||
"../testdata/verify/loadstore_riscv64.s",
|
"../testdata/verify/loadstore_riscv64.s",
|
||||||
|
"../testdata/verify/largeimm_riscv64.s",
|
||||||
} {
|
} {
|
||||||
t.Run(path, func(t *testing.T) {
|
t.Run(path, func(t *testing.T) {
|
||||||
testGroundTruthRISCVFile(t, path)
|
testGroundTruthRISCVFile(t, path)
|
||||||
|
|||||||
Reference in New Issue
Block a user