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)
|
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.
|
// ADD/SUB immediate.
|
||||||
if mnem == "ADD" || mnem == "ADDW" || mnem == "SUB" || mnem == "SUBW" ||
|
if mnem == "ADD" || mnem == "ADDW" || mnem == "SUB" || mnem == "SUBW" ||
|
||||||
mnem == "CMP" || mnem == "CMPW" || mnem == "CMN" || mnem == "CMNW" {
|
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]
|
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
|
// Symbol reference: BL sym(SB), or B sym(SB) for a tail call, against a
|
||||||
// relocation (R_CALLARM64 either way).
|
// relocation (R_CALLARM64 either way).
|
||||||
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "SB" {
|
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
|
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.
|
// 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) {
|
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 {
|
if len(ops) != 1 {
|
||||||
|
|||||||
@@ -572,3 +572,42 @@ func leWords(b []byte) []uint32 {
|
|||||||
}
|
}
|
||||||
return w
|
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 {
|
if t.Frame != nil && t.Frame.Imm.HasVal {
|
||||||
fi.size = int(t.Frame.Imm.Val)
|
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.useFP = true
|
||||||
fi.fpAdjust = int64(fi.size) + 16 // frame + saved BP + return address
|
fi.fpAdjust = int64(fi.size) + 16 // frame + saved BP + return address
|
||||||
fi.spAdjust = int64(fi.size)
|
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) {
|
if (mnem == "CALL" || mnem == "JMP") && isSBCall(s) {
|
||||||
return 5, nil // opcode + rel32, always the long form
|
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
|
return jumpSize(mnem, long), nil
|
||||||
}
|
}
|
||||||
code, _, err := encodeInstr(s, 0, nil, fi, false, nil, link)
|
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
|
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)
|
code, err = encodeJump(s, mnem, pc+len(prefix), offsets, long, resolve)
|
||||||
} else {
|
} else {
|
||||||
code, ps, err = encodeNormal(s, fi, link)
|
code, ps, err = encodeNormal(s, fi, link)
|
||||||
@@ -706,6 +733,44 @@ func labelName(op *ast.Operand) (string, bool) {
|
|||||||
return "", false
|
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.
|
// spReg is the hardware stack pointer used to realise FP/SP pseudo-operands.
|
||||||
var spReg = Reg{idx: 4, size: 8}
|
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()
|
return e.encodeRet()
|
||||||
case upper == "NOP":
|
case upper == "NOP":
|
||||||
return e.emit(&instr{opcode: []byte{0x90}, modrm: -1, sib: -1})
|
return e.emit(&instr{opcode: []byte{0x90}, modrm: -1, sib: -1})
|
||||||
case upper == "CALL":
|
case upper == "CALL" || upper == "JMP":
|
||||||
return e.encodeJmpRel(ops, []byte{0xE8})
|
// Through a register or memory: FF /2 (CALL) or FF /4 (JMP).
|
||||||
case upper == "JMP":
|
// Anything else is a rel32 against a label resolved by the assembler.
|
||||||
return e.encodeJmpRel(ops, []byte{0xE9})
|
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 {
|
if cc, ok := condCode(upper); ok {
|
||||||
return e.encodeJcc(cc, ops)
|
return e.encodeJcc(cc, ops)
|
||||||
|
|||||||
@@ -181,6 +181,39 @@ func TestControl(t *testing.T) {
|
|||||||
checkOp(t, x86asm.JBE, "JLS", Imm(0))
|
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
|
// TestSSEMoveGroundTruth checks the legacy (non-VEX) SSE moves byte for byte
|
||||||
// against the Go assembler. wantOp is the decoder's name, which differs from
|
// 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).
|
// 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))})
|
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.
|
// condCode maps a Plan 9 conditional-jump mnemonic to its x86 condition code.
|
||||||
func condCode(upper string) (int, bool) {
|
func condCode(upper string) (int, bool) {
|
||||||
if len(upper) < 2 || upper[0] != 'J' || upper == "JMP" {
|
if len(upper) < 2 || upper[0] != 'J' || upper == "JMP" {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package asm
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/bits"
|
"math/bits"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"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.
|
// 16-bit branches (BEQ/BNE/BLT/BGE/BLTU/BGEU) and JIRL.
|
||||||
if op, ok := l64branchTable[mnem]; ok {
|
if op, ok := l64branchTable[mnem]; ok {
|
||||||
|
if mnem == "JIRL" {
|
||||||
|
return encodeLOONG64Jirl(op, ops)
|
||||||
|
}
|
||||||
return encodeLOONG64Branch16(mnem, op, ops, pc, offsets, resolve)
|
return encodeLOONG64Branch16(mnem, op, ops, pc, offsets, resolve)
|
||||||
}
|
}
|
||||||
// Single-register branches with 21-bit offsets (BLTZ/BGEZ/BLEZ/BGTZ,
|
// 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
|
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):
|
// 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
|
// 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.
|
// 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)
|
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]
|
op := ops[0]
|
||||||
if op.Addr.Sym == nil || op.Addr.Sym.Pseudo != "SB" {
|
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))")
|
return nil, fmt.Errorf("CALL: local branch target is not supported (use CALL sym(SB))")
|
||||||
}
|
}
|
||||||
if relocs != nil {
|
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
|
return []byte{byte(word), byte(word >> 8), byte(word >> 16), byte(word >> 24)}, nil
|
||||||
}
|
}
|
||||||
target = labelFromOperand(ops[0])
|
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]
|
targetOff, ok := offsets[target]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -886,25 +910,34 @@ func word16(w uint16) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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), JALR rd, offset(rs1) (the trampoline
|
||||||
|
// form), 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) {
|
||||||
ops := instr.Operands
|
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 {
|
if len(ops) == 2 {
|
||||||
rs1 := regFromOperand(ops[0])
|
rs1 := regFromOperand(ops[0])
|
||||||
rd := regFromOperand(ops[1])
|
rd := regFromOperand(ops[1])
|
||||||
if rd < 0 || rs1 < 0 {
|
if rd < 0 || rs1 < 0 {
|
||||||
return nil, fmt.Errorf("JALR: invalid register operand")
|
return nil, fmt.Errorf("JALR: invalid register operand")
|
||||||
}
|
}
|
||||||
word := riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, 0)
|
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, rd, rs1, 0)), nil
|
||||||
return wordLE(word), nil
|
|
||||||
}
|
}
|
||||||
if len(ops) == 1 {
|
if len(ops) == 1 {
|
||||||
rs1, imm := memFromOperandWithFrame(ops[0], fi)
|
rs1, imm := memFromOperandWithFrame(ops[0], fi)
|
||||||
if rs1 < 0 {
|
if rs1 < 0 {
|
||||||
return nil, fmt.Errorf("JALR: invalid memory operand")
|
return nil, fmt.Errorf("JALR: invalid memory operand")
|
||||||
}
|
}
|
||||||
word := riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, imm)
|
return wordLE(riscvIType(riscvEnc{0x67, 0x0, 0x00}, 1, rs1, imm)), nil
|
||||||
return wordLE(word), nil
|
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("JALR expects 1 or 2 operands, got %d", len(ops))
|
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")
|
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
|
return false
|
||||||
}
|
}
|
||||||
case "JALR":
|
case "JALR":
|
||||||
// JALR rs1, rd, a call when rd is X1; JALR offset(rs1) always
|
// JALR rd, offset(rs1) links when the destination register (the
|
||||||
// links to X1.
|
// 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 {
|
if len(in.Operands) == 1 {
|
||||||
return false
|
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
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,8 @@ Two deeper analyses sit on top of the AST:
|
|||||||
- **`unreachable-code`.** Code after a `RET` and before the next label is
|
- **`unreachable-code`.** Code after a `RET` and before the next label is
|
||||||
dead. The check is suppressed for any function whose reachability cannot be
|
dead. The check is suppressed for any function whose reachability cannot be
|
||||||
decided statically: those using PC-relative jumps (`JMP 2(PC)`),
|
decided statically: those using PC-relative jumps (`JMP 2(PC)`),
|
||||||
register-indirect branches (`JALR`/`JR`/`JIRL`/`BR`/`BLR`), or living in a
|
register-indirect branches (`JALR`/`JR`/`JIRL`/`BR`/`BLR`, or a `JMP`/`CALL`
|
||||||
|
through a register or memory operand), or living in a
|
||||||
file with `#ifdef` conditionals. `UNDEF` is deliberately not a terminator:
|
file with `#ifdef` conditionals. `UNDEF` is deliberately not a terminator:
|
||||||
code after it is occasionally intentional metadata.
|
code after it is occasionally intentional metadata.
|
||||||
- **`register-clobber` (register liveness).** The linter builds the function's
|
- **`register-clobber` (register liveness).** The linter builds the function's
|
||||||
|
|||||||
+29
-8
@@ -209,10 +209,10 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m
|
|||||||
lastTerminal := false
|
lastTerminal := false
|
||||||
hasMacro := false
|
hasMacro := false
|
||||||
instrCount := 0
|
instrCount := 0
|
||||||
dead := false // inside a region unreachable from above
|
dead := false // inside a region unreachable from above
|
||||||
reportedDead := false // the current dead region has already been reported
|
reportedDead := false // the current dead region has already been reported
|
||||||
hasPCRel := referencesPC(t) // PC-relative jumps defeat reachability analysis
|
hasPCRel := referencesPC(t) // PC-relative jumps defeat reachability analysis
|
||||||
hasIndirect := hasIndirectBranch(t) // register-indirect branches do too
|
hasIndirect := hasIndirectBranch(t, tab) // register-indirect branches do too
|
||||||
// Unreachable-code analysis is only sound in functions whose control flow is
|
// Unreachable-code analysis is only sound in functions whose control flow is
|
||||||
// fully label-resolvable: no PC-relative jumps, no register-indirect
|
// fully label-resolvable: no PC-relative jumps, no register-indirect
|
||||||
// branches, and (file-level) no preprocessor conditionals.
|
// branches, and (file-level) no preprocessor conditionals.
|
||||||
@@ -549,10 +549,12 @@ func referencesPC(t *ast.Text) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hasIndirectBranch reports whether a function transfers control through a
|
// hasIndirectBranch reports whether a function transfers control through a
|
||||||
// register (JALR/JR/JIRL/BR/BLR). Such targets are computed at runtime, so
|
// register or a computed memory address: the RISC branch-register mnemonics
|
||||||
// reachability cannot be determined statically and the unreachable-code check is
|
// (JALR/JR/JIRL/BR/BLR), or a JMP/CALL whose target is a register or memory
|
||||||
// suppressed for the whole function.
|
// operand rather than a label or symbol. Such targets are computed at
|
||||||
func hasIndirectBranch(t *ast.Text) bool {
|
// runtime, so reachability cannot be determined statically and the
|
||||||
|
// unreachable-code check is suppressed for the whole function.
|
||||||
|
func hasIndirectBranch(t *ast.Text, tab *arch.Table) bool {
|
||||||
for _, s := range t.Body {
|
for _, s := range t.Body {
|
||||||
in, ok := s.(*ast.Instr)
|
in, ok := s.(*ast.Instr)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -561,11 +563,30 @@ func hasIndirectBranch(t *ast.Text) bool {
|
|||||||
switch strings.ToUpper(in.Mnemonic.Text) {
|
switch strings.ToUpper(in.Mnemonic.Text) {
|
||||||
case "JALR", "JR", "JIRL", "BR", "BLR":
|
case "JALR", "JR", "JIRL", "BR", "BLR":
|
||||||
return true
|
return true
|
||||||
|
case "JMP", "CALL":
|
||||||
|
if indirectJumpTarget(in, tab) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// indirectJumpTarget reports whether the JMP/CALL operand addresses a
|
||||||
|
// register or a memory location rather than a label or a static symbol. The
|
||||||
|
// parser delivers a bare register and a bare label in the same shape, so
|
||||||
|
// register membership decides.
|
||||||
|
func indirectJumpTarget(in *ast.Instr, tab *arch.Table) bool {
|
||||||
|
if len(in.Operands) != 1 || in.Operands[0].Kind != ast.OpAddr {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
a := in.Operands[0].Addr
|
||||||
|
if a.Base != "" || a.Index != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return a.Sym != nil && a.Sym.Pseudo == "" && a.Sym.Name != "" && tab.IsRegister(a.Sym.Name)
|
||||||
|
}
|
||||||
|
|
||||||
// isMacroInvocation reports whether a mnemonic is a macro invocation rather
|
// isMacroInvocation reports whether a mnemonic is a macro invocation rather
|
||||||
// than a machine instruction. No Plan 9 mnemonic contains an underscore, so an
|
// than a machine instruction. No Plan 9 mnemonic contains an underscore, so an
|
||||||
// underscore is a reliable macro marker (the runtime headers define macros such
|
// underscore is a reliable macro marker (the runtime headers define macros such
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -495,3 +496,33 @@ TEXT ·f(SB), NOSPLIT, $0
|
|||||||
t.Fatalf("amd64 must not be flagged: %+v", diags)
|
t.Fatalf("amd64 must not be flagged: %+v", diags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestHasIndirectBranchShape checks that a JMP/CALL through a register or
|
||||||
|
// memory suppresses reachability analysis, while a same-named label does not.
|
||||||
|
func TestHasIndirectBranchShape(t *testing.T) {
|
||||||
|
tab := arch.ForArch(arch.AMD64)
|
||||||
|
indirect := `TEXT ·f(SB), NOSPLIT, $0
|
||||||
|
JMP AX
|
||||||
|
RET
|
||||||
|
`
|
||||||
|
f, errs := parser.Parse("t_amd64.s", indirect)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
if !hasIndirectBranch(f.Decls[0].(*ast.Text), tab) {
|
||||||
|
t.Error("JMP AX: indirect branch not detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
label := `TEXT ·f(SB), NOSPLIT, $0
|
||||||
|
loop:
|
||||||
|
JMP loop
|
||||||
|
RET
|
||||||
|
`
|
||||||
|
f, errs = parser.Parse("t_amd64.s", label)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
if hasIndirectBranch(f.Decls[0].(*ast.Text), tab) {
|
||||||
|
t.Error("JMP loop: label treated as an indirect branch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+18
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// Indirect control flow: JMP/CALL through a register or memory, byte-compared
|
||||||
|
// against go tool asm. A CALL in the body also exercises the toolchain's
|
||||||
|
// forced base-pointer frame on a frameless function.
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func f()
|
||||||
|
TEXT ·f(SB), NOSPLIT, $0
|
||||||
|
JMP AX
|
||||||
|
CALL AX
|
||||||
|
JMP (BX)
|
||||||
|
CALL (BX)
|
||||||
|
JMP 8(BX)
|
||||||
|
JMP R8
|
||||||
|
RET
|
||||||
Vendored
+14
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// Indirect control flow: JMP (R0) and CALL (R0) lower to BR/BLR, the only
|
||||||
|
// indirect-branch spellings the toolchain accepts (the raw BR/BLR mnemonics
|
||||||
|
// stay a gasm superset).
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func f()
|
||||||
|
TEXT ·f(SB), NOSPLIT, $0
|
||||||
|
JMP (R0)
|
||||||
|
CALL (R0)
|
||||||
|
RET
|
||||||
Vendored
+14
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// Indirect control flow: JMP (R4) and JAL (R5) are the toolchain's spellings
|
||||||
|
// for jirl; the raw JIRL instruction is deliberately absent, because the Go
|
||||||
|
// loong64 assembler deletes it and a parity kernel could not hold it.
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func f()
|
||||||
|
TEXT ·f(SB), NOSPLIT, $0-0
|
||||||
|
JMP (R4)
|
||||||
|
JAL (R5)
|
||||||
|
RET
|
||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
// Indirect control flow: JMP (X5) lowers to JALR X0, 0(X5), the trampoline
|
||||||
|
// form JALR rd, offset(rs1) encodes with the destination first, and a linking
|
||||||
|
// JALR through X1 is the toolchain's only indirect call.
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// func f()
|
||||||
|
TEXT ·leaf(SB), NOSPLIT, $0-0
|
||||||
|
JMP (X5)
|
||||||
|
JALR X0, 0(X6)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func g()
|
||||||
|
TEXT ·calls(SB), NOSPLIT, $0-0
|
||||||
|
JALR X1, 0(X8)
|
||||||
|
RET
|
||||||
@@ -39,7 +39,7 @@ TEXT ·enterJITChecked(SB), NOSPLIT, $0-16
|
|||||||
MOVV 0(R5), R1 // load leaveJITCheckedRaw into RA
|
MOVV 0(R5), R1 // load leaveJITCheckedRaw into RA
|
||||||
MOVV R5, R3 // SP stays on the leave slot: the kernel
|
MOVV R5, R3 // SP stays on the leave slot: the kernel
|
||||||
// reads its first argument at SP+8
|
// reads its first argument at SP+8
|
||||||
JIRL R0, R4, 0 // jump to JIT function
|
JMP (R4) // jump to JIT function
|
||||||
|
|
||||||
// leaveJITCheckedRaw is the raw return trampoline. It has NO Go function
|
// leaveJITCheckedRaw is the raw return trampoline. It has NO Go function
|
||||||
// declaration, so no ABIInternal wrapper is generated; the JIT function's
|
// declaration, so no ABIInternal wrapper is generated; the JIT function's
|
||||||
@@ -61,6 +61,6 @@ g_ok:
|
|||||||
MOVV savedRA(SB), R1 // restore return address
|
MOVV savedRA(SB), R1 // restore return address
|
||||||
MOVV savedG(SB), g // restore g: Go code needs it the moment it
|
MOVV savedG(SB), g // restore g: Go code needs it the moment it
|
||||||
// resumes, violation or not
|
// resumes, violation or not
|
||||||
JIRL R0, R1, 0 // return to Go caller
|
JMP (R1) // return to Go caller
|
||||||
|
|
||||||
GLOBL savedG(SB), NOPTR, $8
|
GLOBL savedG(SB), NOPTR, $8
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func TestGroundTruthARM64(t *testing.T) {
|
|||||||
"../testdata/verify/call_arm64.s",
|
"../testdata/verify/call_arm64.s",
|
||||||
"../testdata/verify/bigframe_arm64.s",
|
"../testdata/verify/bigframe_arm64.s",
|
||||||
"../testdata/verify/guard_arm64.s",
|
"../testdata/verify/guard_arm64.s",
|
||||||
|
"../testdata/verify/indirect_arm64.s",
|
||||||
} {
|
} {
|
||||||
t.Run(path, func(t *testing.T) {
|
t.Run(path, func(t *testing.T) {
|
||||||
src, err := os.ReadFile(path)
|
src, err := os.ReadFile(path)
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ func TestGroundTruthAMD64(t *testing.T) {
|
|||||||
"../testdata/verify/basic_amd64.s",
|
"../testdata/verify/basic_amd64.s",
|
||||||
"../testdata/verify/bigframe_amd64.s",
|
"../testdata/verify/bigframe_amd64.s",
|
||||||
"../testdata/verify/guard_amd64.s",
|
"../testdata/verify/guard_amd64.s",
|
||||||
|
"../testdata/verify/indirect_amd64.s",
|
||||||
} {
|
} {
|
||||||
t.Run(path, func(t *testing.T) {
|
t.Run(path, func(t *testing.T) {
|
||||||
f, errs := parser.Parse(path, mustRead(t, path))
|
f, errs := parser.Parse(path, mustRead(t, path))
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ func TestGroundTruthLOONG64(t *testing.T) {
|
|||||||
"../testdata/verify/fp_loong64.s",
|
"../testdata/verify/fp_loong64.s",
|
||||||
"../testdata/verify/bigframe_loong64.s",
|
"../testdata/verify/bigframe_loong64.s",
|
||||||
"../testdata/verify/guard_loong64.s",
|
"../testdata/verify/guard_loong64.s",
|
||||||
|
"../testdata/verify/indirect_loong64.s",
|
||||||
|
"trampoline_loong64.s",
|
||||||
} {
|
} {
|
||||||
t.Run(path, func(t *testing.T) {
|
t.Run(path, func(t *testing.T) {
|
||||||
src, err := os.ReadFile(path)
|
src, err := os.ReadFile(path)
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ func TestGroundTruthRISCV(t *testing.T) {
|
|||||||
"../testdata/verify/call_riscv64.s",
|
"../testdata/verify/call_riscv64.s",
|
||||||
"../testdata/verify/bigframe_riscv64.s",
|
"../testdata/verify/bigframe_riscv64.s",
|
||||||
"../testdata/verify/guard_riscv64.s",
|
"../testdata/verify/guard_riscv64.s",
|
||||||
|
"../testdata/verify/indirect_riscv64.s",
|
||||||
|
"trampoline_riscv64.s",
|
||||||
} {
|
} {
|
||||||
t.Run(path, func(t *testing.T) {
|
t.Run(path, func(t *testing.T) {
|
||||||
testGroundTruthRISCVFile(t, path)
|
testGroundTruthRISCVFile(t, path)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// ABI0 JIT trampoline for LoongArch 64.
|
// ABI0 JIT trampoline for LoongArch 64.
|
||||||
//
|
//
|
||||||
// enterJIT saves Go SP and RA (R1), switches to the prepared stack, and
|
// enterJIT saves Go SP and RA (R1), switches to the prepared stack, and
|
||||||
// jumps to the JIT function. When the function RETs (JIRL zero, ra, 0),
|
// jumps to the JIT function. When the function RETs (JMP (R1), the toolchain's spelling for jirl zero, ra, 0),
|
||||||
// control lands in leaveJIT.
|
// control lands in leaveJIT.
|
||||||
|
|
||||||
// func enterJIT(fn uintptr, stack uintptr)
|
// func enterJIT(fn uintptr, stack uintptr)
|
||||||
@@ -19,14 +19,14 @@ TEXT ·enterJIT(SB), NOSPLIT, $0-16
|
|||||||
MOVV R5, R3 // switch to the prepared stack: SP stays on
|
MOVV R5, R3 // switch to the prepared stack: SP stays on
|
||||||
// the leave slot, so the kernel reads its
|
// the leave slot, so the kernel reads its
|
||||||
// first argument at SP+8
|
// first argument at SP+8
|
||||||
JIRL R0, R4, 0 // jump to JIT function
|
JMP (R4) // jump to JIT function
|
||||||
|
|
||||||
// func leaveJIT()
|
// func leaveJIT()
|
||||||
TEXT ·leaveJIT(SB), NOSPLIT, $0-0
|
TEXT ·leaveJIT(SB), NOSPLIT, $0-0
|
||||||
MOVV savedSP(SB), R5 // restore Go stack pointer
|
MOVV savedSP(SB), R5 // restore Go stack pointer
|
||||||
MOVV R5, R3 // restore SP
|
MOVV R5, R3 // restore SP
|
||||||
MOVV savedRA(SB), R1 // restore return address
|
MOVV savedRA(SB), R1 // restore return address
|
||||||
JIRL R0, R1, 0 // return to Go caller
|
JMP (R1) // return to Go caller
|
||||||
|
|
||||||
GLOBL savedRA(SB), NOPTR, $8
|
GLOBL savedRA(SB), NOPTR, $8
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user