feat(riscv64,loong64): PCALIGN, branch relaxation and operand shapes
Assisted-by: GLM 5.3 Flash
This commit is contained in:
+564
-45
@@ -4,7 +4,9 @@
|
||||
package asm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
@@ -31,32 +33,54 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
||||
}
|
||||
|
||||
// Pass 1: collect instructions and compute label offsets assuming 4 bytes
|
||||
// per instruction (or 8 for MOV $large-imm). No encoding yet.
|
||||
// per instruction (or 8 for MOV $large-imm). No encoding yet. PCALIGN
|
||||
// contributes only its padding, which is attached to the following
|
||||
// instruction and emitted ahead of it. A relaxed branch carries the
|
||||
// inverted condition and is followed by an inserted JMP rec (jmpTo set)
|
||||
// that carries the original target.
|
||||
type instrRec struct {
|
||||
instr *ast.Instr
|
||||
compressed bool
|
||||
code []byte
|
||||
pad int
|
||||
relaxed bool
|
||||
jmpTo string
|
||||
}
|
||||
var recs []instrRec
|
||||
offsets := map[string]int{}
|
||||
pos := guardLen + len(prologue)
|
||||
pendingPad := 0
|
||||
for _, stmt := range t.Body {
|
||||
switch s := stmt.(type) {
|
||||
case *ast.Label:
|
||||
offsets[s.Name.Text] = pos
|
||||
case *ast.Instr:
|
||||
recs = append(recs, instrRec{instr: s})
|
||||
if strings.ToUpper(s.Mnemonic.Text) == "PCALIGN" {
|
||||
pendingPad += riscvPCAlignPad(pos, s)
|
||||
pos += riscvPCAlignPad(pos, s)
|
||||
continue
|
||||
}
|
||||
recs = append(recs, instrRec{instr: s, pad: pendingPad})
|
||||
pendingPad = 0
|
||||
pos += riscvInstrSize(s, fi)
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: encode each instruction using Pass-1 offsets.
|
||||
// Pass 2: encode each instruction using Pass-1 offsets. A branch or
|
||||
// jump the offsets prove overlong encodes to a 4-byte placeholder: the
|
||||
// relaxation pass rewrites it before the final encoding. pcRelPcs is
|
||||
// unavailable this early, so the N(PC) forms take the same placeholder
|
||||
// path.
|
||||
pc := len(prologue)
|
||||
for i := range recs {
|
||||
code, err := encodeRISCVInstr(recs[i].instr, pc, offsets, fi, nil) // no relocs in Pass 2
|
||||
if err != nil {
|
||||
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
|
||||
if err != nil && !(branchLike && riscvIsRangeError(err)) {
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", recs[i].instr.Mnemonic.Text, err)
|
||||
}
|
||||
if err != nil {
|
||||
code = make([]byte, 4)
|
||||
}
|
||||
recs[i].code = code
|
||||
pc += len(code)
|
||||
}
|
||||
@@ -72,20 +96,100 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
||||
// 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.
|
||||
// whole slice per statement. PCALIGN padding is recomputed here, since
|
||||
// compression has shifted instruction sizes since Pass 1.
|
||||
offsets = map[string]int{}
|
||||
pos = guardLen + len(prologue)
|
||||
ri := 0
|
||||
pendingPad = 0
|
||||
for _, stmt := range t.Body {
|
||||
switch s := stmt.(type) {
|
||||
case *ast.Label:
|
||||
offsets[s.Name.Text] = pos
|
||||
case *ast.Instr:
|
||||
if strings.ToUpper(s.Mnemonic.Text) == "PCALIGN" {
|
||||
pad := riscvPCAlignPad(pos, s)
|
||||
pendingPad += pad
|
||||
pos += pad
|
||||
continue
|
||||
}
|
||||
recs[ri].pad = pendingPad
|
||||
pendingPad = 0
|
||||
pos += len(recs[ri].code)
|
||||
ri++
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 4b: relax overlong conditional branches exactly as the toolchain
|
||||
// does: invert the branch condition, point it at the instruction after an
|
||||
// inserted JMP, let the JMP carry the original target, and re-layout until
|
||||
// a pass inserts nothing. Inserted JMP recs share their branch's source
|
||||
// line and trail it in emission order, so the body walk flushes them
|
||||
// before every statement and at the end.
|
||||
var pcRelPcs map[*ast.Instr]int
|
||||
for {
|
||||
offsets = map[string]int{}
|
||||
pos = guardLen + len(prologue)
|
||||
ri := 0
|
||||
pcs := make([]int, len(recs))
|
||||
flushJmps := func() {
|
||||
for ri < len(recs) && recs[ri].jmpTo != "" {
|
||||
pcs[ri] = pos
|
||||
pos += 4
|
||||
ri++
|
||||
}
|
||||
}
|
||||
for _, stmt := range t.Body {
|
||||
switch s := stmt.(type) {
|
||||
case *ast.Label:
|
||||
flushJmps()
|
||||
offsets[s.Name.Text] = pos
|
||||
case *ast.Instr:
|
||||
flushJmps()
|
||||
if ri >= len(recs) {
|
||||
continue
|
||||
}
|
||||
pcs[ri] = pos + recs[ri].pad
|
||||
pos += recs[ri].pad + len(recs[ri].code)
|
||||
ri++
|
||||
}
|
||||
}
|
||||
flushJmps()
|
||||
|
||||
changed := false
|
||||
for i := range recs {
|
||||
r := &recs[i]
|
||||
if r.relaxed || r.jmpTo != "" {
|
||||
continue
|
||||
}
|
||||
mnem := strings.ToUpper(r.instr.Mnemonic.Text)
|
||||
if !riscvIsCondBranch(mnem) || len(r.instr.Operands) == 0 {
|
||||
continue
|
||||
}
|
||||
target := labelFromOperand(r.instr.Operands[len(r.instr.Operands)-1])
|
||||
targetOff, ok := offsets[target]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if delta := int32(targetOff - pcs[i]); delta < -4096 || delta >= 4096 {
|
||||
r.relaxed = true
|
||||
recs = slices.Insert(recs, i+1, instrRec{instr: r.instr, jmpTo: target})
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
// Capture the final pcs for the N(PC) branch forms: their target
|
||||
// is the instruction N source slots away, resolved by index.
|
||||
pcRelPcs = map[*ast.Instr]int{}
|
||||
for i := range recs {
|
||||
if _, ok := riscvPCRelOffset(recs[i].instr); ok {
|
||||
pcRelPcs[recs[i].instr] = pcs[i]
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 5: re-encode branches with corrected offsets. Record relocations
|
||||
// during this final pass (relocation offsets are relative to instruction
|
||||
// start). The guard prefix precedes the prologue; its branches target
|
||||
@@ -104,12 +208,40 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
||||
preCount := len(relocs)
|
||||
var lines []LineEntry
|
||||
for _, r := range recs {
|
||||
// PCALIGN padding precedes the instruction it was attached to.
|
||||
if r.pad > 0 {
|
||||
out = append(out, riscvPadBytes(r.pad)...)
|
||||
pc += r.pad
|
||||
}
|
||||
lines = append(lines, LineEntry{Offset: pc, Line: r.instr.Pos().Line})
|
||||
if r.compressed && !isBranchLike(r.instr.Mnemonic.Text) {
|
||||
out = append(out, r.code...)
|
||||
pc += len(r.code)
|
||||
} else {
|
||||
code, err := encodeRISCVInstr(r.instr, pc, offsets, fi, &relocs)
|
||||
var code []byte
|
||||
switch {
|
||||
case r.jmpTo != "":
|
||||
// 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)
|
||||
}
|
||||
offset := int32(targetOff - pc)
|
||||
if err := riscvCheckJumpOffset(r.jmpTo, offset); err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
word := riscvJType(0, offset)
|
||||
code = []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}
|
||||
case r.relaxed:
|
||||
// The inverted half of a relaxed branch: it targets the inserted
|
||||
// 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)
|
||||
}
|
||||
word := riscvBType(enc, rs1, rs2, 4)
|
||||
code = []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}
|
||||
case r.compressed && !isBranchLike(r.instr.Mnemonic.Text):
|
||||
code = r.code
|
||||
default:
|
||||
var err error
|
||||
code, err = encodeRISCVInstr(r.instr, pc, offsets, fi, &relocs, pcRelPcs)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
@@ -131,9 +263,9 @@ func assembleRISCV(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
|
||||
if strings.ToUpper(r.instr.Mnemonic.Text) == "RET" && fi.autosize != 0 {
|
||||
spadj = append(spadj, SpadjStep{PC: pc + riscvReturnEpilogueLen(fi), Value: 0})
|
||||
}
|
||||
out = append(out, code...)
|
||||
pc += len(code)
|
||||
}
|
||||
out = append(out, code...)
|
||||
pc += len(code)
|
||||
}
|
||||
if fi.needSplit {
|
||||
relocs = append(relocs, guardReloc)
|
||||
@@ -150,6 +282,8 @@ var riscvImmAlias = map[string]string{
|
||||
"AND": "ANDI",
|
||||
"OR": "ORI",
|
||||
"XOR": "XORI",
|
||||
"SLT": "SLTI",
|
||||
"SLTU": "SLTIU",
|
||||
"SLL": "SLLI",
|
||||
"SRL": "SRLI",
|
||||
"SRA": "SRAI",
|
||||
@@ -178,6 +312,35 @@ func riscvNormaliseImmAlias(mnem string, ops []*ast.Operand) (string, bool) {
|
||||
return mnem, false
|
||||
}
|
||||
|
||||
// riscvPCAlignPad returns the padding PCALIGN inserts before the next
|
||||
// instruction so that it starts at the requested boundary relative to the
|
||||
// function start. The boundary must be a power of two between 8 and 2048, as
|
||||
// the toolchain requires; anything else pads nothing.
|
||||
func riscvPCAlignPad(pos int, instr *ast.Instr) int {
|
||||
if len(instr.Operands) != 1 || !isImmOperand(instr.Operands[0]) {
|
||||
return 0
|
||||
}
|
||||
align := int(immFromOperand(instr.Operands[0]))
|
||||
if align < 8 || align > 2048 || align&(align-1) != 0 {
|
||||
return 0
|
||||
}
|
||||
return (align - pos%align) % align
|
||||
}
|
||||
|
||||
// riscvPadBytes renders PCALIGN padding: 4-byte NOPs (addi $0, X0, X0) with a
|
||||
// trailing 2-byte compressed NOP when the pad is 2 mod 4, exactly as the
|
||||
// toolchain lays the bytes down.
|
||||
func riscvPadBytes(pad int) []byte {
|
||||
out := make([]byte, 0, pad)
|
||||
for ; pad >= 4; pad -= 4 {
|
||||
out = append(out, 0x13, 0x00, 0x00, 0x00)
|
||||
}
|
||||
if pad == 2 {
|
||||
out = append(out, 0x01, 0x00)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// riscvInstrSize returns the encoded size in bytes of a RISC-V instruction.
|
||||
// Most instructions are 4 bytes; MOV with a large immediate and I-type
|
||||
// arithmetic with a large immediate expand to several (possibly compressed)
|
||||
@@ -224,6 +387,10 @@ func riscvInstrSize(instr *ast.Instr, fi riscvFrameInfo) int {
|
||||
}
|
||||
return riscvItypeImmediateSize(mnem, imm)
|
||||
}
|
||||
// BYTE lays down one raw byte per operand.
|
||||
if mnem == "BYTE" {
|
||||
return len(ops)
|
||||
}
|
||||
// The toolchain's synthesised instructions: some emit one word, others
|
||||
// expand to a fixed sequence.
|
||||
return riscvExtendedSize(mnem, ops)
|
||||
@@ -318,12 +485,171 @@ func isBranchLike(mnem string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// riscvIsCondBranch reports whether m is a conditional branch, the only
|
||||
// instruction class branch relaxation rewrites.
|
||||
func riscvIsCondBranch(mnem string) bool {
|
||||
switch mnem {
|
||||
case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU", "BGT", "BLE", "BGTU", "BLEU",
|
||||
"BEQZ", "BNEZ", "BLTZ", "BGEZ", "BLEZ", "BGTZ":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// riscvCSRNames maps the standard CSR mnemonics the assembler accepts onto
|
||||
// their addresses.
|
||||
var riscvCSRNames = map[string]int32{
|
||||
"FFLAGS": 0x001,
|
||||
"FRM": 0x002,
|
||||
"FCSR": 0x003,
|
||||
"VSTART": 0x008,
|
||||
"VXSAT": 0x009,
|
||||
"VXRM": 0x00A,
|
||||
"VCSR": 0x00F,
|
||||
"CYCLE": 0xC00,
|
||||
"TIME": 0xC01,
|
||||
"INSTRET": 0xC02,
|
||||
"CYCLEH": 0xC80,
|
||||
"TIMEH": 0xC81,
|
||||
"INSTRETH": 0xC82,
|
||||
"VL": 0xC20,
|
||||
"VLENB": 0xC22,
|
||||
}
|
||||
|
||||
// riscvCSRAddress resolves a CSR operand: an integer immediate or one of the
|
||||
// standard CSR names.
|
||||
func riscvCSRAddress(op *ast.Operand) (int32, bool) {
|
||||
if isImmOperand(op) {
|
||||
return immFromOperand(op), true
|
||||
}
|
||||
if op.Addr.Sym != nil {
|
||||
if v, ok := riscvCSRNames[strings.ToUpper(op.Addr.Sym.Name)]; ok {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// riscvPCRelOffset reports the N of a branch or jump operand spelled N(PC):
|
||||
// the displacement counted in source instructions from the branch itself.
|
||||
func riscvPCRelOffset(instr *ast.Instr) (int, bool) {
|
||||
mnem := strings.ToUpper(instr.Mnemonic.Text)
|
||||
switch mnem {
|
||||
case "JMP":
|
||||
if len(instr.Operands) != 1 {
|
||||
return 0, false
|
||||
}
|
||||
case "JAL":
|
||||
if len(instr.Operands) != 1 && len(instr.Operands) != 2 {
|
||||
return 0, false
|
||||
}
|
||||
case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU",
|
||||
"BEQZ", "BNEZ", "BLTZ", "BGEZ", "BLEZ", "BGTZ":
|
||||
if len(instr.Operands) < 2 {
|
||||
return 0, false
|
||||
}
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
op := instr.Operands[len(instr.Operands)-1]
|
||||
if op.Kind == ast.OpAddr && op.Addr.Sym == nil && op.Addr.Base == "PC" {
|
||||
return int(op.Addr.Offset), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// riscvPCRelTargetOff resolves the target displacement of a branch whose last
|
||||
// operand is N(PC): the toolchain's parser counts the source instructions at
|
||||
// a uniform 4 bytes, so the target is the instruction N slots away, and the
|
||||
// displacement tracks that instruction's final pc. A nil pcRelPcs (the
|
||||
// layout passes) yields a placeholder range error; the caller tolerates it
|
||||
// for branch-like instructions.
|
||||
func riscvPCRelTargetOff(instr *ast.Instr, pc int, pcRelPcs map[*ast.Instr]int) (int, bool, error) {
|
||||
off, ok := riscvPCRelOffset(instr)
|
||||
if !ok {
|
||||
return 0, false, nil
|
||||
}
|
||||
if pcRelPcs == nil {
|
||||
return 0, true, &riscvRangeError{"pc-relative placeholder"}
|
||||
}
|
||||
targetPc, ok := pcRelPcs[instr]
|
||||
if !ok {
|
||||
return 0, true, fmt.Errorf("PC-relative target %d out of range", off)
|
||||
}
|
||||
return targetPc, true, nil
|
||||
}
|
||||
|
||||
// riscvInvertedBranchEnc returns the encoding of mnem's inverted condition
|
||||
// for the given operands: InvertBranch's table applied at the encoding level.
|
||||
// The register operands are already in position for the inverted form.
|
||||
func riscvInvertedBranchEnc(mnem string, ops []*ast.Operand) (riscvEnc, int, int, bool) {
|
||||
reg := func(i int) int { return regFromOperand(ops[i]) }
|
||||
switch mnem {
|
||||
case "BEQ": // → BNE rs1, rs2
|
||||
return riscvEnc{0x63, 0x1, 0x00}, reg(0), reg(1), true
|
||||
case "BNE": // → BEQ rs1, rs2
|
||||
return riscvEnc{0x63, 0x0, 0x00}, reg(0), reg(1), true
|
||||
case "BLT": // → BGE rs1, rs2
|
||||
return riscvEnc{0x63, 0x5, 0x00}, reg(0), reg(1), true
|
||||
case "BGE": // → BLT rs1, rs2
|
||||
return riscvEnc{0x63, 0x4, 0x00}, reg(0), reg(1), true
|
||||
case "BLTU": // → BGEU rs1, rs2
|
||||
return riscvEnc{0x63, 0x7, 0x00}, reg(0), reg(1), true
|
||||
case "BGEU": // → BLTU rs1, rs2
|
||||
return riscvEnc{0x63, 0x6, 0x00}, reg(0), reg(1), true
|
||||
case "BEQZ": // → BNEZ rs, X0
|
||||
return riscvEnc{0x63, 0x1, 0x00}, reg(0), 0, true
|
||||
case "BNEZ": // → BEQZ rs, X0
|
||||
return riscvEnc{0x63, 0x0, 0x00}, reg(0), 0, true
|
||||
case "BLTZ": // → BGEZ rs, X0
|
||||
return riscvEnc{0x63, 0x5, 0x00}, reg(0), 0, true
|
||||
case "BGEZ": // → BLTZ rs, X0
|
||||
return riscvEnc{0x63, 0x4, 0x00}, reg(0), 0, true
|
||||
case "BLEZ": // → BGTZ: blt X0, rs
|
||||
return riscvEnc{0x63, 0x4, 0x00}, 0, reg(0), true
|
||||
case "BGTZ": // → BLEZ: bge X0, rs
|
||||
return riscvEnc{0x63, 0x5, 0x00}, 0, reg(0), true
|
||||
case "BGT": // → BLE: bge rs2, rs1
|
||||
return riscvEnc{0x63, 0x5, 0x00}, reg(1), reg(0), true
|
||||
case "BLE": // → BGT: blt rs2, rs1
|
||||
return riscvEnc{0x63, 0x4, 0x00}, reg(1), reg(0), true
|
||||
case "BGTU": // → BLEU: bgeu rs2, rs1
|
||||
return riscvEnc{0x63, 0x7, 0x00}, reg(1), reg(0), true
|
||||
case "BLEU": // → BGTU: bltu rs2, rs1
|
||||
return riscvEnc{0x63, 0x6, 0x00}, reg(1), reg(0), true
|
||||
}
|
||||
return riscvEnc{}, 0, 0, false
|
||||
}
|
||||
|
||||
// riscvRangeError reports a branch or jump displacement beyond its
|
||||
// architecture limit. The layout passes tolerate it (the relaxation pass
|
||||
// rewrites overlong conditional branches before the final encoding); a range
|
||||
// error reaching the final pass is a real failure.
|
||||
type riscvRangeError struct{ msg string }
|
||||
|
||||
func (e *riscvRangeError) Error() string { return e.msg }
|
||||
|
||||
// riscvIsRangeError reports whether err is a displacement-range rejection.
|
||||
func riscvIsRangeError(err error) bool {
|
||||
var re *riscvRangeError
|
||||
return errors.As(err, &re)
|
||||
}
|
||||
|
||||
// riscvRoundModes maps the rounding-mode suffixes onto their funct7 codes.
|
||||
var riscvRoundModes = map[string]uint32{
|
||||
"RNE": 0,
|
||||
"RTZ": 1,
|
||||
"RDN": 2,
|
||||
"RUP": 3,
|
||||
"RMM": 4,
|
||||
}
|
||||
|
||||
// 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 &riscvRangeError{fmt.Sprintf("branch to %q too far (13-bit range)", target)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -332,13 +658,13 @@ func riscvCheckBranchOffset(target string, off int32) error {
|
||||
// 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 &riscvRangeError{fmt.Sprintf("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) {
|
||||
func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscvFrameInfo, relocs *[]Reloc, pcRelPcs map[*ast.Instr]int) ([]byte, error) {
|
||||
mnem := instr.Mnemonic.Text
|
||||
ops := instr.Operands
|
||||
var immNeg bool
|
||||
@@ -351,6 +677,27 @@ 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 "WORD":
|
||||
// WORD $w lays down a raw 32-bit little-endian word.
|
||||
if len(ops) != 1 {
|
||||
return nil, fmt.Errorf("WORD expects 1 operand, got %d", len(ops))
|
||||
}
|
||||
w := int64(immFromOperand(ops[0]))
|
||||
if w < 0 || w > 0xFFFFFFFF {
|
||||
return nil, fmt.Errorf("WORD: immediate %d does not fit a 32-bit word", w)
|
||||
}
|
||||
return []byte{byte(w), byte(w >> 8), byte(w >> 16), byte(w >> 24)}, nil
|
||||
case "BYTE":
|
||||
// BYTE $b lays down one raw byte per operand.
|
||||
var out []byte
|
||||
for _, op := range ops {
|
||||
b := int64(immFromOperand(op))
|
||||
if b < 0 || b > 0xFF {
|
||||
return nil, fmt.Errorf("BYTE: immediate %d does not fit a byte", b)
|
||||
}
|
||||
out = append(out, byte(b))
|
||||
}
|
||||
return out, nil
|
||||
case "CALL":
|
||||
// CALL sym(SB) → JAL X1, sym(SB) with a single R_RISCV_JAL
|
||||
// relocation. The Go assembler rejects CALL to a local branch label.
|
||||
@@ -404,6 +751,17 @@ 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 {
|
||||
@@ -424,6 +782,18 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
} else if len(ops) == 1 {
|
||||
target = labelFromOperand(ops[0])
|
||||
}
|
||||
if off, isPCRel, err := riscvPCRelTargetOff(instr, pc, pcRelPcs); isPCRel {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targetOff := off
|
||||
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
|
||||
}
|
||||
targetOff, ok := offsets[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
@@ -456,10 +826,20 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
if rs < 0 {
|
||||
return nil, fmt.Errorf("%s: invalid register", mnem)
|
||||
}
|
||||
target := labelFromOperand(ops[1])
|
||||
targetOff, ok := offsets[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
targetOff := 0
|
||||
target := ""
|
||||
if off, isPCRel, err := riscvPCRelTargetOff(instr, pc, pcRelPcs); isPCRel {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targetOff = off
|
||||
} else {
|
||||
target = labelFromOperand(ops[1])
|
||||
var ok bool
|
||||
targetOff, ok = offsets[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
}
|
||||
}
|
||||
var enc riscvEnc
|
||||
rs1, rs2 := rs, 0
|
||||
@@ -484,18 +864,25 @@ 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
|
||||
|
||||
// System instructions with no operands.
|
||||
case "FENCE", "ECALL", "EBREAK":
|
||||
case "FENCE", "ECALL", "EBREAK", "FENCE.TSO", "PAUSE":
|
||||
enc, ok := riscvInstrTable[mnem]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported system instruction %q", mnem)
|
||||
}
|
||||
// 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).
|
||||
// (the toolchain's encodeFenceOperand TYPE_NONE default). FENCE.TSO
|
||||
// carries the TSO fence mode with RW predecessor and successor.
|
||||
imm := int32(0)
|
||||
if mnem == "FENCE" {
|
||||
imm = 0x0FF
|
||||
}
|
||||
if mnem == "FENCE.TSO" {
|
||||
imm = 0x833
|
||||
}
|
||||
if mnem == "PAUSE" {
|
||||
imm = 0x010
|
||||
}
|
||||
word = riscvIType(enc, 0, 0, imm)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
}
|
||||
@@ -516,6 +903,29 @@ 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
|
||||
}
|
||||
|
||||
// FP conversions with an explicit rounding mode: FCVTWS.RNE and friends
|
||||
// suffix the base mnemonic with RNE/RTZ/RDN/RUP/RMM, which lands in the
|
||||
// low three bits of the funct7 field.
|
||||
if i := strings.IndexByte(mnem, '.'); i > 0 {
|
||||
if base, ok := riscvCvtTable[mnem[:i]]; ok {
|
||||
rm, ok := riscvRoundModes[mnem[i+1:]]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported rounding mode in %q", mnem)
|
||||
}
|
||||
if len(ops) != 2 {
|
||||
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
rs1 := regFromOperand(ops[0])
|
||||
rd := regFromOperand(ops[1])
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("invalid operand in %s", mnem)
|
||||
}
|
||||
base.funct7 = (base.funct7 &^ 7) | rm
|
||||
word := riscvCvtType(base, rd, rs1)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// R4-type fused multiply-add: INSTR rs1, rs2, rs3, rd (destination last).
|
||||
if fmaEnc, ok := riscvFmaTable[mnem]; ok {
|
||||
if len(ops) != 4 {
|
||||
@@ -532,29 +942,103 @@ 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
|
||||
}
|
||||
|
||||
// CSR instructions: INSTR csr, rs1|uimm, rd (destination last).
|
||||
if csrEnc, ok := riscvCsrTable[mnem]; ok {
|
||||
if len(ops) != 3 {
|
||||
// CSR instructions: INSTR csr, rs1|uimm, rd (destination last). The
|
||||
// write-only pseudos (CSRS/CSRC/CSRW and their immediate forms) spell
|
||||
// the source first, the CSR second, and read the destination as X0; the
|
||||
// immediate or register variant follows the source operand's kind.
|
||||
csrMnem := mnem
|
||||
csrPseudo := false
|
||||
csrRead := false
|
||||
csrFix := int32(0)
|
||||
switch mnem {
|
||||
case "CSRS", "CSRW", "CSRC", "CSRSI", "CSRWI", "CSRCI":
|
||||
csrMnem = map[string]string{
|
||||
"CSRS": "CSRRS", "CSRW": "CSRRW", "CSRC": "CSRRC",
|
||||
"CSRSI": "CSRRSI", "CSRWI": "CSRRWI", "CSRCI": "CSRRCI",
|
||||
}[mnem]
|
||||
csrPseudo = true
|
||||
// CSRR csr, rd is CSRRS rd, csr, X0; the read pseudos RDCYCLE/RDTIME/
|
||||
// RDINSTRET fix the CSR to cycle/time/instret.
|
||||
case "CSRR":
|
||||
csrMnem = "CSRRS"
|
||||
csrPseudo = true
|
||||
csrRead = true
|
||||
case "RDCYCLE", "RDTIME", "RDINSTRET":
|
||||
csrMnem = "CSRRS"
|
||||
csrPseudo = true
|
||||
csrRead = true
|
||||
csrFix = map[string]int32{"RDCYCLE": 0xC00, "RDTIME": 0xC01, "RDINSTRET": 0xC02}[mnem]
|
||||
}
|
||||
if csrEnc, ok := riscvCsrTable[csrMnem]; ok {
|
||||
if csrRead && len(ops) != 1 && len(ops) != 2 {
|
||||
return nil, fmt.Errorf("%s expects 1 or 2 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
if csrPseudo && !csrRead && len(ops) != 2 {
|
||||
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
if !csrPseudo && len(ops) != 3 {
|
||||
return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops))
|
||||
}
|
||||
csr := immFromOperand(ops[0]) // CSR address (12-bit)
|
||||
csrOp := ops[0]
|
||||
srcOp := ops[0]
|
||||
rdOp := ops[len(ops)-1]
|
||||
switch {
|
||||
case csrRead:
|
||||
// CSRR csr, rd (or the fixed-CSR read pseudos with only rd).
|
||||
case csrPseudo:
|
||||
// src, csr.
|
||||
if len(ops) > 1 {
|
||||
csrOp, srcOp = ops[1], ops[0]
|
||||
}
|
||||
rdOp = nil
|
||||
default:
|
||||
// Either src, csr, rd or csr, src, rd: a CSR *name* in the
|
||||
// second operand marks the toolchain's order.
|
||||
srcOp = ops[1]
|
||||
if op := ops[1]; op.Addr.Sym != nil {
|
||||
if _, ok := riscvCSRNames[strings.ToUpper(op.Addr.Sym.Name)]; ok {
|
||||
csrOp, srcOp = ops[1], ops[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
csr, ok := riscvCSRAddress(csrOp)
|
||||
if !ok && csrFix == 0 {
|
||||
return nil, fmt.Errorf("%s: unknown CSR %q", mnem, csrOp.Raw)
|
||||
}
|
||||
if csrFix != 0 {
|
||||
csr = csrFix
|
||||
}
|
||||
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)
|
||||
rd := 0
|
||||
if !csrPseudo {
|
||||
rd = regFromOperand(rdOp) // destination register
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("invalid destination register in %s", mnem)
|
||||
}
|
||||
}
|
||||
if csrRead {
|
||||
rd = regFromOperand(rdOp)
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("invalid destination register in %s", mnem)
|
||||
}
|
||||
}
|
||||
var src int
|
||||
if csrEnc.imm {
|
||||
// Immediate variant: ops[1] is a 5-bit unsigned immediate.
|
||||
src = int(immFromOperand(ops[1]))
|
||||
switch {
|
||||
case csrRead:
|
||||
// CSRR reads with rs1 = X0: src stays zero.
|
||||
case isImmOperand(srcOp):
|
||||
// Immediate variant: the source is a 5-bit unsigned immediate.
|
||||
src = int(immFromOperand(srcOp))
|
||||
if src < 0 || src > 31 {
|
||||
return nil, fmt.Errorf("%s: uimm out of range 0-31", mnem)
|
||||
}
|
||||
} else {
|
||||
// Register variant: ops[1] is a register.
|
||||
src = regFromOperand(ops[1])
|
||||
case csrEnc.imm:
|
||||
return nil, fmt.Errorf("%s expects an immediate source", mnem)
|
||||
default:
|
||||
// Register variant: the source is a register.
|
||||
src = regFromOperand(srcOp)
|
||||
if src < 0 {
|
||||
return nil, fmt.Errorf("invalid source register in %s", mnem)
|
||||
}
|
||||
@@ -738,14 +1222,34 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
}
|
||||
word = riscvSType(enc, rs1, rs2, imm)
|
||||
|
||||
// Branches: rs1, rs2, label.
|
||||
// Branches: rs1, rs2, label. BGT/BLE/BGTU/BLEU are the swapped-spelling
|
||||
// forms of BLT/BGE/BLTU/BGEU (bgt rs1, rs2 is blt rs2, rs1).
|
||||
case len(ops) == 3 && isBranchInstr(mnem):
|
||||
rs1 := regFromOperand(ops[0])
|
||||
rs2 := regFromOperand(ops[1])
|
||||
target := labelFromOperand(ops[2])
|
||||
targetOff, ok := offsets[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
switch mnem {
|
||||
case "BGT":
|
||||
enc, rs1, rs2 = riscvEnc{0x63, 0x4, 0x00}, rs2, rs1 // blt rs2, rs1
|
||||
case "BLE":
|
||||
enc, rs1, rs2 = riscvEnc{0x63, 0x5, 0x00}, rs2, rs1 // bge rs2, rs1
|
||||
case "BGTU":
|
||||
enc, rs1, rs2 = riscvEnc{0x63, 0x6, 0x00}, rs2, rs1 // bltu rs2, rs1
|
||||
case "BLEU":
|
||||
enc, rs1, rs2 = riscvEnc{0x63, 0x7, 0x00}, rs2, rs1 // bgeu rs2, rs1
|
||||
}
|
||||
targetOff := 0
|
||||
if off, isPCRel, err := riscvPCRelTargetOff(instr, pc, pcRelPcs); isPCRel {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targetOff = off
|
||||
} else {
|
||||
var ok bool
|
||||
targetOff, ok = offsets[target]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||
}
|
||||
}
|
||||
offset := int32(targetOff - pc)
|
||||
if rs1 < 0 || rs2 < 0 {
|
||||
@@ -758,10 +1262,15 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
// The Go assembler never compresses branches to C.BEQZ/C.BNEZ.
|
||||
word = riscvBType(enc, rs1, rs2, offset)
|
||||
|
||||
// U-type: rd, imm.
|
||||
// U-type: rd, imm (or the toolchain testdata's INSTR $imm, rd).
|
||||
case len(ops) == 2 && isUTypeInstr(mnem):
|
||||
rd := regFromOperand(ops[0])
|
||||
imm := immFromOperand(ops[1])
|
||||
var rd int
|
||||
var imm int32
|
||||
if isImmOperand(ops[0]) {
|
||||
imm, rd = immFromOperand(ops[0]), regFromOperand(ops[1])
|
||||
} else {
|
||||
rd, imm = regFromOperand(ops[0]), immFromOperand(ops[1])
|
||||
}
|
||||
if rd < 0 {
|
||||
return nil, fmt.Errorf("invalid register in %s", mnem)
|
||||
}
|
||||
@@ -1226,6 +1735,11 @@ func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
|
||||
func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
mnem := riscvCompressMnem(instr)
|
||||
ops := instr.Operands
|
||||
// The immediate aliases fold onto their I-type mnemonics before
|
||||
// compression: the toolchain compresses ADD $imm, rd as c.addi, exactly
|
||||
// as it compresses the spelling ADDI.
|
||||
var immNeg bool
|
||||
mnem, immNeg = riscvNormaliseImmAlias(mnem, ops)
|
||||
|
||||
switch mnem {
|
||||
case "LD", "MOV":
|
||||
@@ -1289,6 +1803,9 @@ func tryCompressRVC(instr *ast.Instr, fi riscvFrameInfo) (uint16, bool) {
|
||||
|
||||
case "ADDI":
|
||||
rd, rs1, imm := extractITypeParams(instr)
|
||||
if immNeg {
|
||||
imm = -imm
|
||||
}
|
||||
if rd == -1 || rs1 == -1 {
|
||||
return 0, false
|
||||
}
|
||||
@@ -2045,7 +2562,8 @@ func isRTypeInstr(m string) bool {
|
||||
case "ADD", "SUB", "SLL", "SLT", "SLTU", "XOR", "SRL", "SRA", "OR", "AND",
|
||||
"ADDW", "SUBW", "SLLW", "SRLW", "SRAW",
|
||||
"MUL", "MULH", "MULHSU", "MULHU", "DIV", "DIVU", "REM", "REMU",
|
||||
"MULW", "DIVW", "DIVUW", "REMW", "REMUW":
|
||||
"MULW", "DIVW", "DIVUW", "REMW", "REMUW",
|
||||
"CZEROEQZ", "CZERONEZ":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -2085,7 +2603,7 @@ func isStoreInstr(m string) bool {
|
||||
|
||||
func isBranchInstr(m string) bool {
|
||||
switch m {
|
||||
case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU":
|
||||
case "BEQ", "BNE", "BLT", "BGE", "BLTU", "BGEU", "BGT", "BLE", "BGTU", "BLEU":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -2111,7 +2629,8 @@ func isFPArithInstr(m string) bool {
|
||||
switch m {
|
||||
case "FADDS", "FSUBS", "FMULS", "FDIVS",
|
||||
"FADDD", "FSUBD", "FMULD", "FDIVD",
|
||||
"FSQRTS", "FSQRTD", "FMINS", "FMAXS", "FMIND", "FMAXD", "FSGNJD":
|
||||
"FSQRTS", "FSQRTD", "FMINS", "FMAXS", "FMIND", "FMAXD", "FSGNJD",
|
||||
"FSGNJS", "FSGNJX", "FSGNJXD", "FSGNJXS", "FSGNJND", "FSGNJNS", "FSGNJNX":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user