feat(asm): encode the amd64 and loong64 tails of the corpus testdata

Assisted-by: GLM 5.3
This commit is contained in:
2026-10-02 00:40:43 +02:00
parent 2f679326c2
commit bafb2fd130
12 changed files with 1404 additions and 81 deletions
+218 -21
View File
@@ -808,17 +808,43 @@ func isJumpMnemonic(mnem string) bool {
if mnem == "JMP" || mnem == "CALL" {
return true
}
if isLoopMnemonic(mnem) {
return true
}
_, ok := condCode(mnem)
return ok
}
// isLoopMnemonic reports the LOOP family, rel8 alone (E0-E2).
func isLoopMnemonic(mnem string) bool {
switch mnem {
case "LOOP", "LOOPE", "LOOPNE":
return true
}
return false
}
// loopOpcode maps the LOOP family to its E0-E2 opcode.
func loopOpcode(mnem string) byte {
switch mnem {
case "LOOPE":
return 0xE1
case "LOOPNE":
return 0xE0
}
return 0xE2
}
// jumpSize returns the length of a jump instruction in the requested form:
// short (rel8) where available, otherwise the rel32 form. CALL is always
// rel32.
// rel32; the LOOP family is rel8 alone.
func jumpSize(mnem string, long bool) int {
if mnem == "CALL" {
return 5 // opcode + rel32
}
if isLoopMnemonic(mnem) {
return 2 // opcode + rel8, the only form
}
if !long {
return 2 // opcode + rel8
}
@@ -945,6 +971,16 @@ func encodeNormal(s *ast.Instr, fi frameInfo, link *linkInfo) ([]byte, []sbPatch
if (mnemUpper == "MOVQ" || mnemUpper == "MOVL") && len(s.Operands) == 2 && isBareTLS(s.Operands[0]) {
return encodeTLSBaseLoad(s, fi, link)
}
// The old paired-register shift spelling, SHLL CX, R11:AX (a colon
// between the two registers), is the toolchain's SHLD family: SHLDL CL,
// AX, R11 with the count register first, the paired source in the reg
// field and the pair's head in r/m.
if code, ps, err := encodeColonShift(s, mnemUpper, fi, link); code != nil || err != nil {
if err != nil {
return nil, nil, nil, err
}
return code, ps, nil, nil
}
_, size := splitSize(mnemUpper)
if size == 0 {
size = 8
@@ -1051,6 +1087,66 @@ func encodeBookkeeping(upper string, s *ast.Instr) ([]byte, error) {
return nil, nil
}
// encodeColonShift encodes the paired-register shift spellings, SHLx CX,
// dst:src: the toolchain reads them as the SHLD family (double-precision
// shift by CL), reg = the paired source, r/m = the pair's head. The second
// operand's raw text carries the colon; ok reports the spelling was found.
func encodeColonShift(s *ast.Instr, mnemUpper string, fi frameInfo, link *linkInfo) ([]byte, []sbPatch, error) {
base, _ := strings.CutPrefix(mnemUpper, "SHL")
if base == mnemUpper || len(s.Operands) != 2 {
return nil, nil, nil
}
_, size := splitSize(mnemUpper)
raw := strings.ReplaceAll(s.Operands[1].Raw, " ", "")
head, tail, ok := strings.Cut(raw, ":")
if !ok || head == "" || tail == "" {
return nil, nil, nil
}
headReg, ok1 := ParseReg(head)
srcReg, ok2 := ParseReg(tail)
if !ok1 || !ok2 {
return nil, nil, fmt.Errorf("%s: invalid paired register %q", mnemUpper, s.Operands[1].Raw)
}
cnt, err := operandFromAST(mnemUpper, s.Operands[0], size, fi, link)
if err != nil {
return nil, nil, err
}
cntReg, ok := cnt.(Reg)
if !ok || cntReg.idx != 1 {
return nil, nil, fmt.Errorf("%s: the paired-register form counts in CL", mnemUpper)
}
// SHLD r/m, reg, CL: 0F A5 (REX.W for the 64-bit width).
e := &enc{}
i := &instr{rexW: size == 8, opcode: []byte{0x0F, 0xA5}, modrm: -1, sib: -1}
if err := setRM(i, srcReg, headReg, size); err != nil {
return nil, nil, err
}
if err := e.emit(i); err != nil {
return nil, nil, err
}
ps := make([]sbPatch, len(e.patches))
for j, p := range e.patches {
ps[j] = sbPatch{off: p.off, name: p.name, addend: p.addend}
}
return e.out, ps, nil
}
// trailingIndexGroup recovers a trailing "(index*scale)" or "(index)" group
// from an operand's raw text: the symbol-pseudo parse returns before the
// index group, so foo(SP)(AX*1) keeps its index only in the spelling.
func trailingIndexGroup(raw string) (string, int, bool) {
compact := strings.ReplaceAll(raw, " ", "")
if !strings.HasSuffix(compact, ")") {
return "", 0, false
}
open := strings.LastIndex(compact, "(")
if open < 2 || !strings.Contains(compact[:open], ")") {
return "", 0, false // one group alone: no trailing index
}
name, scale, _, ok := cutParenGroup(compact[open:])
return name, scale, ok
}
// encodeJump encodes a JMP/CALL/Jcc with a relative offset resolved from the
// target label or from a numeric ±N(PC) instruction count, in the short
// (rel8) or long (rel32) form. numTarget is the resolved byte offset of a
@@ -1085,9 +1181,15 @@ func encodeJump(s *ast.Instr, mnem string, pc int, offsets map[string]int, long
if mnem == "JMP" {
return []byte{0xEB, byte(int8(rel))}, nil
}
if isLoopMnemonic(mnem) {
return []byte{loopOpcode(mnem), byte(int8(rel))}, nil
}
cc, _ := condCode(mnem)
return []byte{0x70 + byte(cc), byte(int8(rel))}, nil
}
if isLoopMnemonic(mnem) {
return nil, fmt.Errorf("%s has no long form", mnem)
}
switch mnem {
case "JMP":
return append([]byte{0xE9}, le32(rel)...), nil
@@ -1139,6 +1241,72 @@ func labelName(op *ast.Operand) (string, bool) {
return "", false
}
// jumpOperand returns the branch-target operand of a JMP/CALL, rewriting the
// `*`-prefixed indirect spellings (JMP *(R12), JMP *4(SP)) into their plain
// memory form. The star marks an indirect target and changes no bytes; the
// address parser leaves the operand's address empty because of the leading
// star, so the fields are rebuilt from the raw text onto a copy of the
// operand, never on the shared syntax tree.
func jumpOperand(s *ast.Instr) *ast.Operand {
if len(s.Operands) != 1 {
return nil
}
op := s.Operands[0]
compact := strings.ReplaceAll(op.Raw, " ", "")
inner, ok := strings.CutPrefix(compact, "*")
if !ok {
return op
}
var addr ast.Address
if i := strings.IndexByte(inner, '('); i > 0 {
v, err := strconv.ParseInt(inner[:i], 0, 64)
if err != nil {
return op
}
addr.Offset, addr.HasOff = v, true
inner = inner[i:]
}
base, _, rest, ok := cutParenGroup(inner)
if !ok {
return op
}
if base != "" {
addr.Base = base
}
if rest != "" {
idx, scale, _, ok := cutParenGroup(rest)
if ok && idx != "" {
addr.Index = idx
addr.Scale = scale
}
}
c := *op
c.Addr = addr
return &c
}
// cutParenGroup splits a leading "(name)" or "(name*n)" off s, returning the
// inner text, the scale it names (1 when the group spells no multiplier) and
// the remainder.
func cutParenGroup(s string) (name string, scale int, rest string, ok bool) {
if !strings.HasPrefix(s, "(") {
return "", 0, "", false
}
i := strings.IndexByte(s, ')')
if i < 0 {
return "", 0, "", false
}
inner, rest := s[1:i], s[i+1:]
if before, after, ok := strings.Cut(inner, "*"); ok {
n, err := strconv.Atoi(after)
if err != nil {
return "", 0, "", false
}
return before, n, rest, true
}
return inner, 1, rest, true
}
// 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
@@ -1147,7 +1315,14 @@ func indirectJumpTarget(s *ast.Instr) bool {
if len(s.Operands) != 1 || s.Operands[0].Kind != ast.OpAddr {
return false
}
a := s.Operands[0].Addr
op := jumpOperand(s)
if op == nil {
return false
}
if op != s.Operands[0] {
return true // the star marker spells an indirect target
}
a := op.Addr
// ±N(PC) is the numeric relative form, the PC counts instructions from
// the branch: relative, not indirect.
if a.Base == "PC" || a.Index == "PC" {
@@ -1165,18 +1340,20 @@ func indirectJumpTarget(s *ast.Instr) bool {
}
// encodeIndirectJump assembles a JMP/CALL through a register or memory
// operand, which carries no relocation and no label to resolve.
// operand, which carries no relocation and no label to resolve. The
// `*`-prefixed spellings go through jumpOperand first, their star rebuilt
// into a plain memory operand.
func encodeIndirectJump(s *ast.Instr, mnem string) ([]byte, error) {
ops := make([]Operand, len(s.Operands))
for i, op := range s.Operands {
o, err := operandFromAST(mnem, op, 8, frameInfo{}, nil)
if err != nil {
return nil, err
}
ops[i] = o
op := s.Operands[0]
if cleaned := jumpOperand(s); cleaned != nil {
op = cleaned
}
o, err := operandFromAST(mnem, op, 8, frameInfo{}, nil)
if err != nil {
return nil, err
}
e := &enc{}
if err := e.encodeIndirectBranch(mnem, ops); err != nil {
if err := e.encodeIndirectBranch(mnem, []Operand{o}); err != nil {
return nil, err
}
return e.out, nil
@@ -1245,31 +1422,51 @@ func operandFromAST(mnemUpper string, op *ast.Operand, size int, fi frameInfo, l
off := a.Sym.Offset + fi.fpAdjust
return Mem{Base: spReg, Disp: off, HasBase: true, Size: size}, nil
}
// SP-relative local: x-N(SP) → (spAdjust + offset)(SP).
// SP-relative local: x-N(SP) → (spAdjust + offset)(SP), keeping a scaled
// index beside the virtual stack pointer (foo(SP)(AX*1)). The
// symbol-pseudo parse returns before the index group, so the index
// is recovered from the raw text when the address lacks it.
if a.Sym != nil && a.Sym.Pseudo == "SP" && a.Base == "" {
off := fi.spAdjust + a.Sym.Offset
return Mem{Base: spReg, Disp: off, HasBase: true, Size: size}, nil
m := Mem{Base: spReg, Disp: off, HasBase: true, Size: size}
if name, scale, ok := trailingIndexGroup(op.Raw); ok {
idx, ok := ParseReg(name)
if !ok {
return nil, fmt.Errorf("unknown index register %q", name)
}
m.Index = idx
m.Scale = scale
m.HasIndex = true
}
return m, nil
}
// SB (global symbol): a symbol defined in the same file (GLOBL) is
// encoded RIP-relative and resolved by the file-level layout;
// anything not defined here needs object-file emission.
// anything not defined here needs object-file emission. A static
// (file-local) spelling of an undefined symbol defers the same way
// the toolchain does: the relocation names it and the linker decides.
if a.Sym != nil && a.Sym.Pseudo == "SB" {
if link == nil || link.symbols == nil {
return nil, fmt.Errorf("symbol %q needs file-level assembly (AssembleFile)", a.Sym.Name)
}
if !link.symbols[a.Sym.Name] {
if a.Sym.Static {
return nil, fmt.Errorf("undefined symbol %q", a.Sym.Name)
}
if !link.allowExternal {
return nil, fmt.Errorf("external symbol %q needs object-file emission", a.Sym.Name)
}
if !link.symbols[a.Sym.Name] && !link.allowExternal {
return nil, fmt.Errorf("external symbol %q needs object-file emission", a.Sym.Name)
}
return sbMem{size: size, name: a.Sym.Name, addend: a.Sym.Offset}, nil
}
// Memory with a real base register: (base), off(base), (base)(index*scale).
if a.Base != "" {
// The TLS pseudo-base, off(TLS): the segment-prefixed absolute
// the thread-local access lowers to, 64 8B 04 25 with its
// R_TLS_LE patch site on the disp32.
if a.Base == "TLS" {
seg := byte(0x64) // FS on linux, freebsd, plan9
if link != nil && link.goos == "windows" {
seg = 0x65 // GS
}
return TLSMem{Disp: a.Offset, Size: size, Seg: seg}, nil
}
// Segment-absolute: 0x30(GS) and 0x28(FS), the windows TLS
// spellings. The segment override prefixes a disp32 absolute
// reference with no relocation.