From 7b11c62f5369571e3a68d67f6d38ec4afb32e18e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Wed, 23 Sep 2026 21:02:50 +0200 Subject: [PATCH] fix(asm): resolve negative numeric PC-relative jumps Assisted-by: GLM 5.3 Flash --- CHANGELOG.md | 5 +++++ asm/assemble.go | 18 ++++++++++++++- asm/assemble_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e7adbc..933883a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 to the document that started it, so renaming a symbol used in a second file moved that file's text into the first. Each edit now applies to the document it was collected in. +- **Negative numeric PC-relative jumps.** `JMP -3(PC)`, the shape the + runtime's exit loops write (sys_linux_amd64.s, sys_netbsd_amd64.s), + resolved to nothing: only the forward forms counted. A negative count + now walks the same instruction statements backwards, labels excluded, + byte-identical with the toolchain. ## [0.35.0] - 2026-09-22 diff --git a/asm/assemble.go b/asm/assemble.go index 3b46849..e72225f 100644 --- a/asm/assemble.go +++ b/asm/assemble.go @@ -543,11 +543,27 @@ func pcJumpOffset(op *ast.Operand) (int, bool) { // pcJumpTarget resolves a numeric jump at statement index j: N counts the // instruction statements after the jump itself (N = 0 is the jump's own // address, the classic park loop), and the target is the start of the Nth -// one. It reports false when the count runs past the end of the function. +// one. A negative N counts the same way backwards, before the jump: the +// exit loops write JMP -3(PC) to land three instructions earlier. Labels +// count not, in either direction. It reports false when the count runs +// past the end of the function, or before its first instruction. func pcJumpTarget(t *ast.Text, j, n int, pcs []int) (int, bool) { if n == 0 { return pcs[j], true } + if n < 0 { + seen := 0 + for k := j - 1; k >= 0; k-- { + if _, ok := t.Body[k].(*ast.Instr); !ok { + continue + } + seen-- + if seen == n { + return pcs[k], true + } + } + return 0, false + } seen := 0 for k := j + 1; k < len(t.Body); k++ { if _, ok := t.Body[k].(*ast.Instr); !ok { diff --git a/asm/assemble_test.go b/asm/assemble_test.go index 84ee798..67a2273 100644 --- a/asm/assemble_test.go +++ b/asm/assemble_test.go @@ -370,6 +370,58 @@ end: } } +// TestAssembleNumericPCJumps pins the numeric ±N(PC) branch operands: N +// counts instruction statements, skipping labels, in both directions (the +// runtime's exit loops write JMP -3(PC)), N = 0 parks on the jump itself. +func TestAssembleNumericPCJumps(t *testing.T) { + fn := firstText(t, ` +#include "textflag.h" +TEXT ·exit(SB), NOSPLIT, $0 + MOVB $1, AL +lab: + MOVB $2, AL + MOVB $3, AL + JMP -3(PC) + MOVB $4, AL +park: + JMP 0(PC) + MOVB $5, AL + JMP 2(PC) + MOVB $6, AL + RET +`) + code, _, err := Assemble(fn) + if err != nil { + t.Fatalf("Assemble: %v", err) + } + // From the Go-assembled function: + // MOVB $1, AL b001 + // MOVB $2, AL b002 + // MOVB $3, AL b003 + // JMP -3(PC) ebf8 (three instructions back, past lab:) + // MOVB $4, AL b004 + // JMP 0(PC) ebfe (the park loop) + // MOVB $5, AL b005 + // JMP 2(PC) eb02 (over MOVB $6 to the RET) + // MOVB $6, AL b006 + // RET c3 + want := []byte{ + 0xb0, 0x01, + 0xb0, 0x02, + 0xb0, 0x03, + 0xeb, 0xf8, + 0xb0, 0x04, + 0xeb, 0xfe, + 0xb0, 0x05, + 0xeb, 0x02, + 0xb0, 0x06, + 0xc3, + } + if hexBytes(code) != hexBytes(want) { + t.Errorf("numeric-PC mismatch:\n got: %s\n want: %s", hexBytes(code), hexBytes(want)) + } +} + func TestAssemblePrefetch(t *testing.T) { fn := firstText(t, ` #include "textflag.h"