commit 9fd6b74d8e1db3d5afe450867e52e4ea6544456d
parent cfb1975a7bd0c912109521795062f394e28aba18
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 29 May 2026 15:33:22 -0700
aa64 asm: parse :lo12:/:got:/:got_lo12: relocation-operator operands
Adds GNU-as relocation-modifier syntax to the aarch64 standalone assembler:
adrp accepts :got: (R_AARCH64_ADR_GOT_PAGE) vs the default page reloc; add
accepts :lo12: (R_AARCH64_ADD_ABS_LO12_NC); loads/stores accept :lo12:
(size-keyed R_AARCH64_LDST{8,16,32,64}_ABS_LO12_NC) and :got_lo12:
(R_AARCH64_LD64_GOT_LO12_NC). A leading ':' is unambiguous at an operand
position. Corpus aa64_reloc_modifiers is byte- and reloc-identical to llvm-mc
(verified via llvm-objdump -r on the cfree-emitted object).
Diffstat:
4 files changed, 123 insertions(+), 3 deletions(-)
diff --git a/src/arch/aa64/asm.c b/src/arch/aa64/asm.c
@@ -269,6 +269,55 @@ static void parse_imm_sym(AsmDriver* d, ObjSymId* sym_out, i64* val_out) {
asm_driver_parse_sym_expr(d, sym_out, val_out);
}
+/* GNU-as relocation modifier on an aarch64 operand (`:lo12:`, `:got:`,
+ * `:got_lo12:`). AA64_RELMOD_NONE means no modifier was present. */
+typedef enum AA64RelMod {
+ AA64_RELMOD_NONE = 0,
+ AA64_RELMOD_LO12,
+ AA64_RELMOD_GOT,
+ AA64_RELMOD_GOT_LO12,
+} AA64RelMod;
+
+/* If the next token is ':', consume a `:name:` relocation modifier prefix and
+ * return its kind. A leading ':' is unambiguous at an operand position (a
+ * label's ':' only appears at end-of-mnemonic). Returns AA64_RELMOD_NONE and
+ * leaves the stream untouched when there is no modifier. */
+static AA64RelMod parse_reloc_mod(AsmDriver* d) {
+ if (!tok_punct(asm_driver_peek(d), ':')) return AA64_RELMOD_NONE;
+ (void)asm_driver_next(d); /* eat ':' */
+ AsmTok name = asm_driver_next(d);
+ if (name.kind != ASM_TOK_IDENT)
+ asm_driver_panic(d, "asm: expected relocation modifier name after ':'");
+ Slice s = pool_slice(asm_driver_pool(d), name.v.ident);
+ AA64RelMod mod;
+ if (icase_eq(s.s, s.len, "lo12"))
+ mod = AA64_RELMOD_LO12;
+ else if (icase_eq(s.s, s.len, "got"))
+ mod = AA64_RELMOD_GOT;
+ else if (icase_eq(s.s, s.len, "got_lo12"))
+ mod = AA64_RELMOD_GOT_LO12;
+ else
+ asm_driver_panic(d, "asm: unsupported relocation modifier");
+ asm_driver_expect_punct(d, ':', "':' closing relocation modifier");
+ return mod;
+}
+
+/* The R_AARCH64_LDST{8,16,32,64}_ABS_LO12_NC reloc for an access log2-size. */
+static RelocKind aa64_ldst_lo12_reloc(AsmDriver* d, u32 size) {
+ switch (size) {
+ case 0:
+ return R_AARCH64_LDST8_ABS_LO12_NC;
+ case 1:
+ return R_AARCH64_LDST16_ABS_LO12_NC;
+ case 2:
+ return R_AARCH64_LDST32_ABS_LO12_NC;
+ case 3:
+ return R_AARCH64_LDST64_ABS_LO12_NC;
+ default:
+ asm_driver_panic(d, "asm: ldr/str: :lo12: not valid for this access size");
+ }
+}
+
static void emit32(AsmDriver* d, u32 word) {
MCEmitter* mc = asm_driver_mc(d);
(void)asm_driver_cur_section(d);
@@ -585,6 +634,30 @@ static void p_addsub(AsmDriver* d, int is_sub, int set_flags) {
AA64Reg rn = parse_reg(d);
expect_comma(d, "add/sub");
AsmTok t = asm_driver_peek(d);
+ if (!is_sub && !set_flags && tok_punct(t, ':')) {
+ /* `add Rd, Rn, :lo12:sym` — ADD (immediate) with a zero imm12 plus an
+ * R_AARCH64_ADD_ABS_LO12_NC relocation (the low-12 PIC/abs sequence). */
+ AA64RelMod mod = parse_reloc_mod(d);
+ if (mod != AA64_RELMOD_LO12)
+ asm_driver_panic(d, "asm: add: only :lo12: is valid here");
+ if (rd.is64 != rn.is64)
+ asm_driver_panic(d, "asm: add :lo12:: width mismatch");
+ ObjSymId sym = OBJ_SYM_NONE;
+ i64 off = 0;
+ parse_imm_sym(d, &sym, &off);
+ u32 word = aa64_addsubimm_pack((AA64AddSubImm){.sf = rd.is64,
+ .op = 0,
+ .S = 0,
+ .sh = 0,
+ .imm12 = 0,
+ .Rn = rn.num,
+ .Rd = rd.num});
+ emit32(d, word);
+ MCEmitter* mc = asm_driver_mc(d);
+ mc->emit_reloc_at(mc, asm_driver_cur_section(d), mc->pos(mc) - 4,
+ R_AARCH64_ADD_ABS_LO12_NC, sym, off, 1, 0);
+ return;
+ }
if (tok_punct(t, '#') || t.kind == ASM_TOK_NUM || tok_punct(t, '-') ||
tok_punct(t, '+')) {
/* immediate form */
@@ -932,6 +1005,9 @@ typedef struct AA64Mem {
i64 imm; /* byte offset (literal as written) */
u32 option;
u32 shift;
+ AA64RelMod reloc_mod; /* :lo12: / :got_lo12: on the offset, or NONE */
+ ObjSymId reloc_sym; /* symbol when reloc_mod != NONE */
+ i64 reloc_off; /* addend when reloc_mod != NONE */
u8 pre_index;
u8 post_index;
u8 has_offset;
@@ -992,11 +1068,17 @@ static AA64Mem parse_mem(AsmDriver* d) {
asm_driver_panic(d, "asm: ldr/str: base register must be 64-bit");
require_sp_spelling(d, m.base, "ldr/str base");
if (asm_driver_eat_comma(d)) {
- /* Either `#imm`/expression (immediate offset) or a register index. */
+ /* `:lo12:sym` / `:got_lo12:sym` relocation offset, a register index, or a
+ * plain `#imm`/expression. */
AsmTok t = asm_driver_peek(d);
AA64Reg idx;
memset(&idx, 0, sizeof idx);
- if (t.kind == ASM_TOK_IDENT && parse_reg_from_ident(d, t.v.ident, &idx)) {
+ if (tok_punct(t, ':')) {
+ m.reloc_mod = parse_reloc_mod(d);
+ parse_imm_sym(d, &m.reloc_sym, &m.reloc_off);
+ m.has_offset = 1; /* imm field stays 0; reloc supplies the low bits */
+ } else if (t.kind == ASM_TOK_IDENT &&
+ parse_reg_from_ident(d, t.v.ident, &idx)) {
(void)asm_driver_next(d);
reject_sp_reg(d, idx, "ldr/str index");
m.index = idx;
@@ -1041,6 +1123,24 @@ static void p_ldst_core(AsmDriver* d, int is_load, int fixed_size,
: !sign_ext ? AA64_LDST_OPC_LDR
: rt.is64 ? 2u /* LDRS*, 64-bit dst */
: 3u; /* LDRS*, 32-bit dst */
+ if (m.reloc_mod != AA64_RELMOD_NONE) {
+ /* [Xn, :lo12:sym] / [Xn, :got_lo12:sym] — unsigned-imm12 form with a zero
+ * immediate; the relocation supplies the low 12 bits. */
+ u32 word = aa64_ldst_uimm_pack((AA64LdStUimm){.size = size,
+ .V = 0,
+ .opc = opc,
+ .imm12 = 0,
+ .Rn = m.base.num,
+ .Rt = rt.num});
+ emit32(d, word);
+ RelocKind k = (m.reloc_mod == AA64_RELMOD_GOT_LO12)
+ ? R_AARCH64_LD64_GOT_LO12_NC
+ : aa64_ldst_lo12_reloc(d, size);
+ MCEmitter* mc = asm_driver_mc(d);
+ mc->emit_reloc_at(mc, asm_driver_cur_section(d), mc->pos(mc) - 4, k,
+ m.reloc_sym, m.reloc_off, 1, 0);
+ return;
+ }
if (m.has_index) {
/* Register-offset form. The S bit (scale by access size) is set
* when an explicit shift was written; the amount must equal the
@@ -1178,6 +1278,11 @@ static void p_ldp_stp(AsmDriver* d, int is_load) {
static void p_adr(AsmDriver* d, int is_adrp) {
AA64Reg rd = parse_reg(d);
expect_comma(d, "adr");
+ /* `adrp Rd, :got:sym` selects the GOT-page relocation; a bare symbol uses
+ * the PC-relative page reloc. `:got:` is only meaningful for adrp. */
+ AA64RelMod mod = parse_reloc_mod(d);
+ if (mod != AA64_RELMOD_NONE && (!is_adrp || mod != AA64_RELMOD_GOT))
+ asm_driver_panic(d, "asm: adr/adrp: only :got: (with adrp) is valid here");
ObjSymId sym = OBJ_SYM_NONE;
i64 off = 0;
parse_imm_sym(d, &sym, &off);
@@ -1190,7 +1295,9 @@ static void p_adr(AsmDriver* d, int is_adrp) {
emit32(d, aa64_pcrel_adr_pack(f));
MCEmitter* mc = asm_driver_mc(d);
u32 ofs = mc->pos(mc) - 4;
- RelocKind k = is_adrp ? R_AARCH64_ADR_PREL_PG_HI21 : R_AARCH64_ADR_PREL_LO21;
+ RelocKind k = !is_adrp ? R_AARCH64_ADR_PREL_LO21
+ : mod == AA64_RELMOD_GOT ? R_AARCH64_ADR_GOT_PAGE
+ : R_AARCH64_ADR_PREL_PG_HI21;
mc->emit_reloc_at(mc, asm_driver_cur_section(d), ofs, k, sym, off, 1, 0);
}
diff --git a/test/asm/encode/aa64_reloc_modifiers.expected.hex b/test/asm/encode/aa64_reloc_modifiers.expected.hex
@@ -0,0 +1 @@
+0000009000000091010040f902004039010000f903000090630040f9c0035fd6
diff --git a/test/asm/encode/aa64_reloc_modifiers.s b/test/asm/encode/aa64_reloc_modifiers.s
@@ -0,0 +1,10 @@
+.text
+t:
+ adrp x0, sym
+ add x0, x0, :lo12:sym
+ ldr x1, [x0, :lo12:sym]
+ ldrb w2, [x0, :lo12:sym]
+ str x1, [x0, :lo12:sym]
+ adrp x3, :got:sym
+ ldr x3, [x3, :got_lo12:sym]
+ ret
diff --git a/test/asm/encode/aa64_reloc_modifiers.targets b/test/asm/encode/aa64_reloc_modifiers.targets
@@ -0,0 +1 @@
+aa64
+\ No newline at end of file