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.  */
