feat(asm): encode indirect JMP and CALL on all four architectures
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -236,6 +236,11 @@ func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64
|
||||
return encodeARM64BranchCond(mnem, enc.op, ops, pc, offsets, resolve)
|
||||
}
|
||||
|
||||
// Unconditional register branches (BR, BLR).
|
||||
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FUncondBranch {
|
||||
return encodeARM64RegBranch(mnem, enc.op, ops)
|
||||
}
|
||||
|
||||
// ADD/SUB immediate.
|
||||
if mnem == "ADD" || mnem == "ADDW" || mnem == "SUB" || mnem == "SUBW" ||
|
||||
mnem == "CMP" || mnem == "CMPW" || mnem == "CMN" || mnem == "CMNW" {
|
||||
@@ -337,6 +342,24 @@ func encodeARM64Branch(mnem string, ops []*ast.Operand, pc int, offsets map[stri
|
||||
}
|
||||
op := ops[0]
|
||||
|
||||
// Register-indirect: JMP (R0) is BR R0, CALL (R0) is BLR R0. The
|
||||
// toolchain's spelling carries no offset and no index; anything else
|
||||
// is reported rather than silently dropped.
|
||||
if op.Addr.Sym == nil && op.Addr.Base != "" {
|
||||
if op.Addr.Offset != 0 || op.Addr.Index != "" {
|
||||
return nil, fmt.Errorf("%s: invalid indirect branch operand %q", mnem, op.Raw)
|
||||
}
|
||||
rn := arm64RegNum(op.Addr.Base)
|
||||
if rn < 0 {
|
||||
return nil, fmt.Errorf("%s: unknown branch register %q", mnem, op.Addr.Base)
|
||||
}
|
||||
opc := uint32(0) // BR
|
||||
if link {
|
||||
opc = 1 // BLR
|
||||
}
|
||||
return a64wordLE(a64UncondBranch(opc, uint32(rn), 0)), nil
|
||||
}
|
||||
|
||||
// Symbol reference: BL sym(SB), or B sym(SB) for a tail call, against a
|
||||
// relocation (R_CALLARM64 either way).
|
||||
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "SB" {
|
||||
@@ -373,6 +396,19 @@ func encodeARM64Branch(mnem string, ops []*ast.Operand, pc int, offsets map[stri
|
||||
return a64wordLE(a64Branch(bop, int32(rel))), nil
|
||||
}
|
||||
|
||||
// encodeARM64RegBranch encodes BR/BLR through a register operand:
|
||||
// BR Xn = 0xd61f0000 | Rn<<5, BLR Xn = 0xd63f0000 | Rn<<5.
|
||||
func encodeARM64RegBranch(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
||||
if len(ops) != 1 {
|
||||
return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
|
||||
}
|
||||
rn := arm64RegNum(operandRegName(ops[0]))
|
||||
if rn < 0 {
|
||||
return nil, fmt.Errorf("%s expects a register operand", mnem)
|
||||
}
|
||||
return a64wordLE(uint32(baseOp) | 31<<16 | uint32(rn)<<5), nil
|
||||
}
|
||||
|
||||
// encodeARM64BranchCond encodes a conditional branch (B.cond) to a label.
|
||||
func encodeARM64BranchCond(mnem string, baseOp uint32, ops []*ast.Operand, pc int, offsets map[string]int, resolve func(string) string) ([]byte, error) {
|
||||
if len(ops) != 1 {
|
||||
|
||||
@@ -572,3 +572,42 @@ func leWords(b []byte) []uint32 {
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// TestArm64IndirectBranch pins the indirect branch forms in a leaf function:
|
||||
// JMP (Rn) lowers to BR Rn, matching the toolchain's spelling, and the raw
|
||||
// BR/BLR mnemonics encode directly (a gasm superset the toolchain's front
|
||||
// end does not accept). CALL (Rn) shares the BLR path and its non-leaf
|
||||
// prologue parity is covered by the ground-truth kernel.
|
||||
func TestArm64IndirectBranch(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
JMP (R0)
|
||||
BR R5
|
||||
BLR R6
|
||||
RET
|
||||
`
|
||||
f, errs := parser.Parse("test_arm64.s", src)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
want := []uint32{
|
||||
0xd61f0000, // BR R0
|
||||
0xd61f00a0, // BR R5
|
||||
0xd63f00c0, // BLR R6
|
||||
0xd65f03c0, // RET (BR LR)
|
||||
}
|
||||
got := leWords(img.Code)
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("word count = %d, want %d", len(got), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Errorf("word %d = %08x, want %08x", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+66
-1
@@ -337,7 +337,18 @@ func computeFrame(t *ast.Text) frameInfo {
|
||||
if t.Frame != nil && t.Frame.Imm.HasVal {
|
||||
fi.size = int(t.Frame.Imm.Val)
|
||||
}
|
||||
if fi.size > 0 {
|
||||
if fi.size == 0 && hasCall(t) {
|
||||
// The toolchain gives a frameless function containing a CALL an
|
||||
// 8-byte frame for the pushed base pointer: the prologue saves BP
|
||||
// with no stack adjustment, every RET pops it back, FP references
|
||||
// pass one extra slot, and the virtual SP is the hardware SP.
|
||||
fi.size = 8
|
||||
fi.useFP = true
|
||||
fi.fpAdjust = int64(fi.size) + 16 // return address + saved BP + args base
|
||||
fi.spAdjust = 0
|
||||
fi.prologue = []byte{0x55, 0x48, 0x89, 0xE5} // PUSHQ BP; MOVQ SP, BP
|
||||
fi.epilogue = []byte{0x5D} // POPQ BP
|
||||
} else if fi.size > 0 {
|
||||
fi.useFP = true
|
||||
fi.fpAdjust = int64(fi.size) + 16 // frame + saved BP + return address
|
||||
fi.spAdjust = int64(fi.size)
|
||||
@@ -518,6 +529,13 @@ func instrSize(s *ast.Instr, fi frameInfo, long bool, link *linkInfo) (int, erro
|
||||
if (mnem == "CALL" || mnem == "JMP") && isSBCall(s) {
|
||||
return 5, nil // opcode + rel32, always the long form
|
||||
}
|
||||
if (mnem == "CALL" || mnem == "JMP") && indirectJumpTarget(s) {
|
||||
code, err := encodeIndirectJump(s, mnem)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(code), nil
|
||||
}
|
||||
return jumpSize(mnem, long), nil
|
||||
}
|
||||
code, _, err := encodeInstr(s, 0, nil, fi, false, nil, link)
|
||||
@@ -585,6 +603,15 @@ func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo, lon
|
||||
}
|
||||
return append(prefix, code...), ps, nil
|
||||
}
|
||||
if (mnem == "CALL" || mnem == "JMP") && indirectJumpTarget(s) {
|
||||
// JMP/CALL through a register or memory: no relocation and no
|
||||
// label to resolve, the operand fully determines the bytes.
|
||||
code, err = encodeIndirectJump(s, mnem)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return append(prefix, code...), nil, nil
|
||||
}
|
||||
code, err = encodeJump(s, mnem, pc+len(prefix), offsets, long, resolve)
|
||||
} else {
|
||||
code, ps, err = encodeNormal(s, fi, link)
|
||||
@@ -706,6 +733,44 @@ func labelName(op *ast.Operand) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// indirectJumpTarget reports whether the JMP/CALL operand addresses a
|
||||
// register or a memory location rather than a label or a static symbol.
|
||||
// A bare identifier is a register when the register table knows the name and
|
||||
// a label otherwise, which is exactly how the parser cannot distinguish them.
|
||||
func indirectJumpTarget(s *ast.Instr) bool {
|
||||
if len(s.Operands) != 1 || s.Operands[0].Kind != ast.OpAddr {
|
||||
return false
|
||||
}
|
||||
a := s.Operands[0].Addr
|
||||
if a.Base != "" || a.Index != "" {
|
||||
return true
|
||||
}
|
||||
if a.Sym != nil && a.Sym.Pseudo == "" && a.Sym.Name != "" {
|
||||
if _, ok := ParseReg(a.Sym.Name); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// encodeIndirectJump assembles a JMP/CALL through a register or memory
|
||||
// operand, which carries no relocation and no label to resolve.
|
||||
func encodeIndirectJump(s *ast.Instr, mnem string) ([]byte, error) {
|
||||
ops := make([]Operand, len(s.Operands))
|
||||
for i, op := range s.Operands {
|
||||
o, err := operandFromAST(op, 8, frameInfo{}, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ops[i] = o
|
||||
}
|
||||
e := &enc{}
|
||||
if err := e.encodeIndirectBranch(mnem, ops); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.out, nil
|
||||
}
|
||||
|
||||
// spReg is the hardware stack pointer used to realise FP/SP pseudo-operands.
|
||||
var spReg = Reg{idx: 4, size: 8}
|
||||
|
||||
|
||||
+14
-4
@@ -40,10 +40,20 @@ func (e *enc) encode(mnem string, ops []Operand) error {
|
||||
return e.encodeRet()
|
||||
case upper == "NOP":
|
||||
return e.emit(&instr{opcode: []byte{0x90}, modrm: -1, sib: -1})
|
||||
case upper == "CALL":
|
||||
return e.encodeJmpRel(ops, []byte{0xE8})
|
||||
case upper == "JMP":
|
||||
return e.encodeJmpRel(ops, []byte{0xE9})
|
||||
case upper == "CALL" || upper == "JMP":
|
||||
// Through a register or memory: FF /2 (CALL) or FF /4 (JMP).
|
||||
// Anything else is a rel32 against a label resolved by the assembler.
|
||||
if len(ops) == 1 {
|
||||
switch ops[0].(type) {
|
||||
case Reg, Mem:
|
||||
return e.encodeIndirectBranch(upper, ops)
|
||||
}
|
||||
}
|
||||
opcode := []byte{0xE8}
|
||||
if upper == "JMP" {
|
||||
opcode = []byte{0xE9}
|
||||
}
|
||||
return e.encodeJmpRel(ops, opcode)
|
||||
}
|
||||
if cc, ok := condCode(upper); ok {
|
||||
return e.encodeJcc(cc, ops)
|
||||
|
||||
@@ -181,6 +181,39 @@ func TestControl(t *testing.T) {
|
||||
checkOp(t, x86asm.JBE, "JLS", Imm(0))
|
||||
}
|
||||
|
||||
// TestIndirectControlFlow pins the indirect JMP/CALL forms: FF /4 for JMP and
|
||||
// FF /2 for CALL through a register or memory. A REX appears only for the
|
||||
// extended registers, never REX.W: the branch operand size is fixed at 64
|
||||
// bits in long mode.
|
||||
func TestIndirectControlFlow(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mnem string
|
||||
ops []Operand
|
||||
want string
|
||||
}{
|
||||
{"JMP AX", "JMP", []Operand{AX}, "ffe0"},
|
||||
{"CALL AX", "CALL", []Operand{AX}, "ffd0"},
|
||||
{"JMP (BX)", "JMP", []Operand{Ptr(BX, 0, 8)}, "ff23"},
|
||||
{"CALL (BX)", "CALL", []Operand{Ptr(BX, 0, 8)}, "ff13"},
|
||||
{"JMP 8(BX)", "JMP", []Operand{Ptr(BX, 8, 8)}, "ff6308"},
|
||||
{"CALL -16(BX)", "CALL", []Operand{Ptr(BX, -16, 8)}, "ff53f0"},
|
||||
{"JMP R8", "JMP", []Operand{Reg{idx: 8, size: 2}}, "41ffe0"},
|
||||
{"CALL R9", "CALL", []Operand{Reg{idx: 9, size: 2}}, "41ffd1"},
|
||||
{"JMP R15", "JMP", []Operand{Reg{idx: 15, size: 2}}, "41ffe7"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
code, err := Encode(c.mnem, c.ops...)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
if got := fmt.Sprintf("%x", code); got != c.want {
|
||||
t.Errorf("%s: got %s, want %s", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSSEMoveGroundTruth checks the legacy (non-VEX) SSE moves byte for byte
|
||||
// against the Go assembler. wantOp is the decoder's name, which differs from
|
||||
// the Plan 9 spelling for the octa moves (MOVOU = MOVDQU, MOVO = MOVDQA).
|
||||
|
||||
@@ -575,6 +575,24 @@ func (e *enc) encodeJmpRel(ops []Operand, opcode []byte) error {
|
||||
return e.emit(&instr{opcode: opcode, modrm: -1, sib: -1, imm: le32(int64(imm))})
|
||||
}
|
||||
|
||||
// encodeIndirectBranch encodes JMP/CALL through a register or memory operand:
|
||||
// FF /4 for JMP, FF /2 for CALL. The operand size is fixed at 64 bits in
|
||||
// 64-bit mode, so no REX.W is emitted; a REX appears only for R8-R15 bases.
|
||||
func (e *enc) encodeIndirectBranch(mnem string, ops []Operand) error {
|
||||
if len(ops) != 1 {
|
||||
return fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
|
||||
}
|
||||
digit := 4 // JMP r/m64
|
||||
if mnem == "CALL" {
|
||||
digit = 2 // CALL r/m64
|
||||
}
|
||||
i := &instr{opcode: []byte{0xFF}, modrm: -1, sib: -1}
|
||||
if err := setRMDigit(i, digit, ops[0], 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.emit(i)
|
||||
}
|
||||
|
||||
// condCode maps a Plan 9 conditional-jump mnemonic to its x86 condition code.
|
||||
func condCode(upper string) (int, bool) {
|
||||
if len(upper) < 2 || upper[0] != 'J' || upper == "JMP" {
|
||||
|
||||
@@ -6,6 +6,7 @@ package asm
|
||||
import (
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
@@ -258,6 +259,9 @@ func encodeLOONG64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi loo
|
||||
|
||||
// 16-bit branches (BEQ/BNE/BLT/BGE/BLTU/BGEU) and JIRL.
|
||||
if op, ok := l64branchTable[mnem]; ok {
|
||||
if mnem == "JIRL" {
|
||||
return encodeLOONG64Jirl(op, ops)
|
||||
}
|
||||
return encodeLOONG64Branch16(mnem, op, ops, pc, offsets, resolve)
|
||||
}
|
||||
// Single-register branches with 21-bit offsets (BLTZ/BGEZ/BLEZ/BGTZ,
|
||||
@@ -554,6 +558,46 @@ func encodeLOONG64Branch(instr *ast.Instr, mnem string, pc int, offsets map[stri
|
||||
return l64wordLE(l64bbl(opc, v)), nil
|
||||
}
|
||||
|
||||
// encodeLOONG64Jirl encodes the raw JIRL spelling, JIRL rd, rj, offset, the
|
||||
// form the verify trampolines use. The (rj) indirect form without an offset
|
||||
// is handled by encodeLOONG64Branch.
|
||||
func encodeLOONG64Jirl(op uint32, ops []*ast.Operand) ([]byte, error) {
|
||||
if len(ops) != 3 {
|
||||
return nil, fmt.Errorf("JIRL expects 3 operands, got %d", len(ops))
|
||||
}
|
||||
rd := l64Reg(ops[0])
|
||||
rj := l64Reg(ops[1])
|
||||
if rd < 0 || rj < 0 {
|
||||
return nil, fmt.Errorf("invalid register operand")
|
||||
}
|
||||
off, ok := l64offsetOperand(ops[2])
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("JIRL expects an immediate offset, got %q", ops[2].Raw)
|
||||
}
|
||||
if (int64(off)<<16)>>16 != int64(off) {
|
||||
return nil, fmt.Errorf("JIRL offset %d out of the 16-bit range", off)
|
||||
}
|
||||
return l64wordLE(l64irr16(op, int(off), rj, rd)), nil
|
||||
}
|
||||
|
||||
// l64offsetOperand reads a bare numeric branch offset: an immediate ($n) or a
|
||||
// plain number, which parses as an empty address carrying the digits in Raw.
|
||||
func l64offsetOperand(op *ast.Operand) (int32, bool) {
|
||||
if op.Imm.HasVal {
|
||||
v := op.Imm.Val
|
||||
if op.Imm.Neg {
|
||||
v = -v
|
||||
}
|
||||
return int32(v), true
|
||||
}
|
||||
if op.Kind == ast.OpAddr && op.Addr.Sym == nil && op.Addr.Base == "" && op.Addr.Index == "" {
|
||||
if v, err := strconv.ParseInt(op.Raw, 0, 64); err == nil {
|
||||
return int32(v), true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// encodeLOONG64Branch16 encodes a 16-bit branch (BEQ/BNE/BLT/BGE/BLTU/BGEU):
|
||||
// INSTR rj, rd, label, or INSTR rj, label with rd = R0, which the toolchain
|
||||
// turns into the 21-bit BEQZ/BNEZ form when the register is the only operand.
|
||||
|
||||
@@ -291,3 +291,40 @@ done:
|
||||
t.Errorf("code = % x\nwant % x", code, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLOONG64IndirectBranch pins the indirect branch encodings: JMP (Rj) and
|
||||
// JAL (Rj) lower to jirl, and the raw JIRL spelling encodes the written
|
||||
// offset (the Go loong64 assembler deletes raw JIRL instructions entirely,
|
||||
// so this form is a gasm-only superset with faithful semantics).
|
||||
func TestLOONG64IndirectBranch(t *testing.T) {
|
||||
fn := firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
JMP (R4)
|
||||
JIRL R0, R4, 8
|
||||
RET
|
||||
`)
|
||||
code := assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x4C000080, // jirl r0, r4, 0
|
||||
0x4C002080, // jirl r0, r4, 8
|
||||
0x4C000020, // jirl r0, r1, 0 (RET)
|
||||
)
|
||||
|
||||
// JAL (R5) links, so the toolchain gives the function its autosize-8
|
||||
// prologue and epilogue around the call and the closing RET.
|
||||
fn = firstTextLOONG64(t, `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
JAL (R5)
|
||||
RET
|
||||
`)
|
||||
code = assembleLOONG64Helper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x29FFE061, // addi.d r1, r2, -8 (prologue)
|
||||
0x02FFE063, // addi.d r3, r3, -8
|
||||
0x29C00061, // st.d r1, r2, 0 (prologue saves RA)
|
||||
0x4C0000A1, // jirl r1, r5, 0
|
||||
0x28C00061, // ld.d r1, r2, 0 (epilogue restores RA)
|
||||
0x02C02063, // addi.d r3, r3, 8
|
||||
0x4C000020, // jirl r0, r1, 0 (RET)
|
||||
)
|
||||
}
|
||||
|
||||
+38
-5
@@ -208,6 +208,18 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
||||
}
|
||||
op := ops[0]
|
||||
if op.Addr.Sym == nil || op.Addr.Sym.Pseudo != "SB" {
|
||||
// CALL (X5): an indirect call, the toolchain's JALR X1, 0(X5).
|
||||
if op.Addr.Sym == nil && op.Addr.Base != "" {
|
||||
if op.Addr.Offset != 0 || op.Addr.Index != "" {
|
||||
return nil, fmt.Errorf("CALL: invalid indirect operand %q", op.Raw)
|
||||
}
|
||||
rs1 := riscvRegNum(op.Addr.Base)
|
||||
if rs1 < 0 {
|
||||
return nil, fmt.Errorf("CALL: unknown branch register %q", op.Addr.Base)
|
||||
}
|
||||
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, 0)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("CALL: local branch target is not supported (use CALL sym(SB))")
|
||||
}
|
||||
if relocs != nil {
|
||||
@@ -229,6 +241,18 @@ 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 (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 != "" {
|
||||
return nil, fmt.Errorf("JMP: invalid indirect operand %q", ops[0].Raw)
|
||||
}
|
||||
rs1 := riscvRegNum(ops[0].Addr.Base)
|
||||
if rs1 < 0 {
|
||||
return nil, fmt.Errorf("JMP: unknown branch register %q", ops[0].Addr.Base)
|
||||
}
|
||||
word = riscvIType(riscvEnc{0x67, 0x0, 0x00}, 0, rs1, 0)
|
||||
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||
}
|
||||
}
|
||||
targetOff, ok := offsets[target]
|
||||
if !ok {
|
||||
@@ -886,25 +910,34 @@ func word16(w uint16) []byte {
|
||||
}
|
||||
|
||||
// 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), JALR rd, offset(rs1) (the trampoline
|
||||
// form), or JALR offset(rs1) (memory → rd=X1).
|
||||
func encodeRISCVJALR(instr *ast.Instr, fi riscvFrameInfo) ([]byte, error) {
|
||||
ops := instr.Operands
|
||||
// JALR rd, offset(rs1): the memory operand's base is the jump-target
|
||||
// register, not the destination.
|
||||
if len(ops) == 2 && isMemOperand(ops[1]) {
|
||||
rd := regFromOperand(ops[0])
|
||||
rs1, imm := memFromOperandWithFrame(ops[1], fi)
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("JALR: invalid register operand")
|
||||
}
|
||||
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, imm)), nil
|
||||
}
|
||||
if len(ops) == 2 {
|
||||
rs1 := regFromOperand(ops[0])
|
||||
rd := regFromOperand(ops[1])
|
||||
if rd < 0 || rs1 < 0 {
|
||||
return nil, fmt.Errorf("JALR: invalid register operand")
|
||||
}
|
||||
word := riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, 0)
|
||||
return wordLE(word), nil
|
||||
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, 0)), nil
|
||||
}
|
||||
if len(ops) == 1 {
|
||||
rs1, imm := memFromOperandWithFrame(ops[0], fi)
|
||||
if rs1 < 0 {
|
||||
return nil, fmt.Errorf("JALR: invalid memory operand")
|
||||
}
|
||||
word := riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, imm)
|
||||
return wordLE(word), nil
|
||||
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, imm)), nil
|
||||
}
|
||||
return nil, fmt.Errorf("JALR expects 1 or 2 operands, got %d", len(ops))
|
||||
}
|
||||
|
||||
@@ -761,3 +761,24 @@ sub:
|
||||
t.Error("expected error for CALL to local label, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRISCVIndirectBranch pins the indirect branch encodings: JMP (X5) is the
|
||||
// toolchain's JALR X0, 0(X5), and the trampoline form JALR rd, offset(rs1)
|
||||
// takes its destination from the first operand (regression: the base
|
||||
// register was once read as the destination, silently jumping to X0).
|
||||
func TestRISCVIndirectBranch(t *testing.T) {
|
||||
fn := firstTextRISCV(t, `#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0-0
|
||||
JMP (X5)
|
||||
JALR X0, 0(X6)
|
||||
JALR X28, 0(X9)
|
||||
RET
|
||||
`)
|
||||
code := assembleRISCVHelper(t, fn)
|
||||
wantWords(t, code,
|
||||
0x00028067, // jalr x0, 5(x0), 0
|
||||
0x00030067, // jalr x0, 6(x0), 0
|
||||
0x00048e67, // jalr x28, 9(x0), 0
|
||||
0x00008067, // jalr x0, 1(x0), 0 (RET)
|
||||
)
|
||||
}
|
||||
|
||||
+10
-3
@@ -93,12 +93,19 @@ func riscvIsLeaf(t *ast.Text) bool {
|
||||
return false
|
||||
}
|
||||
case "JALR":
|
||||
// JALR rs1, rd, a call when rd is X1; JALR offset(rs1) always
|
||||
// links to X1.
|
||||
// JALR rd, offset(rs1) links when the destination register (the
|
||||
// first operand) is X1; JALR rs1, rd links when the second
|
||||
// register is X1; JALR offset(rs1) always links to X1.
|
||||
if len(in.Operands) == 1 {
|
||||
return false
|
||||
}
|
||||
if len(in.Operands) >= 2 && regFromOperand(in.Operands[1]) == 1 {
|
||||
if isMemOperand(in.Operands[1]) {
|
||||
if regFromOperand(in.Operands[0]) == 1 {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if regFromOperand(in.Operands[1]) == 1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user