[1/2] RISC-V: Add ARC-V APEX assembler support

Message ID 20260525133052.1885254-2-luiss@synopsys.com
State New
Headers
Series RISC-V: Add ARC-V APEX assembler and disassembler support |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_binutils_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_binutils_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_binutils_check--master-aarch64 success Test passed
linaro-tcwg-bot/tcwg_binutils_check--master-arm success Test passed

Commit Message

Luis Silva May 25, 2026, 1:30 p.m. UTC
  Add support for ARC-V APEX (ARC Processor EXtension), which allows users to
dynamically define custom instructions that use the Custom-0 encoding space.

APEX defines four instruction formats:
  - XD: 3-register (up to 256 sub-opcodes)
  - XS: 2-register + 8-bit signed immediate (up to 64 sub-opcodes)
  - XI: 1-register + 12-bit signed immediate (up to 32 sub-opcodes)
  - XC: compressed form where rd == rs1 (up to 32 sub-opcodes)

Instructions are registered at assembly via the .extInstruction directive:

  .extInstruction <name>, <sub_opcode>, <format>[, <attributes>]

Each registered instruction emits a metadata record into a COMDAT ELF
section so that instruction definitions are preserved in the linked
object.

gas/ChangeLog:

	* config/tc-riscv.c (validate_riscv_insn): Add APEX vendor
	operand validation.
	(riscv_ip): Add 'X' vendor operand handling for APEX.
	(arcv_apex_xs_xc_pair_p): New function.
	(arcv_apex_validate_insn): New function.
	(arcv_apex_check_and_set_insn): New function.
	(arcv_apex_get_metadata_section): New function.
	(arcv_apex_write_metadata): New function.
	(arcv_apex_register_insn): New function.
	(arcv_apex_section_parser): New function.
	* doc/c-riscv.texi: Document .extInstruction directive
	* testsuite/gas/riscv/x-arcv-apex-01.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-01.s: New test.
	* testsuite/gas/riscv/x-arcv-apex-02.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-02.s: New test.
	* testsuite/gas/riscv/x-arcv-apex-03.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-04.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-04.s: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-01.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-01.l: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-01.s: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-02.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-02.l: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-02.s: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-03.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-03.l: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-03.s: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-04.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-04.l: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-04.s: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-05.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-05.l: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-05.s: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-06.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-06.l: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-06.s: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-07.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-07.l: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-07.s: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-08.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-08.l: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-08.s: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-09.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-09.l: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-09.s: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-10.d: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-10.l: New test.
	* testsuite/gas/riscv/x-arcv-apex-fail-10.s: New test.

include/ChangeLog:

	* opcode/riscv.h (arcv_apex_match_rd_ne_rs1): New declaration.
	(arcv_apex_init_dynamic_insn): Likewise.
	(arcv_apex_setup_xd_insn): Likewise.
	(arcv_apex_setup_xs_insn): Likewise.
	(arcv_apex_setup_xi_insn): Likewise.
	(arcv_apex_setup_xc_insn): Likewise.
	(enum apex_flags): New enum.
	(struct apex_insn): New struct.
	Add APEX encoding masks, offsets, and EXTRACT/ENCODE macros.

ld/ChangeLog:

	* testsuite/ld-riscv-elf/ld-riscv-elf.exp: Add APEX tests.
	* testsuite/ld-riscv-elf/x-arcv-apex-01-a.s: New test.
	* testsuite/ld-riscv-elf/x-arcv-apex-01-b.s: New test.
	* testsuite/ld-riscv-elf/x-arcv-apex-01-c.s: New test.
	* testsuite/ld-riscv-elf/x-arcv-apex-01.d: New test.
	* testsuite/ld-riscv-elf/x-arcv-apex-02.d: New test.

opcodes/ChangeLog:

	* riscv-opc.c (arcv_apex_init_dynamic_insn): New function.
	(arcv_apex_match_rd_ne_rs1): New function.
	(arcv_apex_setup_xd_insn): New function.
	(arcv_apex_setup_xs_insn): New function.
	(arcv_apex_setup_xi_insn): New function.
	(arcv_apex_setup_xc_insn): New function.

Co-Authored-By: Alex Turjan <turjan@synopsys.com>
Signed-off-by: Luis Silva <luiss@synopsys.com>
---
 gas/config/tc-riscv.c                         | 631 ++++++++++++++++++
 gas/doc/c-riscv.texi                          |  53 ++
 gas/testsuite/gas/riscv/x-arcv-apex-01.d      |  18 +
 gas/testsuite/gas/riscv/x-arcv-apex-01.s      |  19 +
 gas/testsuite/gas/riscv/x-arcv-apex-02.d      |  13 +
 gas/testsuite/gas/riscv/x-arcv-apex-02.s      |   6 +
 gas/testsuite/gas/riscv/x-arcv-apex-03.d      |  46 ++
 gas/testsuite/gas/riscv/x-arcv-apex-04.d      |  15 +
 gas/testsuite/gas/riscv/x-arcv-apex-04.s      |  10 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-01.d |   1 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-01.l |   2 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-01.s |   1 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-02.d |   1 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-02.l |   5 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-02.s |   3 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-03.d |   1 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-03.l |   9 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-03.s |  12 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-04.d |   1 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-04.l |   3 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-04.s |   2 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-05.d |   1 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-05.l |   3 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-05.s |   3 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-06.d |   1 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-06.l |   7 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-06.s |   7 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-07.d |   1 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-07.l |   9 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-07.s |   9 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-08.d |   1 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-08.l |   5 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-08.s |   7 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-09.d |   1 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-09.l |   3 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-09.s |   1 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-10.d |   1 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-10.l |   3 +
 gas/testsuite/gas/riscv/x-arcv-apex-fail-10.s |   2 +
 include/opcode/riscv.h                        |  68 ++
 ld/testsuite/ld-riscv-elf/ld-riscv-elf.exp    |  10 +
 ld/testsuite/ld-riscv-elf/x-arcv-apex-01-a.s  |   8 +
 ld/testsuite/ld-riscv-elf/x-arcv-apex-01-b.s  |  21 +
 ld/testsuite/ld-riscv-elf/x-arcv-apex-01-c.s  |  21 +
 ld/testsuite/ld-riscv-elf/x-arcv-apex-01.d    |  32 +
 ld/testsuite/ld-riscv-elf/x-arcv-apex-02.d    |  36 +
 opcodes/riscv-opc.c                           | 140 ++++
 47 files changed, 1253 insertions(+)
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-01.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-01.s
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-02.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-02.s
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-03.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-04.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-04.s
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-01.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-01.l
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-01.s
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-02.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-02.l
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-02.s
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-03.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-03.l
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-03.s
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-04.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-04.l
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-04.s
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-05.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-05.l
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-05.s
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-06.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-06.l
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-06.s
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-07.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-07.l
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-07.s
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-08.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-08.l
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-08.s
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-09.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-09.l
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-09.s
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-10.d
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-10.l
 create mode 100644 gas/testsuite/gas/riscv/x-arcv-apex-fail-10.s
 create mode 100644 ld/testsuite/ld-riscv-elf/x-arcv-apex-01-a.s
 create mode 100644 ld/testsuite/ld-riscv-elf/x-arcv-apex-01-b.s
 create mode 100644 ld/testsuite/ld-riscv-elf/x-arcv-apex-01-c.s
 create mode 100644 ld/testsuite/ld-riscv-elf/x-arcv-apex-01.d
 create mode 100644 ld/testsuite/ld-riscv-elf/x-arcv-apex-02.d
  

Comments

Jan Beulich June 2, 2026, 9:12 a.m. UTC | #1
On 25.05.2026 15:30, Luis Silva wrote:
> --- a/gas/doc/c-riscv.texi
> +++ b/gas/doc/c-riscv.texi
> @@ -265,6 +265,59 @@ Floating point constructors for the bfloat16 type, example usage:
>  	.bfloat16 0b:ffc1
>  @end smallexample
>  
> +@cindex @code{.extInstruction} directive, RISC-V

To me this directive is too long. .extinsn perhaps, or even one of .einsn or
.xinsn? Otoh ...

> +@cindex ARC-V APEX instructions
> +@item .extInstruction @var{name}, @var{opcode}, @var{attrs}@dots{}
> +Define a dynamic APEX instruction that uses the Custom-0 encoding space.

... the ARC-V / APEX aspect isn't encoded at all in the directive name. Some
other vendor could come up with some different scheme. So maybe .apexinsn?
Or one of the earlier suggested names, but with a first operand required to
be "apex" for this variant?

> +The @var{name} is the mnemonic used in subsequent assembly.  The
> +@var{opcode} is the function code (sub-opcode) whose valid range depends
> +on the format.  The @var{attrs} are a comma-separated list drawn from:
> +
> +@table @code
> +@item XD
> +XD format (3 register operands, 8-bit sub-opcode, range 0--255).
> +@item XS
> +XS format (2 registers + 8-bit signed immediate, 6-bit sub-opcode,
> +range 0--63).
> +@item XI
> +XI format (1 register + 12-bit signed immediate, 5-bit sub-opcode,
> +range 0--31).
> +@item XC
> +XC format (1 register acting as both destination and source +
> +12-bit signed immediate, 5-bit sub-opcode, range 0--31).

What's the difference between XI and XC? The description says "compressed"
for the latter, but the encoding space needed (without the major opcode)
is 5 + 12 + 5 = 22, i.e. way beyond 16 bits. That's really no different
from XI. What am I missing?

> +@item void
> +Omit the destination register from the operand list.
> +@item no_src0
> +Omit the first source register from the operand list.
> +@item no_src1
> +Omit the second source register (or immediate) from the operand list.
> +@end table
> +
> +Exactly one of @code{XD}, @code{XS}, @code{XI}, or @code{XC} must be
> +specified.  @code{XS} and @code{XC} may be combined;

This contradicts the first sentence. That earlier sentence would better be
written to avoid such a contradiction, e.g. "With one exception, eaxctly
one ...".

> in that case the
> +assembler uses the XS encoding when the destination and source registers
> +differ (and the immediate fits in 8 bits), and the XC encoding when the
> +registers are the same or the immediate requires 12 bits.

This can't be correct: XC, as per what's said elsewhere, requires RD == RS1,
and hence the form can't be used when the immediate requires 12 bits but the
two registers don't match.

> +Examples:
> +
> +@smallexample
> +	.extInstruction myalu, 1, XD
> +	myalu a0, a1, a2
> +
> +	.extInstruction myimm, 5, XS
> +	myimm a0, a1, 42
> +
> +	.extInstruction myflex, 3, XS, XC
> +	myflex a0, a1, 10
> +	myflex a0, a0, 1000
> +@end smallexample
> +
> +The assembler emits metadata about each @code{.extInstruction} into
> +ELF sections named @code{.riscvapex.@var{fmt}.@var{custom0}.@var{opcode}},
> +which the disassembler reads to reconstruct instruction names and
> +operand formats.

So these sections exist solely for the disassembler's convenience? I.e.
them getting stripped (or them not being understood) isn't a functional
issue?

Jan
  
Silva, Luis June 5, 2026, 3:54 p.m. UTC | #2
>> --- a/gas/doc/c-riscv.texi
>> +++ b/gas/doc/c-riscv.texi
>> @@ -265,6 +265,59 @@ Floating point constructors for the bfloat16 type, example usage:
>>       .bfloat16 0b:ffc1
>>  @end smallexample
>>
>> +@cindex @code{.extInstruction} directive, RISC-V

> To me this directive is too long. .extinsn perhaps, or even one of .einsn or
> .xinsn? Otoh ...

>> +@cindex ARC-V APEX instructions
>> +@item .extInstruction @var{name}, @var{opcode}, @var{attrs}@dots{}
>> +Define a dynamic APEX instruction that uses the Custom-0 encoding space.

> ... the ARC-V / APEX aspect isn't encoded at all in the directive name. Some
> other vendor could come up with some different scheme. So maybe .apexinsn?
> Or one of the earlier suggested names, but with a first operand required to
> be "apex" for this variant?

The existing ARC support in binutils already has a comparable mechanism in place
that serves the same purpose, so this directive name isn’t new.

>> +The @var{name} is the mnemonic used in subsequent assembly.  The
>> +@var{opcode} is the function code (sub-opcode) whose valid range depends
>> +on the format.  The @var{attrs} are a comma-separated list drawn from:
>> +
>> +@table @code
>> +@item XD
>> +XD format (3 register operands, 8-bit sub-opcode, range 0--255).
>> +@item XS
>> +XS format (2 registers + 8-bit signed immediate, 6-bit sub-opcode,
>> +range 0--63).
>> +@item XI
>> +XI format (1 register + 12-bit signed immediate, 5-bit sub-opcode,
>> +range 0--31).
>> +@item XC
>> +XC format (1 register acting as both destination and source +
>> +12-bit signed immediate, 5-bit sub-opcode, range 0--31).

> What's the difference between XI and XC? The description says "compressed"
> for the latter, but the encoding space needed (without the major opcode)
> is 5 + 12 + 5 = 22, i.e. way beyond 16 bits. That's really no different
> from XI. What am I missing?

Indeed, "compressed" was the wrong term here; XC is not a 16-bit compressed
instruction.

The difference is that XI has no source register, while XC uses the destination
register as an implicit source register.  Therefore XI has 2 operands
(`rd`, `imm`), whereas XC behaves as 3 operands (`rd/rs1`, `imm`).

>> +@item void
>> +Omit the destination register from the operand list.
>> +@item no_src0
>> +Omit the first source register from the operand list.
>> +@item no_src1
>> +Omit the second source register (or immediate) from the operand list.
>> +@end table
>> +
>> +Exactly one of @code{XD}, @code{XS}, @code{XI}, or @code{XC} must be
>> +specified.  @code{XS} and @code{XC} may be combined;

> This contradicts the first sentence. That earlier sentence would better be
> written to avoid such a contradiction, e.g. "With one exception, eaxctly
> one ...".

Good point. The current wording is contradictory: "Exactly one" conflicts with
the later statement that XS and XC may be combined.  I'll rephrase it to make the
exception explicit.

>> in that case the
>> +assembler uses the XS encoding when the destination and source registers
>> +differ (and the immediate fits in 8 bits), and the XC encoding when the
>> +registers are the same or the immediate requires 12 bits.

> This can't be correct: XC, as per what's said elsewhere, requires RD == RS1,
> and hence the form can't be used when the immediate requires 12 bits but the
> two registers don't match.

You're right, my wording is incorrect.

XC is only valid when RD == RS1.  The intention was to describe selection between
XS and XC based on both register equality and immediate range, but the current
sentence incorrectly suggests XC could be chosen based solely on immediate size.

I'll rephrase this to make the register constraint explicit and ensure XC is
only used when RD == RS1.

>> +Examples:
>> +
>> +@smallexample
>> +     .extInstruction myalu, 1, XD
>> +     myalu a0, a1, a2
>> +
>> +     .extInstruction myimm, 5, XS
>> +     myimm a0, a1, 42
>> +
>> +     .extInstruction myflex, 3, XS, XC
>> +     myflex a0, a1, 10
>> +     myflex a0, a0, 1000
>> +@end smallexample
>> +
>> +The assembler emits metadata about each @code{.extInstruction} into
>> +ELF sections named @code{.riscvapex.@var{fmt}.@var{custom0}.@var{opcode}},
>> +which the disassembler reads to reconstruct instruction names and
>> +operand formats.

> So these sections exist solely for the disassembler's convenience? I.e.
> them getting stripped (or them not being understood) isn't a functional
> issue?

Yes, the metadata is for disassembler/debugger use.  Without it,
execution is unaffected; only recognition of the dynamically defined
instructions is lost.

Regards,
Luis
  

Patch

diff --git a/gas/config/tc-riscv.c b/gas/config/tc-riscv.c
index 7337ad07d8d..278a976b375 100644
--- a/gas/config/tc-riscv.c
+++ b/gas/config/tc-riscv.c
@@ -195,6 +195,10 @@  static bool start_assemble = false;
 
 static bool probing_insn_operands;
 
+/* Global instruction table for APEX extensions.
+   Indexed by instruction format offset (XD, XS, XI, XC) plus sub-opcode.  */
+struct riscv_opcode *arcv_apex_insns[ARCV_APEX_INSN_LIMIT];
+
 /* Set the default_isa_spec.  Return 0 if the spec isn't supported.
    Otherwise, return 1.  */
 
@@ -1770,6 +1774,18 @@  validate_riscv_insn (const struct riscv_opcode *opc, int length)
 		    goto unknown_validate_operand;
 		}
 		break;
+	    case 'a': /* Vendor-specific (ARC-V) operands.  */
+	      switch (*++oparg)
+		{
+		case 'd': USE_BITS (OP_MASK_RD, OP_SH_RD); break;
+		case 's': USE_BITS (OP_MASK_RS1, OP_SH_RS1); break;
+		case 't': USE_BITS (OP_MASK_RS2, OP_SH_RS2); break;
+		case 'k': used_bits |= ENCODE_ARCV_APEX_8BIT_IMM (-1U); break;
+		case 'j': used_bits |= ENCODE_ARCV_APEX_12BIT_IMM (-1U); break;
+		default:
+		  goto unknown_validate_operand;
+		}
+	      break;
 	    default:
 	      goto unknown_validate_operand;
 	    }
@@ -2835,6 +2851,164 @@  static symbolS *deferred_sym_lastP;
 static symbolS *orphan_sym_rootP;
 static symbolS *orphan_sym_lastP;
 
+/* Return true if INSN is the first entry of an APEX XS|XC pair
+   (i.e. the next contiguous entry has the same name and an XC mask).  */
+
+static bool
+arcv_apex_xs_xc_pair_p (const struct riscv_opcode *insn)
+{
+  return (insn->mask == ARCV_APEX_MASK_XS
+	  && insn[1].name != NULL
+	  && strcmp (insn->name, insn[1].name) == 0
+	  && insn[1].mask == ARCV_APEX_MASK_XC);
+}
+
+/* Validate operands of an APEX instruction.
+
+   Verifies that operands match expected argument types for XD, XS, XI,
+   or XC APEX instruction formats.  Ensures operand count matches, register
+   operands are valid GPRs, and immediate operands conform to constraints.
+   For XS|XC pairs, an out-of-range immediate requires rd == rs1 (the XC
+   encoding only has a single register field).  Returns true if valid.  */
+
+static bool
+arcv_apex_validate_insn (const char *operands, const struct riscv_opcode *insn)
+{
+  /* Skip non-APEX instructions.  */
+  if (insn == NULL
+      || (insn->mask != ARCV_APEX_MASK_XD
+	  && insn->mask != ARCV_APEX_MASK_XS
+	  && insn->mask != ARCV_APEX_MASK_XI
+	  && insn->mask != ARCV_APEX_MASK_XC))
+    return true;
+
+  /* Make local copies for tokenization.  */
+  char *operands_copy = xstrdup (operands);
+  char *args_copy = xstrdup (insn->args);
+  bool ok = true;
+
+  char *operand_tokens[3] = { NULL };
+  char *arg_types[3] = { NULL };
+  expressionS operand_exprs[3];
+
+  int operand_count = 0;
+
+  /* Split operands into tokens (at most 3).  */
+  char *token = strtok (operands_copy, ",");
+  while (token && operand_count < 3)
+    {
+      operand_tokens[operand_count++] = token;
+      token = strtok (NULL, ",");
+    }
+  if (token)
+    operand_count++;
+
+  /* Split expected argument types into tokens (at most 3).  */
+  int arg_count = 0;
+  token = strtok (args_copy, ",");
+  while (token && arg_count < 3)
+    {
+      arg_types[arg_count++] = token;
+      token = strtok (NULL, ",");
+    }
+
+  /* Check if operand count matches the expected argument count.  */
+  if (arg_count != operand_count)
+    {
+      as_bad (_("expected %d operands but %d were specified"),
+	      arg_count, operand_count);
+      ok = false;
+    }
+  else
+    {
+      for (int i = 0; i < arg_count && ok; i++)
+	{
+	  const char *arg_type = arg_types[i];
+	  char *operand = operand_tokens[i];
+	  while (ISSPACE (*operand))
+	    operand++;
+	  /* Skip the ARC-V APEX operand prefix "Xa".  */
+	  arg_type += 2;
+
+	  my_getExpression (&operand_exprs[i], operand, false);
+	  /* Check that immediate operands are constants.  */
+	  if ((*arg_type == 'k' || *arg_type == 'j')
+	      && operand_exprs[i].X_op != O_constant)
+	    {
+	      as_bad (_("operands do not conform to the "
+			"specified APEX format"));
+	      ok = false;
+	    }
+	  else if (*arg_type != 'k' && *arg_type != 'j'
+		   && !str_hash_find (reg_names_hash, operand))
+	    {
+	      as_bad (_("operand must be a general-purpose "
+			"register"));
+	      ok = false;
+	    }
+	}
+
+      /* For plain XS instructions (no XC fallback), check the 8-bit
+	 immediate range here so the user gets a specific message
+	 rather than "illegal operands" from the assembly phase.  */
+      if (ok && insn->mask == ARCV_APEX_MASK_XS
+	  && !arcv_apex_xs_xc_pair_p (insn))
+	{
+	  for (int i = 0; i < arg_count && ok; i++)
+	    {
+	      const char *at = arg_types[i];
+	      if (at[0] == 'X' && at[1] == 'a' && at[2] == 'k')
+		{
+		  if (operand_exprs[i].X_op == O_constant
+		      && (operand_exprs[i].X_add_number < -128
+			  || operand_exprs[i].X_add_number > 127))
+		    {
+		      as_bad (_("integer operand out of range; "
+				"should be from -128 to 127, "
+				"inclusive"));
+		      ok = false;
+		    }
+		}
+	    }
+	}
+
+      /* For XS|XC pairs: if the immediate exceeds the XS 8-bit range,
+	 the assembler will fall through to the XC encoding which only
+	 has a single register field (dest == src).  Reject early if
+	 the registers differ.  */
+      if (ok && arcv_apex_xs_xc_pair_p (insn) && arg_count == 3)
+	{
+	  const char *arg2 = arg_types[2];
+	  if (arg2[0] == 'X' && arg2[1] == 'a' && arg2[2] == 'k')
+	    {
+	      if (operand_exprs[2].X_op == O_constant
+		  && (operand_exprs[2].X_add_number < -128
+		      || operand_exprs[2].X_add_number > 127))
+		{
+		  const char *op0 = operand_tokens[0];
+		  const char *op1 = operand_tokens[1];
+		  while (ISSPACE (*op0))
+		    op0++;
+		  while (ISSPACE (*op1))
+		    op1++;
+		  if (strcmp (op0, op1) != 0)
+		    {
+		      as_bad (_("APEX XS|XC instruction needs identical "
+				"destination and source when the "
+				"immediate is outside the range "
+				"-128..127"));
+		      ok = false;
+		    }
+		}
+	    }
+	}
+    }
+
+  xfree (operands_copy);
+  xfree (args_copy);
+  return ok;
+}
+
 /* This routine assembles an instruction into its binary format.  As a
    side effect, it sets the global variable imm_reloc to the type of
    relocation to do if one of the operands is an address expression.  */
@@ -2871,6 +3045,10 @@  riscv_ip (char *str, struct riscv_cl_insn *ip, expressionS *imm_expr,
 
   insn = str_hash_find (hash, str);
 
+  /* Early validation for APEX instructions.  */
+  if (!arcv_apex_validate_insn (asarg, insn))
+    return error;
+
   probing_insn_operands = true;
 
   asargStart = asarg;
@@ -4286,6 +4464,64 @@  riscv_ip (char *str, struct riscv_cl_insn *ip, expressionS *imm_expr,
 		    }
 		  break;
 
+		case 'a': /* Vendor-specific (ARC-V) operands.  */
+		  switch (*++oparg)
+		    {
+		    case 'k': /* ARC-V APEX 8-bit immediate.  */
+		      my_getExpression (imm_expr, asarg, false);
+		      check_absolute_expr (ip, imm_expr, false);
+		      if (imm_expr->X_add_number < -128
+			  || imm_expr->X_add_number > 127)
+			/* Out of 8-bit range.  Break to try next variant
+			   (e.g., XC encoding in XS|XC pairs). Validation
+			   already checked this and provided user feedback.  */
+			break;
+		      ip->insn_opcode
+			  |= ENCODE_ARCV_APEX_8BIT_IMM (imm_expr->X_add_number);
+		      asarg = expr_parse_end;
+		      continue;
+		    case 'j': /* ARC-V APEX 12-bit immediate.  */
+		      my_getExpression (imm_expr, asarg, false);
+		      check_absolute_expr (ip, imm_expr, false);
+		      if (imm_expr->X_add_number < -2048
+			  || imm_expr->X_add_number > 2047)
+			as_bad (_("integer operand out of range; "
+				  "should be from -2048 to 2047, "
+				  "inclusive"));
+		      ip->insn_opcode
+			|= ENCODE_ARCV_APEX_12BIT_IMM
+			     (imm_expr->X_add_number);
+		      asarg = expr_parse_end;
+		      continue;
+		    case 'd': /* ARC-V APEX destination register.  */
+		    case 's': /* ARC-V APEX source register.  */
+		    case 't': /* ARC-V APEX target register.  */
+		      if (reg_lookup (&asarg, RCLASS_GPR, &regno))
+			{
+			  char c = *oparg;
+			  if (*asarg == ' ')
+			    ++asarg;
+
+			  switch (c)
+			    {
+			    case 's':
+			      INSERT_OPERAND (RS1, *ip, regno);
+			      break;
+			    case 'd':
+			      INSERT_OPERAND (RD, *ip, regno);
+			      break;
+			    case 't':
+			      INSERT_OPERAND (RS2, *ip, regno);
+			      break;
+			    }
+			  continue;
+			}
+		      break;
+		    default:
+		      goto unknown_riscv_ip_operand;
+		    }
+		  break;
+
 		default:
 		  goto unknown_riscv_ip_operand;
 		}
@@ -5899,6 +6135,400 @@  s_variant_cc (int ignored ATTRIBUTE_UNUSED)
   elfsym->internal_elf_sym.st_other |= STO_RISCV_VARIANT_CC;
 }
 
+/* Record INSN in the arcv_apex_insns table for the given format OFFSET
+   and function OPCODE.  Returns false if that slot is already taken
+   (duplicate .extInstruction for the same encoding).  */
+
+static bool
+arcv_apex_check_and_set_insn (struct riscv_opcode *insn,
+			      unsigned int opcode,
+			      unsigned int offset)
+{
+  if (arcv_apex_insns[offset + opcode])
+    {
+      as_bad (_(".extInstruction '%s' duplicates opcode used by '%s'"),
+	      insn->name, arcv_apex_insns[offset + opcode]->name);
+      return false;
+    }
+  arcv_apex_insns[offset + opcode] = insn;
+  return true;
+}
+
+/* Create and get a read-only COMDAT section for an APEX instruction.
+
+   Section names encode the instruction format flags (XD=1<<0, XS=1<<1,
+   XI=1<<2, XC=1<<3), followed by the fixed Custom-0 value (11) and the
+   instruction opcode.
+
+   For example, an instruction declared as:
+     .extInstruction foo,7,XS,XC
+   maps to:
+     .riscvapex.<XS|XC>.11.<function_code>
+   and concretely:
+     .riscvapex.10.11.7
+
+   The section is placed in a COMDAT group so that the linker keeps only
+   one copy when multiple compilation units define the same section.
+   The COMDAT group name mirrors the section name but uses a ".apex."
+   prefix instead of ".riscvapex.".  */
+
+static segT
+arcv_apex_get_metadata_section (unsigned int insn_format,
+				unsigned int sub_opcode)
+{
+  /* Allocate buffer for full section name.  */
+  const char *section_name
+    = xasprintf (".riscvapex.%u.%u.%u",
+		 insn_format, ARCV_APEX_CUSTOM0_OPCODE, sub_opcode);
+
+  /* Create a new section with appropriate flags.  */
+  segT apex_section = subseg_new (section_name, 0);
+  bfd_set_section_flags (apex_section,
+			 SEC_READONLY | SEC_HAS_CONTENTS | SEC_LINK_ONCE);
+
+  /* Build the COMDAT group name for the section, prefixed with ".apex.".  */
+  const char *group_name
+    = xasprintf (".apex.%u.%u.%u",
+		 insn_format, ARCV_APEX_CUSTOM0_OPCODE, sub_opcode);
+
+  /* Attach the group name to the section.  */
+  elf_set_group_name (apex_section, group_name);
+
+  return apex_section;
+}
+
+/* Write APEX instruction metadata into the current section.
+
+   Builds an apex_insn record with fixed fields, function code,
+   flags, and the instruction mnemonic.  The record is padded to
+   maintain 16-bit alignment, ensuring correct layout in the
+   section.  This section is later used by the disassembler to
+   decode the instruction.  */
+
+static void
+arcv_apex_write_metadata (const char *insn_name,
+			  unsigned int flags,
+			  unsigned int sub_opcode)
+{
+  struct apex_insn apex;
+  size_t name_len = strlen (insn_name);
+
+  /* NUL terminator, plus one extra zero byte if needed to
+     keep the total record length even.  */
+  size_t nul_pad = (name_len & 1) ? 1 : 2;
+
+  size_t total_size = offsetof (struct apex_insn, name)
+		    + name_len + nul_pad;
+
+  apex.len = total_size;
+  apex.type = ARCV_APEX_METADATA_TYPE;
+  apex.opcode = ARCV_APEX_CUSTOM0_OPCODE;
+  apex.func_t = sub_opcode;
+  apex.flags = flags;
+
+  /* Emit fixed fields.  */
+  char *where = frag_more (offsetof (struct apex_insn, name));
+  memcpy (where, &apex, offsetof (struct apex_insn, name));
+
+  /* Emit instruction name.  */
+  where = frag_more (name_len);
+  memcpy (where, insn_name, name_len);
+
+  /* Emit NUL terminator (+ alignment padding).  */
+  where = frag_more (nul_pad);
+  md_number_to_chars (where, 0x0, nul_pad);
+}
+
+/* Allocate and register an APEX instruction.
+
+   Allocates riscv_opcode structure(s), initializes them according to the
+   instruction format using the appropriate setup function, and inserts
+   them into the opcode hash table for later lookup during assembly.
+
+   For combined XS|XC instructions, two contiguous entries are allocated
+   (XS first, then XC, followed by a NULL sentinel) so that riscv_ip's
+   opcode iteration loop naturally tries the XS encoding first and falls
+   through to the wider XC encoding when the immediate does not fit.  */
+
+static void
+arcv_apex_register_insn (const char *insn_name,
+			 unsigned int flags,
+			 unsigned int sub_opcode)
+{
+  struct riscv_opcode *insn;
+  unsigned int offset = 0;
+  unsigned int fmt = flags & (APEX_FLAG_XD | APEX_FLAG_XS
+			      | APEX_FLAG_XI | APEX_FLAG_XC);
+  unsigned int max_code;
+  void **hash_slot;
+
+  if (str_hash_find (op_hash, insn_name) != NULL)
+    {
+      as_bad (_("instruction `%s' is already defined; "
+		"`.extInstruction' cannot reuse that name"),
+	      insn_name);
+      xfree ((void *) insn_name);
+      return;
+    }
+
+  if (fmt == APEX_FLAG_XD)
+    max_code = 255;
+  else if (fmt == APEX_FLAG_XS)
+    max_code = 63;
+  else if (fmt == (APEX_FLAG_XS | APEX_FLAG_XC)
+	   || fmt == APEX_FLAG_XI || fmt == APEX_FLAG_XC)
+    max_code = 31;
+  else
+    {
+      as_bad (_("`.extInstruction' requires at least one of XD, XS, XI, XC"));
+      xfree ((void *) insn_name);
+      return;
+    }
+
+  if (sub_opcode > max_code)
+    {
+      as_bad (_("APEX instruction function code out of range"));
+      xfree ((void *) insn_name);
+      return;
+    }
+
+  if (fmt == (APEX_FLAG_XS | APEX_FLAG_XC))
+    {
+      /* Allocate a contiguous pair of entries plus a NULL sentinel so
+	 that riscv_ip's insn++ iteration finds both encodings.  The
+	 XS entry is tried first; its match_func rejects same-register
+	 operands so that they always use the XC encoding.  If the 8-bit
+	 immediate does not fit, riscv_ip also falls through to the XC
+	 (12-bit) entry.  */
+      struct riscv_opcode *entries = XCNEWVEC (struct riscv_opcode, 3);
+      struct riscv_opcode *xs_insn = &entries[0];
+      struct riscv_opcode *xc_insn = &entries[1];
+
+      arcv_apex_init_dynamic_insn (xs_insn);
+      xs_insn->name = insn_name;
+      arcv_apex_setup_xs_insn (xs_insn, flags, sub_opcode);
+      xs_insn->match_func = arcv_apex_match_rd_ne_rs1;
+
+      arcv_apex_init_dynamic_insn (xc_insn);
+      xc_insn->name = insn_name;
+      arcv_apex_setup_xc_insn (xc_insn, sub_opcode);
+
+      /* entries[2] is zero-initialized (name == NULL) by XCNEWVEC,
+	 which acts as the sentinel for the riscv_ip iteration.  */
+
+      if (!arcv_apex_check_and_set_insn (xs_insn, sub_opcode,
+					 ARCV_APEX_OFFSET_XS))
+	{
+	  xfree (entries);
+	  xfree ((void *) insn_name);
+	  return;
+	}
+      if (!arcv_apex_check_and_set_insn (xc_insn, sub_opcode,
+					 ARCV_APEX_OFFSET_XC))
+	{
+	  arcv_apex_insns[sub_opcode + ARCV_APEX_OFFSET_XS] = NULL;
+	  xfree (entries);
+	  xfree ((void *) insn_name);
+	  return;
+	}
+
+      insn = xs_insn;
+    }
+  else
+    {
+      /* Allocate a 2-entry array: the instruction plus a zero sentinel
+	 (name == NULL) so that riscv_ip's insn++ loop and helpers like
+	 arcv_apex_xs_xc_pair_p can safely read insn[1].  */
+      insn = XCNEWVEC (struct riscv_opcode, 2);
+      arcv_apex_init_dynamic_insn (insn);
+      insn->name = insn_name;
+
+      switch (fmt)
+	{
+	case APEX_FLAG_XD:
+	  arcv_apex_setup_xd_insn (insn, flags, sub_opcode);
+	  offset = ARCV_APEX_OFFSET_XD;
+	  break;
+	case APEX_FLAG_XS:
+	  arcv_apex_setup_xs_insn (insn, flags, sub_opcode);
+	  offset = ARCV_APEX_OFFSET_XS;
+	  break;
+	case APEX_FLAG_XI:
+	  arcv_apex_setup_xi_insn (insn, flags, sub_opcode);
+	  offset = ARCV_APEX_OFFSET_XI;
+	  break;
+	case APEX_FLAG_XC:
+	  arcv_apex_setup_xc_insn (insn, sub_opcode);
+	  offset = ARCV_APEX_OFFSET_XC;
+	  break;
+	default:
+	  as_bad (_("internal: bad APEX instruction format in registration"));
+	  xfree (insn);
+	  xfree ((void *) insn_name);
+	  return;
+	}
+
+      if (!arcv_apex_check_and_set_insn (insn, sub_opcode, offset))
+	{
+	  xfree (insn);
+	  xfree ((void *) insn_name);
+	  return;
+	}
+    }
+
+  hash_slot = str_hash_insert (op_hash, insn->name, insn, 0);
+  if (hash_slot != NULL)
+    {
+      as_bad (_("internal: cannot register APEX instruction `%s'"),
+	      insn_name);
+      if (fmt == (APEX_FLAG_XS | APEX_FLAG_XC))
+	{
+	  arcv_apex_insns[sub_opcode + ARCV_APEX_OFFSET_XS] = NULL;
+	  arcv_apex_insns[sub_opcode + ARCV_APEX_OFFSET_XC] = NULL;
+	}
+      else
+	arcv_apex_insns[sub_opcode + offset] = NULL;
+      xfree (insn);
+      xfree ((void *) insn_name);
+      return;
+    }
+
+  segT saved_seg = now_seg;
+  subsegT saved_subseg = now_subseg;
+
+  segT apex_section = arcv_apex_get_metadata_section (flags & 0xF, sub_opcode);
+  subseg_set (apex_section, 0);
+
+  arcv_apex_write_metadata (insn_name, flags, sub_opcode);
+
+  subseg_set (saved_seg, saved_subseg);
+}
+
+/* Parse an APEX .extInstruction directive.
+
+   Syntax: .extInstruction <name>, <sub_opcode>, <attributes>
+
+   The <attributes> may include XD, XS, XI, XC, void, no_src0, and no_src1.
+   Validates attribute combinations and registers the instruction once
+   successfully parsed.  */
+
+static void
+arcv_apex_section_parser (int ignore ATTRIBUTE_UNUSED)
+{
+  char *p, c, *insn_name;
+  char *insn_format;
+  unsigned int sub_opcode;
+  /* Skip leading whitespace.  */
+  SKIP_WHITESPACE ();
+
+  p = input_line_pointer;
+  c = get_symbol_name (&p);
+
+  insn_name = xstrdup (p);
+  restore_line_pointer (c);
+
+  /* Convert mnemonic to lowercase for canonical form.  */
+  for (p = insn_name; *p; ++p)
+    *p = TOLOWER (*p);
+
+  /* Expect a comma after mnemonic.  */
+  if (*input_line_pointer != ',')
+    {
+      as_bad (_("expected comma after instruction name"));
+      ignore_rest_of_line ();
+      xfree (insn_name);
+      return;
+    }
+
+  input_line_pointer++;
+  /* Parse sub-opcode value.  */
+  sub_opcode = get_absolute_expression ();
+
+  /* Expect a comma after function code.  */
+  if (*input_line_pointer != ',')
+    {
+      as_bad (_("expected comma after instruction opcode"));
+      ignore_rest_of_line ();
+      xfree (insn_name);
+      return;
+    }
+
+  uint8_t flags = 0;
+  while (*input_line_pointer == ',')
+    {
+      input_line_pointer++;
+
+      p = input_line_pointer;
+      c = get_symbol_name (&p);
+      insn_format = xstrdup (p);
+
+      if (strcmp (insn_format, "XD") == 0)
+	flags |= APEX_FLAG_XD;
+      else if (strcmp (insn_format, "XS") == 0)
+	flags |= APEX_FLAG_XS;
+      else if (strcmp (insn_format, "XI") == 0)
+	flags |= (APEX_FLAG_XI | APEX_FLAG_NO_SRC1);
+      else if (strcmp (insn_format, "XC") == 0)
+	flags |= APEX_FLAG_XC;
+      else if (strcmp (insn_format, "void") == 0)
+	flags |= APEX_FLAG_VOID;
+      else if (strcmp (insn_format, "no_src0") == 0)
+	flags |= APEX_FLAG_NO_SRC0;
+      else if (strcmp (insn_format, "no_src1") == 0)
+	flags |= APEX_FLAG_NO_SRC1;
+      else
+	{
+	  as_bad (_("unrecognized APEX attribute; must be one of "
+		    "XD, XS, XI, XC, void, no_src0, no_src1"));
+	  restore_line_pointer (c);
+	  xfree (insn_format);
+	  xfree (insn_name);
+	  ignore_rest_of_line ();
+	  return;
+	}
+
+      restore_line_pointer (c);
+      xfree (insn_format);
+    }
+
+  if (flags & APEX_FLAG_XD
+      && (flags & (APEX_FLAG_XS | APEX_FLAG_XI | APEX_FLAG_XC)))
+    {
+      as_bad (_("XD and non-XD APEX formats cannot be combined"));
+      xfree (insn_name);
+      ignore_rest_of_line ();
+      return;
+    }
+
+  if ((flags & APEX_FLAG_VOID) && (flags & APEX_FLAG_XC))
+    {
+      as_bad (_("XC and VOID APEX formats cannot be combined"));
+      xfree (insn_name);
+      ignore_rest_of_line ();
+      return;
+    }
+
+  if (flags & APEX_FLAG_NO_SRC0
+      && (flags & (APEX_FLAG_XS | APEX_FLAG_XI | APEX_FLAG_XC)))
+    {
+      as_bad (_("NO_SRC0 and non-XD APEX formats cannot be combined"));
+      xfree (insn_name);
+      ignore_rest_of_line ();
+      return;
+    }
+
+  if (flags & APEX_FLAG_NO_SRC1
+      && (flags & (APEX_FLAG_XS | APEX_FLAG_XC)))
+    {
+      as_bad (_("NO_SRC1 and non-XD/XI APEX formats cannot be combined"));
+      xfree (insn_name);
+      ignore_rest_of_line ();
+      return;
+    }
+
+  arcv_apex_register_insn (insn_name, flags, sub_opcode);
+}
+
 /* RISC-V pseudo-ops table.  */
 static const pseudo_typeS riscv_pseudo_table[] =
 {
@@ -5915,6 +6545,7 @@  static const pseudo_typeS riscv_pseudo_table[] =
   {"variant_cc", s_variant_cc, 0},
   {"float16", float_cons, 'h'},
   {"bfloat16", float_cons, 'b'},
+  {"extinstruction", arcv_apex_section_parser, 0},
 
   { NULL, NULL, 0 },
 };
diff --git a/gas/doc/c-riscv.texi b/gas/doc/c-riscv.texi
index bd296c3de04..39a588282c0 100644
--- a/gas/doc/c-riscv.texi
+++ b/gas/doc/c-riscv.texi
@@ -265,6 +265,59 @@  Floating point constructors for the bfloat16 type, example usage:
 	.bfloat16 0b:ffc1
 @end smallexample
 
+@cindex @code{.extInstruction} directive, RISC-V
+@cindex ARC-V APEX instructions
+@item .extInstruction @var{name}, @var{opcode}, @var{attrs}@dots{}
+Define a dynamic APEX instruction that uses the Custom-0 encoding space.
+The @var{name} is the mnemonic used in subsequent assembly.  The
+@var{opcode} is the function code (sub-opcode) whose valid range depends
+on the format.  The @var{attrs} are a comma-separated list drawn from:
+
+@table @code
+@item XD
+XD format (3 register operands, 8-bit sub-opcode, range 0--255).
+@item XS
+XS format (2 registers + 8-bit signed immediate, 6-bit sub-opcode,
+range 0--63).
+@item XI
+XI format (1 register + 12-bit signed immediate, 5-bit sub-opcode,
+range 0--31).
+@item XC
+XC format (1 register acting as both destination and source +
+12-bit signed immediate, 5-bit sub-opcode, range 0--31).
+@item void
+Omit the destination register from the operand list.
+@item no_src0
+Omit the first source register from the operand list.
+@item no_src1
+Omit the second source register (or immediate) from the operand list.
+@end table
+
+Exactly one of @code{XD}, @code{XS}, @code{XI}, or @code{XC} must be
+specified.  @code{XS} and @code{XC} may be combined; in that case the
+assembler uses the XS encoding when the destination and source registers
+differ (and the immediate fits in 8 bits), and the XC encoding when the
+registers are the same or the immediate requires 12 bits.
+
+Examples:
+
+@smallexample
+	.extInstruction myalu, 1, XD
+	myalu a0, a1, a2
+
+	.extInstruction myimm, 5, XS
+	myimm a0, a1, 42
+
+	.extInstruction myflex, 3, XS, XC
+	myflex a0, a1, 10
+	myflex a0, a0, 1000
+@end smallexample
+
+The assembler emits metadata about each @code{.extInstruction} into
+ELF sections named @code{.riscvapex.@var{fmt}.@var{custom0}.@var{opcode}},
+which the disassembler reads to reconstruct instruction names and
+operand formats.
+
 @end table
 
 @node RISC-V-Modifiers
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-01.d b/gas/testsuite/gas/riscv/x-arcv-apex-01.d
new file mode 100644
index 00000000000..e3faa8bd632
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-01.d
@@ -0,0 +1,18 @@ 
+#source: x-arcv-apex-01.s
+#objdump: -d
+
+.*:[ 	]+file format .*
+
+
+Disassembly of section .text:
+
+0+[0-9a-f]+ <.text>:
+[ 	]+[0-9a-f]+:[ 	]+00c5c50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+02c5800b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0205c00b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0400000b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0b15b50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0b15d00b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0163a50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0164200b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0214e50b[ 	]+.*
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-01.s b/gas/testsuite/gas/riscv/x-arcv-apex-01.s
new file mode 100644
index 00000000000..2090bf28a27
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-01.s
@@ -0,0 +1,19 @@ 
+	.extInstruction foo0,1,XD
+	.extInstruction foo1,2,XD,void
+	.extInstruction foo2,3,XD,void,no_src1
+	.extInstruction foo3,4,XD,void,no_src0,no_src1
+	.extInstruction foo4,5,XS
+	.extInstruction foo5,6,XS,void
+	.extInstruction foo6,7,XI
+	.extInstruction foo7,8,XI,void
+	.extInstruction foo8,9,XC
+
+	foo0 a0,a1,a2
+	foo1 a1,a2
+	foo2 a1
+	foo3
+	foo4 a0,a1,11
+	foo5 a1,11
+	foo6 a0,22
+	foo7 22
+	foo8 a0,a0,33
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-02.d b/gas/testsuite/gas/riscv/x-arcv-apex-02.d
new file mode 100644
index 00000000000..6bb08202e2a
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-02.d
@@ -0,0 +1,13 @@ 
+#source: x-arcv-apex-02.s
+#objdump: -d
+
+.*:[ 	]+file format .*
+
+
+Disassembly of section .text:
+
+0+[0-9a-f]+ <.text>:
+[ 	]+[0-9a-f]+:[ 	]+0020e50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0205b50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+1000e50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0ff0e50b[ 	]+.*
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-02.s b/gas/testsuite/gas/riscv/x-arcv-apex-02.s
new file mode 100644
index 00000000000..2fd0da33575
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-02.s
@@ -0,0 +1,6 @@ 
+	.extInstruction foo,1,XS,XC
+
+	foo a0,a0,2
+	foo a0,a1,2
+	foo a0,a0,256
+	foo a0,a0,255
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-03.d b/gas/testsuite/gas/riscv/x-arcv-apex-03.d
new file mode 100644
index 00000000000..fc3bc432de4
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-03.d
@@ -0,0 +1,46 @@ 
+#source: x-arcv-apex-01.s
+#objdump: -s
+
+.*:[ 	]+file format .*
+
+Contents of section .group:
+ 0000 01000000 0d000000.*
+Contents of section .group:
+ 0000 01000000 0e000000.*
+Contents of section .group:
+ 0000 01000000 0f000000.*
+Contents of section .group:
+ 0000 01000000 10000000.*
+Contents of section .group:
+ 0000 01000000 11000000.*
+Contents of section .group:
+ 0000 01000000 12000000.*
+Contents of section .group:
+ 0000 01000000 13000000.*
+Contents of section .group:
+ 0000 01000000 14000000.*
+Contents of section .group:
+ 0000 01000000 15000000.*
+Contents of section \.text:
+ 0000 0bc5c500 0b80c502 0bc00502 0b000004.*
+ 0010 0bb5150b 0bd0150b 0ba56301 0b206401.*
+ 0020 0be51402.*
+Contents of section \.riscvapex\.1\.11\.1:
+ 0000 0c010b01 0100666f 6f300000.*
+Contents of section \.riscvapex\.1\.11\.2:
+ 0000 0c010b02 1100666f 6f310000.*
+Contents of section \.riscvapex\.1\.11\.3:
+ 0000 0c010b03 5100666f 6f320000.*
+Contents of section \.riscvapex\.1\.11\.4:
+ 0000 0c010b04 7100666f 6f330000.*
+Contents of section \.riscvapex\.2\.11\.5:
+ 0000 0c010b05 0200666f 6f340000.*
+Contents of section \.riscvapex\.2\.11\.6:
+ 0000 0c010b06 1200666f 6f350000.*
+Contents of section \.riscvapex\.4\.11\.7:
+ 0000 0c010b07 4400666f 6f360000.*
+Contents of section \.riscvapex\.4\.11\.8:
+ 0000 0c010b08 5400666f 6f370000.*
+Contents of section \.riscvapex\.8\.11\.9:
+ 0000 0c010b09 0800666f 6f380000.*
+#pass
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-04.d b/gas/testsuite/gas/riscv/x-arcv-apex-04.d
new file mode 100644
index 00000000000..f62d160a30b
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-04.d
@@ -0,0 +1,15 @@ 
+#source: x-arcv-apex-04.s
+#objdump: -d
+
+.*:[ 	]+file format .*
+
+
+Disassembly of section .text:
+
+0+[0-9a-f]+ <.text>:
+[ 	]+[0-9a-f]+:[ 	]+8005b50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+7f05b50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+8000a50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+7ff0a50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+8000e50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+7ff0e50b[ 	]+.*
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-04.s b/gas/testsuite/gas/riscv/x-arcv-apex-04.s
new file mode 100644
index 00000000000..c3cd0e2fc46
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-04.s
@@ -0,0 +1,10 @@ 
+	.extInstruction foo0,1,XS
+	.extInstruction foo1,1,XI
+	.extInstruction foo2,1,XC
+
+	foo0 a0,a1,-128
+	foo0 a0,a1,127
+	foo1 a0,-2048
+	foo1 a0,2047
+	foo2 a0,a0,-2048
+	foo2 a0,a0,2047
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-01.d b/gas/testsuite/gas/riscv/x-arcv-apex-fail-01.d
new file mode 100644
index 00000000000..fe85c681d0f
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-01.d
@@ -0,0 +1 @@ 
+#error_output: x-arcv-apex-fail-01.l
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-01.l b/gas/testsuite/gas/riscv/x-arcv-apex-fail-01.l
new file mode 100644
index 00000000000..43a5a6adaa7
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-01.l
@@ -0,0 +1,2 @@ 
+.*: Assembler messages:
+.*: Error: unrecognized APEX attribute; must be one of XD, XS, XI, XC, void, no_src0, no_src1
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-01.s b/gas/testsuite/gas/riscv/x-arcv-apex-fail-01.s
new file mode 100644
index 00000000000..d0a9095cd1c
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-01.s
@@ -0,0 +1 @@ 
+	.extInstruction foo,1,XY
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-02.d b/gas/testsuite/gas/riscv/x-arcv-apex-fail-02.d
new file mode 100644
index 00000000000..9366a34b7a0
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-02.d
@@ -0,0 +1 @@ 
+#error_output: x-arcv-apex-fail-02.l
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-02.l b/gas/testsuite/gas/riscv/x-arcv-apex-fail-02.l
new file mode 100644
index 00000000000..fc7d3cae283
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-02.l
@@ -0,0 +1,5 @@ 
+.*: Assembler messages:
+.*: Error: NO_SRC0 and non-XD APEX formats cannot be combined
+.*: Error: NO_SRC0 and non-XD APEX formats cannot be combined
+.*: Error: XC and VOID APEX formats cannot be combined
+
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-02.s b/gas/testsuite/gas/riscv/x-arcv-apex-fail-02.s
new file mode 100644
index 00000000000..48d1e2da885
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-02.s
@@ -0,0 +1,3 @@ 
+	.extInstruction foo0,1,XS,no_src0,no_src1
+	.extInstruction foo1,1,XI,no_src0,no_src1
+	.extInstruction foo2,1,XC,void,no_src0,no_src1
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-03.d b/gas/testsuite/gas/riscv/x-arcv-apex-fail-03.d
new file mode 100644
index 00000000000..f72280a88b3
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-03.d
@@ -0,0 +1 @@ 
+#error_output: x-arcv-apex-fail-03.l
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-03.l b/gas/testsuite/gas/riscv/x-arcv-apex-fail-03.l
new file mode 100644
index 00000000000..7573ed9ea66
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-03.l
@@ -0,0 +1,9 @@ 
+.*: Assembler messages:
+.*: Error: integer operand out of range; should be from -128 to 127, inclusive
+.*: Error: unrecognized opcode `foo0'
+.*: Error: integer operand out of range; should be from -128 to 127, inclusive
+.*: Error: unrecognized opcode `foo0'
+.*: Error: integer operand out of range; should be from -2048 to 2047, inclusive
+.*: Error: integer operand out of range; should be from -2048 to 2047, inclusive
+.*: Error: integer operand out of range; should be from -2048 to 2047, inclusive
+.*: Error: integer operand out of range; should be from -2048 to 2047, inclusive
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-03.s b/gas/testsuite/gas/riscv/x-arcv-apex-fail-03.s
new file mode 100644
index 00000000000..b718ac08816
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-03.s
@@ -0,0 +1,12 @@ 
+	.extInstruction foo0,1,XS
+	.extInstruction foo1,2,XI
+	.extInstruction foo2,3,XC
+
+	foo0 a0,a1,-129
+	foo0 a0,a1,128
+
+	foo1 a0,-2049
+	foo1 a0,2048
+
+	foo2 a0,a0,-2049
+	foo2 a0,a0,2048
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-04.d b/gas/testsuite/gas/riscv/x-arcv-apex-fail-04.d
new file mode 100644
index 00000000000..942e11e5503
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-04.d
@@ -0,0 +1 @@ 
+#error_output: x-arcv-apex-fail-04.l
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-04.l b/gas/testsuite/gas/riscv/x-arcv-apex-fail-04.l
new file mode 100644
index 00000000000..a46a42d3148
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-04.l
@@ -0,0 +1,3 @@ 
+.*: Assembler messages:
+.*: Error: expected comma after instruction name
+.*: Error: expected comma after instruction opcode
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-04.s b/gas/testsuite/gas/riscv/x-arcv-apex-fail-04.s
new file mode 100644
index 00000000000..72a03e09205
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-04.s
@@ -0,0 +1,2 @@ 
+	.extInstruction foo
+	.extInstruction bar,1
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-05.d b/gas/testsuite/gas/riscv/x-arcv-apex-fail-05.d
new file mode 100644
index 00000000000..76244da553c
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-05.d
@@ -0,0 +1 @@ 
+#error_output: x-arcv-apex-fail-05.l
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-05.l b/gas/testsuite/gas/riscv/x-arcv-apex-fail-05.l
new file mode 100644
index 00000000000..83c052e4099
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-05.l
@@ -0,0 +1,3 @@ 
+.*: Assembler messages:
+.*: Error: expected 3 operands but 2 were specified
+#...
\ No newline at end of file
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-05.s b/gas/testsuite/gas/riscv/x-arcv-apex-fail-05.s
new file mode 100644
index 00000000000..ded33d90c9e
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-05.s
@@ -0,0 +1,3 @@ 
+	.extInstruction foo,1,XD
+
+	foo a0,a1
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-06.d b/gas/testsuite/gas/riscv/x-arcv-apex-fail-06.d
new file mode 100644
index 00000000000..7483f348395
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-06.d
@@ -0,0 +1 @@ 
+#error_output: x-arcv-apex-fail-06.l
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-06.l b/gas/testsuite/gas/riscv/x-arcv-apex-fail-06.l
new file mode 100644
index 00000000000..477fb3e0dc1
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-06.l
@@ -0,0 +1,7 @@ 
+.*: Assembler messages:
+.*: Error: operands do not conform to the specified APEX format
+#...
+.*: Error: operands do not conform to the specified APEX format
+#...
+.*: Error: operands do not conform to the specified APEX format
+#...
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-06.s b/gas/testsuite/gas/riscv/x-arcv-apex-fail-06.s
new file mode 100644
index 00000000000..27991596a58
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-06.s
@@ -0,0 +1,7 @@ 
+	.extInstruction foo,1,XS
+	.extInstruction bar,1,XI
+	.extInstruction baz,1,XC
+
+	foo a0,a1,a2
+	bar a3,a4
+	baz a5,a6,a7
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-07.d b/gas/testsuite/gas/riscv/x-arcv-apex-fail-07.d
new file mode 100644
index 00000000000..149f7877e2c
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-07.d
@@ -0,0 +1 @@ 
+#error_output: x-arcv-apex-fail-07.l
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-07.l b/gas/testsuite/gas/riscv/x-arcv-apex-fail-07.l
new file mode 100644
index 00000000000..f49a39b4b06
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-07.l
@@ -0,0 +1,9 @@ 
+.*: Assembler messages:
+.*: Error: operand must be a general-purpose register
+#...
+.*: Error: operand must be a general-purpose register
+#...
+.*: Error: operand must be a general-purpose register
+#...
+.*: Error: operand must be a general-purpose register
+#...
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-07.s b/gas/testsuite/gas/riscv/x-arcv-apex-fail-07.s
new file mode 100644
index 00000000000..e18a09f643a
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-07.s
@@ -0,0 +1,9 @@ 
+	.extInstruction foo0,1,XD
+	.extInstruction foo1,1,XS
+	.extInstruction foo2,1,XI
+	.extInstruction foo3,1,XC
+
+	foo0 a0,a1,r2
+	foo1 a0,r1,1
+	foo2 r0,1
+	foo3 a0,r0,1
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-08.d b/gas/testsuite/gas/riscv/x-arcv-apex-fail-08.d
new file mode 100644
index 00000000000..4a1f774f178
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-08.d
@@ -0,0 +1 @@ 
+#error_output: x-arcv-apex-fail-08.l
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-08.l b/gas/testsuite/gas/riscv/x-arcv-apex-fail-08.l
new file mode 100644
index 00000000000..53454166897
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-08.l
@@ -0,0 +1,5 @@ 
+.*: Assembler messages:
+.*: Error: .extInstruction 'bar1' duplicates opcode used by 'foo1'
+.*: Error: .extInstruction 'baz1' duplicates opcode used by 'foo1'
+.*: Error: .extInstruction 'bar2' duplicates opcode used by 'foo2'
+.*: Error: .extInstruction 'baz2' duplicates opcode used by 'foo2'
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-08.s b/gas/testsuite/gas/riscv/x-arcv-apex-fail-08.s
new file mode 100644
index 00000000000..3c75537861a
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-08.s
@@ -0,0 +1,7 @@ 
+	.extInstruction foo1,1,XS,XC
+	.extInstruction bar1,1,XC
+	.extInstruction baz1,1,XS
+
+	.extInstruction foo2,2,XC,XS
+	.extInstruction bar2,2,XC
+	.extInstruction baz2,2,XS
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-09.d b/gas/testsuite/gas/riscv/x-arcv-apex-fail-09.d
new file mode 100644
index 00000000000..52528816a73
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-09.d
@@ -0,0 +1 @@ 
+#error_output: x-arcv-apex-fail-09.l
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-09.l b/gas/testsuite/gas/riscv/x-arcv-apex-fail-09.l
new file mode 100644
index 00000000000..66fa6c1afcf
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-09.l
@@ -0,0 +1,3 @@ 
+.*: Assembler messages:
+.*: Error: instruction `add' is already defined; `\.extInstruction' cannot reuse that name
+#...
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-09.s b/gas/testsuite/gas/riscv/x-arcv-apex-fail-09.s
new file mode 100644
index 00000000000..b9a2c27b142
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-09.s
@@ -0,0 +1 @@ 
+	.extInstruction add,1,XS
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-10.d b/gas/testsuite/gas/riscv/x-arcv-apex-fail-10.d
new file mode 100644
index 00000000000..b11459a30db
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-10.d
@@ -0,0 +1 @@ 
+#error_output: x-arcv-apex-fail-10.l
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-10.l b/gas/testsuite/gas/riscv/x-arcv-apex-fail-10.l
new file mode 100644
index 00000000000..4fba5d3a59f
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-10.l
@@ -0,0 +1,3 @@ 
+.*: Assembler messages:
+.*: Error: instruction `fooapex' is already defined; `\.extInstruction' cannot reuse that name
+#...
diff --git a/gas/testsuite/gas/riscv/x-arcv-apex-fail-10.s b/gas/testsuite/gas/riscv/x-arcv-apex-fail-10.s
new file mode 100644
index 00000000000..4e067ce5677
--- /dev/null
+++ b/gas/testsuite/gas/riscv/x-arcv-apex-fail-10.s
@@ -0,0 +1,2 @@ 
+	.extInstruction fooapex,1,XS
+	.extInstruction fooapex,2,XS
diff --git a/include/opcode/riscv.h b/include/opcode/riscv.h
index de105f5df8b..fdb23c0958d 100644
--- a/include/opcode/riscv.h
+++ b/include/opcode/riscv.h
@@ -732,4 +732,72 @@  extern const struct riscv_opcode riscv_insn_types[];
 
 extern unsigned int riscv_get_sp_base (insn_t, unsigned int);
 
+extern int arcv_apex_match_rd_ne_rs1 (const struct riscv_opcode *, insn_t);
+extern void arcv_apex_init_dynamic_insn (struct riscv_opcode *);
+extern void arcv_apex_setup_xd_insn (struct riscv_opcode *, unsigned int,
+				     unsigned int);
+extern void arcv_apex_setup_xs_insn (struct riscv_opcode *, unsigned int,
+				     unsigned int);
+extern void arcv_apex_setup_xi_insn (struct riscv_opcode *, unsigned int,
+				     unsigned int);
+extern void arcv_apex_setup_xc_insn (struct riscv_opcode *, unsigned int);
+
+/* ARC-V APEX instruction format flags.  Used by .extInstruction directive
+   and metadata serialization/deserialization.  */
+enum apex_flags {
+  APEX_FLAG_NONE    = 0,
+  APEX_FLAG_XD      = 1 << 0,
+  APEX_FLAG_XS      = 1 << 1,
+  APEX_FLAG_XI      = 1 << 2,
+  APEX_FLAG_XC      = 1 << 3,
+  APEX_FLAG_VOID    = 1 << 4,
+  APEX_FLAG_NO_SRC0 = 1 << 5,
+  APEX_FLAG_NO_SRC1 = 1 << 6,
+};
+
+/* ARC-V APEX instruction metadata record.  Serialized in ELF .riscvapex.*
+   sections and read by the disassembler.  */
+struct apex_insn
+{
+  uint8_t len;
+  uint8_t type;
+  uint8_t opcode;
+  uint8_t func_t;
+  uint16_t flags;
+  char name[1];
+};
+
+/* The XD-type has 8 function bits encoding up to 256 instructions.
+   The XS-type has 6 function bits encoding up to 64 instructions.
+   Both the XI-type and the XC-type have 5 function bits each encoding up
+   to 32 instructions respectively.  Thus giving a total of 384 possible
+   different instructions.  */
+#define ARCV_APEX_INSN_LIMIT 384
+#define ARCV_APEX_OFFSET_XD  0				 /* 256 entries.  */
+#define ARCV_APEX_OFFSET_XS  (ARCV_APEX_OFFSET_XD + 256) /* 0  + 256 = 256  */
+#define ARCV_APEX_OFFSET_XI  (ARCV_APEX_OFFSET_XS + 64)  /* 256 + 64 = 320  */
+#define ARCV_APEX_OFFSET_XC  (ARCV_APEX_OFFSET_XI + 32)  /* 320 + 32 = 352  */
+
+/* Fixed encoding bits that distinguish XS, XI, and XC formats.  */
+#define ARCV_APEX_XS_FIXED_BITS   0x1000
+#define ARCV_APEX_XI_FIXED_BITS   0x2000
+#define ARCV_APEX_XC_FIXED_BITS   0x6000
+
+#define ARCV_APEX_MASK_XD  0xFE00407F
+#define ARCV_APEX_MASK_XS  0xF0707F
+#define ARCV_APEX_MASK_XI  0xFA07F
+#define ARCV_APEX_MASK_XC  0xFE07F
+
+/* RISC-V Custom-0 major opcode used by ARC-V APEX instructions.  */
+#define ARCV_APEX_CUSTOM0_OPCODE  0xb
+
+/* Metadata record type for APEX instructions.  */
+#define ARCV_APEX_METADATA_TYPE   1
+
+#define ENCODE_ARCV_APEX_8BIT_IMM(x) \
+  (RV_X(x, 0, 8) << 24)
+
+#define ENCODE_ARCV_APEX_12BIT_IMM(x) \
+  (RV_X(x, 0, 12) << 20)
+
 #endif /* _RISCV_H_ */
diff --git a/ld/testsuite/ld-riscv-elf/ld-riscv-elf.exp b/ld/testsuite/ld-riscv-elf/ld-riscv-elf.exp
index 8e26ccff10a..5a145628bdf 100644
--- a/ld/testsuite/ld-riscv-elf/ld-riscv-elf.exp
+++ b/ld/testsuite/ld-riscv-elf/ld-riscv-elf.exp
@@ -37,6 +37,14 @@  proc riscv_choose_lp64_emul {} {
     return "elf64lriscv"
 }
 
+proc riscv_choose_64_or_32_emul {} {
+    if { [istarget "riscv64*-*"] } {
+	return [riscv_choose_lp64_emul]
+    } else {
+	return [riscv_choose_ilp32_emul]
+    }
+}
+
 # target: rv32 or rv64.
 # output: Which output you want?  (exe, pie, .so)
 proc run_dump_test_ifunc { name target output} {
@@ -174,6 +182,8 @@  if [istarget "riscv*-*-*"] {
     run_dump_test "uleb128"
     run_dump_test "pr31179"
     run_dump_test "pr31179-r"
+    run_dump_test "x-arcv-apex-01"
+    run_dump_test "x-arcv-apex-02"
     run_ld_link_tests [list \
 	[list "Weak reference 32" "-T weakref.ld -m[riscv_choose_ilp32_emul]" "" \
 	    "-march=rv32i -mabi=ilp32" {weakref32.s} \
diff --git a/ld/testsuite/ld-riscv-elf/x-arcv-apex-01-a.s b/ld/testsuite/ld-riscv-elf/x-arcv-apex-01-a.s
new file mode 100644
index 00000000000..0d2a4f72f9d
--- /dev/null
+++ b/ld/testsuite/ld-riscv-elf/x-arcv-apex-01-a.s
@@ -0,0 +1,8 @@ 
+	.extern func_1
+	.extern func_2
+	.globl _start
+
+	_start:
+	  call func_1
+	  call func_2
+	  ret
diff --git a/ld/testsuite/ld-riscv-elf/x-arcv-apex-01-b.s b/ld/testsuite/ld-riscv-elf/x-arcv-apex-01-b.s
new file mode 100644
index 00000000000..b5073a941fa
--- /dev/null
+++ b/ld/testsuite/ld-riscv-elf/x-arcv-apex-01-b.s
@@ -0,0 +1,21 @@ 
+	.extInstruction foo0,1,XD
+	.extInstruction foo1,2,XD,void
+	.extInstruction foo2,3,XD,void,no_src1
+	.extInstruction foo3,4,XD,void,no_src0,no_src1
+	.extInstruction foo4,5,XS
+	.extInstruction foo5,6,XS,void
+	.extInstruction foo6,7,XI
+	.extInstruction foo7,8,XI,void
+	.extInstruction foo8,9,XC
+
+	.globl func_1
+	func_1:
+	  foo0	a0,a1,a2
+	  foo1	a1,a2
+	  foo2	a1
+	  foo3
+	  foo4	a0,a1,11
+	  foo5	a1,11
+	  foo6	a0,22
+	  foo7	22
+	  foo8	a0,a0,33
diff --git a/ld/testsuite/ld-riscv-elf/x-arcv-apex-01-c.s b/ld/testsuite/ld-riscv-elf/x-arcv-apex-01-c.s
new file mode 100644
index 00000000000..58fce1a4607
--- /dev/null
+++ b/ld/testsuite/ld-riscv-elf/x-arcv-apex-01-c.s
@@ -0,0 +1,21 @@ 
+	.extInstruction foo0,1,XD
+	.extInstruction foo1,2,XD,void
+	.extInstruction foo2,3,XD,void,no_src1
+	.extInstruction foo3,4,XD,void,no_src0,no_src1
+	.extInstruction foo4,5,XS
+	.extInstruction foo5,6,XS,void
+	.extInstruction foo6,7,XI
+	.extInstruction foo7,8,XI,void
+	.extInstruction foo8,9,XC
+
+	.globl func_2
+	func_2:
+	  foo0	a0,a1,a2
+	  foo1	a1,a2
+	  foo2	a1
+	  foo3
+	  foo4	a0,a1,11
+	  foo5	a1,11
+	  foo6	a0,22
+	  foo7	22
+	  foo8	a0,a0,33
diff --git a/ld/testsuite/ld-riscv-elf/x-arcv-apex-01.d b/ld/testsuite/ld-riscv-elf/x-arcv-apex-01.d
new file mode 100644
index 00000000000..e5b407e0889
--- /dev/null
+++ b/ld/testsuite/ld-riscv-elf/x-arcv-apex-01.d
@@ -0,0 +1,32 @@ 
+#source: x-arcv-apex-01-a.s
+#source: x-arcv-apex-01-b.s
+#source: x-arcv-apex-01-c.s
+#ld: -m[riscv_choose_64_or_32_emul]
+#objdump: -s
+
+.*:[ 	]+file format .*
+
+#...
+Contents of section \.text:
+#...
+Contents of section \.riscv\.attributes:
+#...
+Contents of section .riscvapex.1.11.1:
+ 0000 0c010b01 0100666f 6f300000.*
+Contents of section .riscvapex.1.11.2:
+ 0000 0c010b02 1100666f 6f310000.*
+Contents of section .riscvapex.1.11.3:
+ 0000 0c010b03 5100666f 6f320000.*
+Contents of section .riscvapex.1.11.4:
+ 0000 0c010b04 7100666f 6f330000.*
+Contents of section .riscvapex.2.11.5:
+ 0000 0c010b05 0200666f 6f340000.*
+Contents of section .riscvapex.2.11.6:
+ 0000 0c010b06 1200666f 6f350000.*
+Contents of section .riscvapex.4.11.7:
+ 0000 0c010b07 4400666f 6f360000.*
+Contents of section .riscvapex.4.11.8:
+ 0000 0c010b08 5400666f 6f370000.*
+Contents of section .riscvapex.8.11.9:
+ 0000 0c010b09 0800666f 6f380000.*
+#pass
diff --git a/ld/testsuite/ld-riscv-elf/x-arcv-apex-02.d b/ld/testsuite/ld-riscv-elf/x-arcv-apex-02.d
new file mode 100644
index 00000000000..abebb3566e6
--- /dev/null
+++ b/ld/testsuite/ld-riscv-elf/x-arcv-apex-02.d
@@ -0,0 +1,36 @@ 
+#source: x-arcv-apex-01-a.s
+#source: x-arcv-apex-01-b.s
+#source: x-arcv-apex-01-c.s
+#ld: -m[riscv_choose_64_or_32_emul]
+#objdump: -D -j .text
+
+.*:[ 	]+file format .*
+
+
+Disassembly of section .text:
+
+0+[0-9a-f]+ <_start>:
+#...
+
+0+[0-9a-f]+ <func_1>:
+[ 	]+[0-9a-f]+:[ 	]+00c5c50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+02c5800b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0205c00b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0400000b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0b15b50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0b15d00b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0163a50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0164200b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0214e50b[ 	]+.*
+
+0+[0-9a-f]+ <func_2>:
+[ 	]+[0-9a-f]+:[ 	]+00c5c50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+02c5800b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0205c00b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0400000b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0b15b50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0b15d00b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0163a50b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0164200b[ 	]+.*
+[ 	]+[0-9a-f]+:[ 	]+0214e50b[ 	]+.*
+#pass
diff --git a/opcodes/riscv-opc.c b/opcodes/riscv-opc.c
index 54887c97880..a45b8aa5fc6 100644
--- a/opcodes/riscv-opc.c
+++ b/opcodes/riscv-opc.c
@@ -448,6 +448,146 @@  match_rd_x1x5_opcode (const struct riscv_opcode *op,
   return match_opcode (op, insn) && (rd == 1 || rd == 5);
 }
 
+/* Invariant fields for dynamically allocated APEX opcodes (GAS / metadata).
+   Per-insn name, match, mask, and args are filled by arcv_apex_setup_* or
+   the XS|XC conversion path.  */
+
+static const struct riscv_opcode arcv_apex_dynamic_insn_proto =
+{
+  NULL,			/* name.  */
+  0,			/* xlen_requirement.  */
+  INSN_CLASS_I,		/* insn_class.  */
+  NULL,			/* args.  */
+  0,			/* match.  */
+  0,			/* mask.  */
+  match_opcode,		/* match_func.  */
+  0			/* pinfo.  */
+};
+
+void
+arcv_apex_init_dynamic_insn (struct riscv_opcode *insn)
+{
+  memcpy (insn, &arcv_apex_dynamic_insn_proto, sizeof (*insn));
+}
+
+/* Match only when rd != rs1.  Used by the XS entry in an XS|XC pair so
+   that same-register operands always fall through to the XC encoding.  */
+
+int
+arcv_apex_match_rd_ne_rs1 (const struct riscv_opcode *op, insn_t insn)
+{
+  int rd  = (insn & MASK_RD) >> OP_SH_RD;
+  int rs1 = (insn & MASK_RS1) >> OP_SH_RS1;
+  return match_opcode (op, insn) && rd != rs1;
+}
+
+/* Initialize an APEX instruction in XD format.
+   Sets mask, match, and operand argument string based on flags.
+   The flags APEX_FLAG_VOID, APEX_FLAG_NO_SRC0, and APEX_FLAG_NO_SRC1
+   control which operands are present in the instruction definition.  */
+
+void
+arcv_apex_setup_xd_insn (struct riscv_opcode *insn,
+		       unsigned int flags,
+		       unsigned int sub_opcode)
+{
+  insn->mask = ARCV_APEX_MASK_XD;
+  insn->match = ((uint32_t)(sub_opcode & 0xFE) << 24)
+		| ((uint32_t)(sub_opcode & 0x1) << 14)
+		| ARCV_APEX_CUSTOM0_OPCODE;
+
+  /* Select operand pattern based on flags.
+     Operands use vendor prefix Xa (ARC-V APEX):
+     d = dest, s = src1, t = src2.  */
+  switch (flags & (APEX_FLAG_VOID | APEX_FLAG_NO_SRC0 | APEX_FLAG_NO_SRC1))
+    {
+    case APEX_FLAG_NO_SRC1:
+      insn->args = "Xad,Xas";
+      break;
+    case APEX_FLAG_NO_SRC0 | APEX_FLAG_NO_SRC1:
+      insn->args = "Xad";
+      break;
+    case APEX_FLAG_VOID | APEX_FLAG_NO_SRC0 | APEX_FLAG_NO_SRC1:
+      insn->args = "";
+      break;
+    case APEX_FLAG_VOID | APEX_FLAG_NO_SRC1:
+      insn->args = "Xas";
+      break;
+    case APEX_FLAG_VOID:
+      insn->args = "Xas,Xat";
+      break;
+    case 0:
+      insn->args = "Xad,Xas,Xat";
+      break;
+    default:
+      insn->args = "Xad,Xas,Xat";
+      break;
+    }
+}
+
+/* Initialize an APEX instruction in XS format.
+   Sets mask, match, and operand argument string based on flags.
+   The APEX_FLAG_VOID flag controls whether the destination operand
+   is included.  */
+
+void
+arcv_apex_setup_xs_insn (struct riscv_opcode *insn,
+		       unsigned int flags,
+		       unsigned int sub_opcode)
+{
+  insn->mask = ARCV_APEX_MASK_XS;
+  insn->match = ((uint32_t)(sub_opcode & 0x3C) << 18)
+		| ((uint32_t)(sub_opcode & 0x3) << 13)
+		| ARCV_APEX_CUSTOM0_OPCODE
+		| ARCV_APEX_XS_FIXED_BITS;
+
+  /* Select operand pattern based on APEX_FLAG_VOID.
+     Operands: d = dest, s = src1, k = 8-bit immediate.  */
+  if (flags & APEX_FLAG_VOID)
+    insn->args = "Xas,Xak";
+  else
+    insn->args = "Xad,Xas,Xak";
+}
+
+/* Initialize an APEX instruction in XI format.
+   Sets mask, match, and operand argument string based on flags.
+   The APEX_FLAG_VOID flag controls whether the destination operand
+   is included.  */
+
+void
+arcv_apex_setup_xi_insn (struct riscv_opcode *insn,
+		       unsigned int flags,
+		       unsigned int sub_opcode)
+{
+  insn->mask = ARCV_APEX_MASK_XI;
+  insn->match = ((uint32_t)(sub_opcode & 0x1F) << 15)
+		| ARCV_APEX_CUSTOM0_OPCODE
+		| ARCV_APEX_XI_FIXED_BITS;
+
+  if (flags & APEX_FLAG_VOID)
+    insn->args = "Xaj";
+  else
+    insn->args = "Xad,Xaj";
+}
+
+/* Initialize an APEX instruction in XC format.
+   Sets mask and match.  Set fixed operand argument string.
+   The XC form represents instructions where dest == src.  */
+
+void
+arcv_apex_setup_xc_insn (struct riscv_opcode *insn,
+		       unsigned int sub_opcode)
+{
+  insn->mask = ARCV_APEX_MASK_XC;
+  insn->match = ((uint32_t)(sub_opcode & 0x1F) << 15)
+		| ARCV_APEX_CUSTOM0_OPCODE
+		| ARCV_APEX_XC_FIXED_BITS;
+
+  /* Fixed operand pattern for XC instructions:
+     dest, dest, 12-bit immediate (same encoding field as XI).  */
+  insn->args = "Xad,Xad,Xaj";
+}
+
 const struct riscv_opcode riscv_opcodes[] =
 {
 /* name, xlen, isa, operands, match, mask, match_func, pinfo.  */