fix(riscv64): compressed store offsets, FENCE and branch range checks
Assisted-by: GLM 5.3
This commit is contained in:
+135
-19
@@ -15,7 +15,10 @@ import (
|
||||
func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
|
||||
fi := riscvComputeFrame(t)
|
||||
prologue := riscvPrologue(fi)
|
||||
guardLen := riscvGuardLen(fi)
|
||||
guardLen, err := riscvGuardLen(fi)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
var relocs []Reloc
|
||||
var spadj []SpadjStep
|
||||
@@ -66,20 +69,20 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 4: recompute offsets with actual sizes.
|
||||
// Pass 4: recompute offsets with actual sizes. recs holds the
|
||||
// instructions in emission order, so an index into it walks t.Body in
|
||||
// lockstep (the same single pass Pass 1 uses) instead of rescanning the
|
||||
// whole slice per statement.
|
||||
offsets = map[string]int{}
|
||||
pos = guardLen + len(prologue)
|
||||
ri := 0
|
||||
for _, stmt := range t.Body {
|
||||
switch s := stmt.(type) {
|
||||
case *ast.Label:
|
||||
offsets[s.Name.Text] = pos
|
||||
case *ast.Instr:
|
||||
for _, r := range recs {
|
||||
if r.instr == s {
|
||||
pos += len(r.code)
|
||||
break
|
||||
}
|
||||
}
|
||||
pos += len(recs[ri].code)
|
||||
ri++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +92,10 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
||||
// the morestack block at the end of the function, which the previous
|
||||
// passes have sized.
|
||||
var out []byte
|
||||
guardBytes, guardReloc := riscvGuard(fi)
|
||||
guardBytes, guardReloc, err := riscvGuard(fi)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
if fi.needSplit {
|
||||
out = append(out, guardBytes...)
|
||||
}
|
||||
@@ -231,6 +237,25 @@ func isBranchLike(mnem string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// riscvCheckBranchOffset rejects a B-type displacement outside its signed
|
||||
// 13-bit span [-4096, 4094]; the encoder masks to 13 bits, so an
|
||||
// out-of-range offset would otherwise wrap to a wrong target.
|
||||
func riscvCheckBranchOffset(target string, off int32) error {
|
||||
if off < -4096 || off > 4094 {
|
||||
return fmt.Errorf("branch to %q too far (13-bit range)", target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// riscvCheckJumpOffset rejects a J-type displacement outside its signed
|
||||
// 21-bit span [-1048576, 1048574].
|
||||
func riscvCheckJumpOffset(target string, off int32) error {
|
||||
if off < -1048576 || off > 1048574 {
|
||||
return fmt.Errorf("jump to %q too far (21-bit range)", target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeRISCVInstr encodes a single RISC-V instruction.
|
||||
func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc) ([]byte, error) {
|
||||
mnem := instr.Mnemonic.Text
|
||||
@@ -304,6 +329,9 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
}
|
||||
offset := int32(targetOff - pc)
|
||||
if err := riscvCheckJumpOffset(target, offset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
word = riscvJType(0, offset)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
case "JAL":
|
||||
@@ -320,6 +348,9 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
}
|
||||
offset := int32(targetOff - pc)
|
||||
if err := riscvCheckJumpOffset(target, offset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
word = riscvJType(rd, offset)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
|
||||
@@ -365,6 +396,9 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
case "BGTZ":
|
||||
enc, rs1, rs2 = riscvEnc{0x63, 0x4, 0x00}, 0, rs // blt x0, rs
|
||||
}
|
||||
if err := riscvCheckBranchOffset(target, int32(targetOff-pc)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
word = riscvBType(enc, rs1, rs2, int32(targetOff-pc))
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
|
||||
@@ -374,7 +408,14 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported system instruction %q", mnem)
|
||||
}
|
||||
word = riscvIType(enc, 0, 0, 0)
|
||||
// The bare FENCE expands to fence iorw, iorw: the predecessor and
|
||||
// successor fields both carry 0xF in the I-type immediate
|
||||
// (the toolchain's encodeFenceOperand TYPE_NONE default).
|
||||
imm := int32(0)
|
||||
if mnem == "FENCE" {
|
||||
imm = 0x0FF
|
||||
}
|
||||
word = riscvIType(enc, 0, 0, imm)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
}
|
||||
|
||||
@@ -416,7 +457,10 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
csr := immFromOperand(ops[0]) // CSR address (12-bit)
|
||||
rd := regFromOperand(ops[2]) // destination register
|
||||
if csr < 0 || csr > 0xFFF {
|
||||
return nil, fmt.Errorf("%s: CSR address %d out of range 0-0xFFF", mnem, csr)
|
||||
}
|
||||
rd := regFromOperand(ops[2]) // destination register
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("invalid destination register in %s", mnem)
|
||||
}
|
||||
@@ -561,9 +605,9 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
// I-type with immediate: Plan 9 order is INSTR $imm, rs1, rd; the
|
||||
// two-operand form INSTR $imm, rd uses rd as the source.
|
||||
case len(ops) == 3 && isITypeInstr(mnem):
|
||||
imm := immFromOperand(ops[0]) // immediate
|
||||
if immNeg {
|
||||
imm = -imm // SUB $imm arrived through the ADDI alias
|
||||
imm, err := riscvImm32FromOperand(ops[0], immNeg) // immediate
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rs1 := regFromOperand(ops[1]) // source register
|
||||
rd := regFromOperand(ops[2]) // destination
|
||||
@@ -573,9 +617,9 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
return encodeRISCVItypeImmediate(mnem, enc, rd, rs1, imm)
|
||||
|
||||
case len(ops) == 2 && isITypeInstr(mnem):
|
||||
imm := immFromOperand(ops[0])
|
||||
if immNeg {
|
||||
imm = -imm
|
||||
imm, err := riscvImm32FromOperand(ops[0], immNeg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rd := regFromOperand(ops[1])
|
||||
if rd < 0 {
|
||||
@@ -614,6 +658,9 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
if rs1 < 0 || rs2 < 0 {
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
}
|
||||
if err := riscvCheckBranchOffset(target, offset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The Go assembler never compresses branches to C.BEQZ/C.BNEZ.
|
||||
word = riscvBType(enc, rs1, rs2, offset)
|
||||
@@ -695,7 +742,10 @@ func encodeRISCVMov(instr *ast.Instr, fi riscvFrameInfo, relocs *[]Reloc) ([]byt
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("MOV $imm: invalid destination register")
|
||||
}
|
||||
imm := immFromOperand(src)
|
||||
imm, err := riscvImm32FromOperand(src, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encodeRISCVLoadImm(rd, imm), nil
|
||||
}
|
||||
|
||||
@@ -1081,7 +1131,7 @@ func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
|
||||
// tryCompressRVC attempts to compress a RISC-V instruction to its 16-bit
|
||||
// RVC form. It returns the compressed instruction word and true on success.
|
||||
func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
mnem := instr.Mnemonic.Text
|
||||
mnem := riscvCompressMnem(instr)
|
||||
ops := instr.Operands
|
||||
|
||||
switch mnem {
|
||||
@@ -1331,6 +1381,49 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// riscvCompressMnem maps a MOV-family load or store onto the base mnemonic
|
||||
// the toolchain lowers it to (MOVW 4(SP), X9 is LW under another name), so
|
||||
// the width spellings compress exactly like their base forms. Register and
|
||||
// immediate forms keep their own mnemonic: the C.MV path matches "MOV" and
|
||||
// nothing else in the switch has a width case.
|
||||
func riscvCompressMnem(instr *ast.Instr) string {
|
||||
mnem := instr.Mnemonic.Text
|
||||
ops := instr.Operands
|
||||
if !strings.HasPrefix(mnem, "MOV") || len(ops) != 2 {
|
||||
return mnem
|
||||
}
|
||||
load := isMemOperand(ops[0]) && !isMemOperand(ops[1])
|
||||
store := !isMemOperand(ops[0]) && isMemOperand(ops[1])
|
||||
if !load && !store {
|
||||
return mnem
|
||||
}
|
||||
switch mnem {
|
||||
case "MOVW":
|
||||
if load {
|
||||
return "LW"
|
||||
}
|
||||
return "SW"
|
||||
case "MOVF":
|
||||
if load {
|
||||
return "FLW"
|
||||
}
|
||||
return "FSW"
|
||||
case "MOVD":
|
||||
if load {
|
||||
return "FLD"
|
||||
}
|
||||
return "FSD"
|
||||
case "MOV":
|
||||
if load {
|
||||
return "LD"
|
||||
}
|
||||
return "SD"
|
||||
}
|
||||
// MOVB/MOVBU/MOVH/MOVHU/MOVWU have no compressed form; their base
|
||||
// mnemonics (LB/LBU/LH/LHU/LWU, SB/SH) match no case either.
|
||||
return mnem
|
||||
}
|
||||
|
||||
// extractLDParams extracts rd, rs1, and immediate offset for a load instruction.
|
||||
func extractLDParams(instr *ast.Instr, fi riscvFrameInfo) (rd, rs1 int, imm int32) {
|
||||
ops := instr.Operands
|
||||
@@ -1507,6 +1600,29 @@ func immFromOperand(op *ast.Operand) int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// riscvImm32FromOperand reads an immediate for the MOV/I-type paths as a
|
||||
// signed 32-bit value. The toolchain materialises wider constants through
|
||||
// its SLLI expansion, which this assembler does not implement, so values
|
||||
// outside the int32 span are diagnosed instead of silently truncated (MOV
|
||||
// $0x123456789 must not assemble as $0x3456789). The neg flag carries the
|
||||
// SUB $imm alias, whose negated value may fit when the written one does not.
|
||||
func riscvImm32FromOperand(op *ast.Operand, neg bool) (int32, error) {
|
||||
var v int64
|
||||
if op.Imm.HasVal {
|
||||
v = op.Imm.Val
|
||||
if op.Imm.Neg {
|
||||
v = -v
|
||||
}
|
||||
}
|
||||
if neg {
|
||||
v = -v
|
||||
}
|
||||
if int64(int32(v)) != v {
|
||||
return 0, fmt.Errorf("immediate %d out of range; 64-bit materialisation not supported", v)
|
||||
}
|
||||
return int32(v), nil
|
||||
}
|
||||
|
||||
func memFromOperand(op *ast.Operand) (rs1 int, imm int32) {
|
||||
rs1 = riscvRegNum(op.Addr.Base)
|
||||
imm = int32(op.Addr.Offset)
|
||||
|
||||
Reference in New Issue
Block a user