patch for supporting decoding 8087 fpu-emulation

Message ID 9f2fcfed-30ff-45f9-a983-c9a329432cea@contre.com
State New
Headers
Series patch for supporting decoding 8087 fpu-emulation |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_binutils_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_binutils_build--master-aarch64 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

Wolfgang Sourdeau Sept. 25, 2026, 2:02 a.m. UTC
  Good evening!


As a retro-computing enthousiast, I have been using objdump to 
disassemble old binaries for which I have lost the source code. This 
patch solves decoding issues that arise when a piece of code has been 
linked with an FPU emulator library for the 16 bit and early 32 bit x86 
processors. A bit of context : when intel cpus did not have an 
integrated FPU yet, Intel implemented FPU emulation using a scheme that 
allowed the linker to determine whether to replace the original FPU 
instructions with a special type of interrupt calls, where the resulting 
instruction bytes would basically fit the size of the original FPU 
instructions and would leave the operands encoded in the same way. 
Various vendors used various variants of that scheme, where the range of 
interrupts being used would differ but the approach would remain similar.
More details on all of this can be read on 
https://www.os2museum.com/wp/learn-something-old-every-day-part-xx-8087-emulation-on-8086-systems/
The problem with vanilla disassemblers is that they can decode those int 
calls but they do not know how to interpret the bytes following "CD XX", 
which are in effect instruction parameters/metadata and not code. And 
this prevents objdump from jumping properly to the next real instruction.


For example, for the same byte sequence, you will get this with the 
"fpu-emulation" parameter, added by this patch :


 > 2ad:       cd 35 1e 5e 00          int    $0x35 # fstps 0x5e

 > 2b2:       cd 3d              int    $0x3d # fwait

 > 2b4:       b8 ff ff             mov    $0xffff,%ax

 > 2b7:       50             push   %ax

 > 2b8:       9a 73 54 29 02          lcall  $0x229,$0x5473

(note the comments which give you the equivalent x87 instruction)

but without it, you get this :

 > 2ad:       cd 35              int    $0x35

 > 2af:       1e             push   %ds

 > 2b0:       5e             pop    %si

 > 2b1:       00 cd              add    %cl,%ch

 > 2b3:       3d b8 ff             cmp    $0xffb8,%ax

 > 2b6:       ff 50 9a             call   *-0x66(%bx,%si)

 > 2b9:       73 54              jae    0x30f

 > 2bb:       29 02              sub    %ax,(%bp,%si)



This attached patch is meant to address that issue for multiple vendors, 
using the following command-line parameter :
 >    --fpu-emulation=CONV       Decode 8087 emulation INTs for code 
using convention CONV (none, intel, microsoft, borland, watcom, ibm); 
same as -M fpu-emulation=CONV

The tests and documentation have been updated too.


I hope this patch will be seen as a valuable improvement, albeit maybe 
40 years late...


Regards!
  

Comments

Jan Beulich Sept. 25, 2026, 10:06 a.m. UTC | #1
On 25.09.2026 04:02, Wolfgang Sourdeau wrote:
> Good evening!
> 
> 
> As a retro-computing enthousiast, I have been using objdump to 
> disassemble old binaries for which I have lost the source code. This 
> patch solves decoding issues that arise when a piece of code has been 
> linked with an FPU emulator library for the 16 bit and early 32 bit x86 
> processors. A bit of context : when intel cpus did not have an 
> integrated FPU yet, Intel implemented FPU emulation using a scheme that 
> allowed the linker to determine whether to replace the original FPU 
> instructions with a special type of interrupt calls, where the resulting 
> instruction bytes would basically fit the size of the original FPU 
> instructions and would leave the operands encoded in the same way. 
> Various vendors used various variants of that scheme, where the range of 
> interrupts being used would differ but the approach would remain similar.
> More details on all of this can be read on 
> https://www.os2museum.com/wp/learn-something-old-every-day-part-xx-8087-emulation-on-8086-systems/
> The problem with vanilla disassemblers is that they can decode those int 
> calls but they do not know how to interpret the bytes following "CD XX", 
> which are in effect instruction parameters/metadata and not code. And 
> this prevents objdump from jumping properly to the next real instruction.
> 
> 
> For example, for the same byte sequence, you will get this with the 
> "fpu-emulation" parameter, added by this patch :
> 
> 
>  > 2ad:       cd 35 1e 5e 00          int    $0x35 # fstps 0x5e
> 
>  > 2b2:       cd 3d              int    $0x3d # fwait
> 
>  > 2b4:       b8 ff ff             mov    $0xffff,%ax
> 
>  > 2b7:       50             push   %ax
> 
>  > 2b8:       9a 73 54 29 02          lcall  $0x229,$0x5473
> 
> (note the comments which give you the equivalent x87 instruction)
> 
> but without it, you get this :
> 
>  > 2ad:       cd 35              int    $0x35
> 
>  > 2af:       1e             push   %ds
> 
>  > 2b0:       5e             pop    %si
> 
>  > 2b1:       00 cd              add    %cl,%ch
> 
>  > 2b3:       3d b8 ff             cmp    $0xffb8,%ax
> 
>  > 2b6:       ff 50 9a             call   *-0x66(%bx,%si)
> 
>  > 2b9:       73 54              jae    0x30f
> 
>  > 2bb:       29 02              sub    %ax,(%bp,%si)
> 
> 
> 
> This attached patch is meant to address that issue for multiple vendors, 
> using the following command-line parameter :
>  >    --fpu-emulation=CONV       Decode 8087 emulation INTs for code 
> using convention CONV (none, intel, microsoft, borland, watcom, ibm); 
> same as -M fpu-emulation=CONV
> 
> The tests and documentation have been updated too.
> 
> 
> I hope this patch will be seen as a valuable improvement, albeit maybe 
> 40 years late...

Sure, why not. Some formal things first: Please send as well-formed patch
(title, description, patch hunks). It'll also need your Signed-off-by:,
unless you have a copyright assignment in place with the FSF.

Further it complicates commenting if patches are sent as (only)
attachments. Therefore for now only a few basic remarks:

An arch-independent top-level option for objdump is imo inappropriate
here. Just -Mfpu-emulation=CONV will do.

In print_insn() I can't see why the new variable can't have its initial
value set by way of an initializer.

Please try to get away with as few separate declarations of static
functions as possible, by suitably ordering / inserting them ahead of
their first use.

I can't spot how you avoid applying the transformation also on 64-bit
code. Above you explicitly say it's 16-/32-bit code only which may need
this.

The comment ahead of emu_is_32bit_addr() is questionable: SIB addressing
has nothing to do with displacement size. That's purely determined by
the address size that's in use.

Subsequent functions also look to do generic x86 ModR/M decoding and
register printing, which we already do elsewhere. Please make sure you
use existing code/data wherever possible.

Something like

    return (int_no >= 0x14 && int_no <= 0x17)
      || (int_no >= 0x18 && int_no <= 0x1f);

is odd to see, as it is easier as

    return int_no >= 0x14 && int_no <= 0x1f;

"emu_kind" also wants to be consistently of type enum emu8087_kind.

Finally: This will want to come with a testcase.

Jan
  
Wolfgang Sourdeau Sept. 25, 2026, 12:27 p.m. UTC | #2
Hi Jan!


> 
> Sure, why not. Some formal things first: Please send as well-formed 
> patch
> (title, description, patch hunks). It'll also need your Signed-off-by:,
> unless you have a copyright assignment in place with the FSF.

I do not currently have a copyright assignment but doesn;'t this fit the 
trivial contribution of a few lines?
Otherwise, where and how to I write the "Signed-off-by:" comment?

> Further it complicates commenting if patches are sent as (only)
> attachments. Therefore for now only a few basic remarks:

In what form would you like me the send the patch?

> I can't spot how you avoid applying the transformation also on 64-bit
> code. Above you explicitly say it's 16-/32-bit code only which may need
> this.

Because the FPU started being included in the x86 chips with the i486. 
Thus, no emulation is needed for i486 and above. And since those chips 
were released much before 64 bit instructions showed up, this would 
provide support for a feature that does not exist for 64 bit processors 
and will probably never be implemented. Does that answer your question?

> 
> Finally: This will want to come with a testcase.

Sorry, I think I forgot them (I just created a git diff of the local 
changes, which excludes new files)

I will address the comments you made about the coding-style and the 
adjustements you asked and create another patch when it is done.


Thanks!
  
Jan Beulich Sept. 25, 2026, 1:34 p.m. UTC | #3
On 25.09.2026 14:27, Wolfgang Sourdeau wrote:
>> Sure, why not. Some formal things first: Please send as well-formed 
>> patch
>> (title, description, patch hunks). It'll also need your Signed-off-by:,
>> unless you have a copyright assignment in place with the FSF.
> 
> I do not currently have a copyright assignment but doesn;'t this fit the 
> trivial contribution of a few lines?

This is far more than "a few lines".

> Otherwise, where and how to I write the "Signed-off-by:" comment?

Below the description, followed by your name and email address. Please
look around, e.g. at other postings. Before adding this line you will
want to make sure you also understand what you signify by doing so.

>> Further it complicates commenting if patches are sent as (only)
>> attachments. Therefore for now only a few basic remarks:
> 
> In what form would you like me the send the patch?

Inline, e.g. via "git send-email". If don't use that and your mailer is
at risk of corrupting formatting, send it inline and as attachment. The
former to be able to comment on, the latter to use when actually meaning
to apply it.

>> I can't spot how you avoid applying the transformation also on 64-bit
>> code. Above you explicitly say it's 16-/32-bit code only which may need
>> this.
> 
> Because the FPU started being included in the x86 chips with the i486. 
> Thus, no emulation is needed for i486 and above. And since those chips 
> were released much before 64 bit instructions showed up, this would 
> provide support for a feature that does not exist for 64 bit processors 
> and will probably never be implemented. Does that answer your question?

No. I indicated that I can't spot how your new code is avoided when
disassembling 64-bit code. Afaict it is engaged in all modes right now.

Jan
  
Wolfgang Sourdeau Sept. 25, 2026, 1:36 p.m. UTC | #4
Thanks for the clarifications. I will do all of that soon.


Have a nice day!
  

Patch

diff --git a/binutils/doc/binutils.texi b/binutils/doc/binutils.texi
index 198ae8244ef..63b2c7b6d7c 100644
--- a/binutils/doc/binutils.texi
+++ b/binutils/doc/binutils.texi
@@ -2375,6 +2375,7 @@  objdump [@option{-a}|@option{--archive-headers}]
         [@option{--prefix=}@var{prefix}]
         [@option{--prefix-strip=}@var{level}]
         [@option{--insn-width=}@var{width}]
+        [@option{--fpu-emulation=}@var{borland|ibm|intel|microsoft|watcom}]
         [@option{--visualize-jumps[=color|=extended-color|=off]}
         [@option{--disassembler-color=[off|terminal|on|extended]}
         [@option{-U} @var{method}] [@option{--unicode=}@var{method}]
@@ -2743,6 +2744,20 @@  Display the symbol associated with immediate values, should there be
 one.  Only works on executable binaries and shared libraries, not
 static object files.
 
+@item fpu-emulation=@var{borland|ibm|intel|microsoft|watcom}
+Decode 8087 emulation @code{interrupt} instructions assuming the code
+uses conventions @code{intel}, @code{microsoft}, @code{borland},
+@code{watcom} or @code{ibm}.  Without this option @code{interrupt},
+instructions are left untouched.  The @code{intel} convention uses
+@code{INT 18h-1Fh} (plus @code{INT 14h-17h} for segment overrides); the
+other conventions use the DOS range @code{INT 34h-3Dh}.  Each decoded
+sequence shows the @code{interrupt} call, aligned with plain
+@code{interrupt} lines, and a comment mapping each byte group to the
+element it represents: the instruction bytes to the base FPU mnemonic,
+and each displacement argument to its value, e.g.  @code{int $0x3a #
+fmulp %st,%st(1)}.  An in-convention @code{interrupt} whose arguments do
+not decode is shown as a bare @code{interrupt} with a @code{# ?} marker.
+
 @end table
 
 For PowerPC, the @option{-M} argument @option{raw} selects
@@ -2975,6 +2990,18 @@  This is the default when @option{--prefix-addresses} is used.
 Display @var{width} bytes on a single line when disassembling
 instructions.
 
+@item --fpu-emulation=@var{borland|ibm|intel|microsoft|watcom}
+@cindex FPU emulation convention
+Decode 8087 emulation @code{interrupt} instructions assuming the code
+uses conventions @code{borland}, @code{ibm}, @code{intel},
+@code{microsoft} or @code{watcom}.  This is equivalent to @option{-M
+fpu-emulation=...}.  Without this option, the related
+@code{interrupt} instruction decoding will decode the sequence as a
+confusing series of ghost instructions.  Comment dialect and stability
+are as described under @option{-M fpu-emulation} above: parenthesized
+byte groups mapping bytes to elements, a @code{# ?} marker for
+undecodable sequences.
+
 @item --visualize-jumps[=color|=extended-color|=off]
 Visualize jumps that stay inside a function by drawing ASCII art between
 the start and target addresses.  The optional @option{=color} argument
diff --git a/binutils/objdump.c b/binutils/objdump.c
index 944b81b17e0..7807cce5258 100644
--- a/binutils/objdump.c
+++ b/binutils/objdump.c
@@ -371,6 +371,10 @@  usage (FILE *stream, int status)
       fprintf (stream, _("\
   -M, --disassembler-options=OPT Pass text OPT on to the disassembler\n"));
       fprintf (stream, _("\
+      --fpu-emulation=CONV       Decode x87 emulation interrupts for code using\n\
+                                 the specified convention.\n\
+                                   CONV=[intel|microsoft|borland|watcom|ibm]\n"));
+      fprintf (stream, _("\
   -EB --endian=big               Assume big endian format when disassembling\n"));
       fprintf (stream, _("\
   -EL --endian=little            Assume little endian format when disassembling\n"));
@@ -502,7 +506,8 @@  enum option_values
     OPTION_SFRAME,
     OPTION_VISUALIZE_JUMPS,
     OPTION_DISASSEMBLER_COLOR,
-    OPTION_MAP_GLOBAL_VARS
+    OPTION_MAP_GLOBAL_VARS,
+    OPTION_FPU_EMULATION
   };
 
 static struct option long_options[]=
@@ -525,6 +530,7 @@  static struct option long_options[]=
   {"disassemble-all", no_argument, NULL, 'D'},
   {"disassemble-zeroes", no_argument, NULL, 'z'},
   {"disassembler-options", required_argument, NULL, 'M'},
+  {"fpu-emulation", required_argument, NULL, OPTION_FPU_EMULATION},
   {"dwarf", optional_argument, NULL, OPTION_DWARF},
   {"dwarf-check", no_argument, 0, OPTION_DWARF_CHECK},
   {"dwarf-depth", required_argument, 0, OPTION_DWARF_DEPTH},
@@ -6127,6 +6133,25 @@  main (int argc, char **argv)
 	      free (options);
 	  }
 	  break;
+	case OPTION_FPU_EMULATION:
+	  {
+	    char *emu_opt;
+	    char *options;
+	    emu_opt = concat ("fpu-emulation=", optarg, (const char *) NULL);
+	    if (disassembler_options)
+	      {
+		options = concat (disassembler_options, ",",
+				  emu_opt, (const char *) NULL);
+		free (disassembler_options);
+		free (emu_opt);
+	      }
+	    else
+	      options = emu_opt;
+	    disassembler_options = remove_whitespace_and_extra_commas (options);
+	    if (!disassembler_options)
+	      free (options);
+	  }
+	  break;
 	case 'j':
 	  add_only (optarg);
 	  break;
diff --git a/opcodes/i386-dis.c b/opcodes/i386-dis.c
index 44529d0aa82..3f4d0be085b 100644
--- a/opcodes/i386-dis.c
+++ b/opcodes/i386-dis.c
@@ -289,6 +289,29 @@  struct dis_private {
   uint8_t the_buffer[2 * MAX_CODE_LENGTH - 1];
 };
 
+/* 8087 emulation conventions for 8086/8088/186/286/386 code without
+   a coprocessor.  Language translators emitted normal WAIT/ESC code
+   with fix-ups; the linker replaced WAIT/ESC pairs with software INT
+   instructions when linking with an emulation library (see fixup.txt).
+   Decoding such binaries as plain INT bytes hides the original FPU
+   instruction and misaligns the following bytes, so the disassembler
+   can optionally swallow the emulator arguments and show the
+   equivalent FPU instruction as a comment.  */
+enum emu8087_kind
+{
+  emu8087_none,
+  emu8087_intel,
+  emu8087_microsoft,
+  emu8087_unknown
+};
+
+static enum emu8087_kind emu8087_value_from_name (const char *);
+static enum emu8087_kind emu8087_parse_token (const char *);
+static int try_emu8087 (instr_info *, enum emu8087_kind, int);
+static int emu_failed_int (instr_info *, int);
+static bool OP_ST (instr_info *, int, int);
+static bool OP_STi (instr_info *, int, int);
+
 /* Mark parts used in the REX prefix.  When we are testing for
    empty prefix (for 8bit register REX extension), just mask it
    out.  Otherwise test for REX bit is excuse for existence of REX
@@ -9069,6 +9092,35 @@  prefix_name (enum address_mode mode, uint8_t pref, int sizeflag)
     }
 }
 
+static enum emu8087_kind
+emu8087_value_from_name (const char *name)
+{
+  if (strcasecmp (name, "intel") == 0)
+    return emu8087_intel;
+  if (strcasecmp (name, "microsoft") == 0
+      ||strcasecmp (name, "borland") == 0
+      ||strcasecmp (name, "watcom") == 0
+      ||strcasecmp (name, "ibm") == 0)
+    return emu8087_microsoft;
+  return emu8087_unknown;
+}
+
+
+/* Recognize the fpu-emulation=CONV token, where CONV names the 8087
+   emulation convention used in the code being disassembled.
+   Return the new emu kind, or -1 when TOKEN is not such an option.  */
+static enum emu8087_kind
+emu8087_parse_token (const char *token)
+{
+  static const char FPU_EMULATION_PREFIX[] = "fpu-emulation=";
+
+  if (!startswith (token, FPU_EMULATION_PREFIX))
+    return emu8087_none;
+
+  const char *name = token + sizeof(FPU_EMULATION_PREFIX) - 1;
+  return emu8087_value_from_name (name);
+}
+
 void
 print_i386_disassembler_options (FILE *stream)
 {
@@ -9094,6 +9146,10 @@  with the -M switch (multiple options should be separated by commas):\n"));
   fprintf (stream, _("  amd64       Display instruction in AMD64 ISA\n"));
   fprintf (stream, _("  intel64     Display instruction in Intel64 ISA\n"));
   fprintf (stream, _("  annotate-immediates  Annotate immediate operands that match symbols\n"));
+  fprintf (stream, _("  fpu-emulation=intel|microsoft|borland|watcom|ibm\n"));
+  fprintf (stream, _("              Decode 8087 emulation INT sequences for code\n"));
+  fprintf (stream, _("              using the given convention and show the\n"));
+  fprintf (stream, _("              equivalent FPU instruction as a comment\n"));
 }
 
 /* Bad opcode.  */
@@ -9777,6 +9833,7 @@  print_insn (bfd_vma pc, disassemble_info *info, int intel_syntax)
   int needcomma;
   bool intel_swap_2_3;
   int sizeflag, orig_sizeflag;
+  enum emu8087_kind emu8087;
   const char *p;
   struct dis_private priv;
   int prefix_length;
@@ -9806,6 +9863,7 @@  print_insn (bfd_vma pc, disassemble_info *info, int intel_syntax)
   char cm_out[MAX_OPERANDS][COMMENT_BUFFER_SIZE];
 
   priv.orig_sizeflag = AFLAG | DFLAG;
+  emu8087 = emu8087_none;
   if ((info->mach & bfd_mach_i386_i386) != 0)
     ins.address_mode = mode_32bit;
   else if (info->mach == bfd_mach_i386_i8086)
@@ -9878,6 +9936,12 @@  print_insn (bfd_vma pc, disassemble_info *info, int intel_syntax)
 	priv.orig_sizeflag |= SUFFIX_ALWAYS;
       else if (startswith (p, "annotate"))
 	ins.annotate_immediates = true;
+      else
+	{
+	  enum emu8087_kind emu_value = emu8087_parse_token (p);
+	  if (emu_value < emu8087_unknown)
+	    emu8087 = emu_value;
+	}
 
       p = strchr (p, ',');
       if (p != NULL)
@@ -9953,6 +10017,24 @@  print_insn (bfd_vma pc, disassemble_info *info, int intel_syntax)
       goto out;
     }
 
+  if (emu8087 != emu8087_none)
+    {
+      int emu_len = try_emu8087 (&ins, emu8087, sizeflag);
+      if (emu_len <= 0)
+	emu_len = emu_failed_int (&ins, emu8087);
+      if (emu_len > 0)
+	{
+	  info->insn_info_valid = 0;
+	  info->branch_delay_insns = 0;
+	  info->data_size = 0;
+	  info->insn_type = dis_noninsn;
+	  info->target = 0;
+	  info->target2 = 0;
+	  ret = emu_len;
+	  goto out;
+	}
+    }
+
   ins.two_source_ops = (*ins.codep == 0x62 || *ins.codep == 0xc8);
 
   if ((ins.prefixes & PREFIX_FWAIT)
@@ -10849,6 +10931,887 @@  dofloat (instr_info *ins, int sizeflag)
   return true;
 }
 
+/* True when MODRM/SIB addressing uses the 32-bit (SIB capable) form.
+   Both 32-bit and 64-bit modes always use it; 16-bit mode uses it only
+   with an address-size override.  Emulator binaries are 16-bit history,
+   but the same INT conventions decode with 32-bit addressing.  */
+static bool
+emu_is_32bit_addr (enum address_mode mode, int sizeflag)
+{
+  if (mode == mode_64bit)
+    return true;
+  return (sizeflag & AFLAG) != 0;
+}
+
+static bool
+emu_ensure (instr_info *ins, int total)
+{
+  return fetch_code (ins->info, ins->codep + total);
+}
+
+static bool
+emu_peek (instr_info *ins, int offset, unsigned int *val)
+{
+  if (!emu_ensure (ins, offset + 1))
+    return false;
+  *val = ins->codep[offset];
+  return true;
+}
+
+static int
+emu_disp_len_16 (int mod, int rm)
+{
+  if (mod == 1)
+    return 1;
+  if (mod == 2)
+    return 2;
+  if (mod == 0 && rm == 6)
+    return 2;
+  return 0;
+}
+
+static bool
+emu_sib_present_32 (int mod, int rm)
+{
+  return mod != 3 && rm == 4;
+}
+
+static int
+emu_disp_len_32 (int mod, int rm, bool has_sib, int sib_base)
+{
+  if (mod == 1)
+    return 1;
+  if (mod == 2)
+    return 4;
+  if (mod != 0)
+    return 0;
+  if (has_sib)
+    return sib_base == 5 ? 4 : 0;
+  return rm == 5 ? 4 : 0;
+}
+
+/* Split emulator arguments at MODRM_OFF into instruction bytes and
+   the displacement argument.  Sets *DISP_OFF to the first displacement
+   byte (the end of the instruction bytes) and *DISP_LEN to its length
+   (0 when the instruction takes no argument).  Returns false when the
+   bytes are not yet available.  The caller validated INT + ESC bytes.  */
+static bool
+emu_split_args (instr_info *ins, int modrm_off, bool is32,
+		int *disp_off, int *disp_len)
+{
+  unsigned int modrm;
+  int mod;
+  int rm;
+
+  if (!emu_peek (ins, modrm_off, &modrm))
+    return false;
+  mod = (modrm >> 6) & 3;
+  rm = modrm & 7;
+  if (mod == 3)
+    {
+      *disp_off = modrm_off + 1;
+      *disp_len = 0;
+      return true;
+    }
+  if (!is32)
+    {
+      *disp_len = emu_disp_len_16 (mod, rm);
+      *disp_off = modrm_off + 1;
+      return emu_ensure (ins, *disp_off + *disp_len);
+    }
+  if (!emu_sib_present_32 (mod, rm))
+    {
+      *disp_len = emu_disp_len_32 (mod, rm, false, 0);
+      *disp_off = modrm_off + 1;
+      return emu_ensure (ins, *disp_off + *disp_len);
+    }
+  {
+    unsigned int sib;
+
+    if (!emu_peek (ins, modrm_off + 1, &sib))
+      return false;
+    *disp_len = emu_disp_len_32 (mod, rm, true, sib & 7);
+    *disp_off = modrm_off + 2;
+    return emu_ensure (ins, *disp_off + *disp_len);
+  }
+}
+
+/* Advance past "{alt|...}": Intel syntax takes the second
+   alternative, AT&T syntax just steps past the opening brace.  */
+static const char *
+emu_template_alt (const char *tmpl, bool intel_syntax)
+{
+  if (!intel_syntax)
+    return tmpl + 1;
+  do
+    tmpl++;
+  while (*tmpl != '|' && *tmpl != '}' && *tmpl != '\0');
+  if (*tmpl == '|')
+    tmpl++;
+  return tmpl;
+}
+
+/* Advance past the rest of a "{...}" template to its closing brace.  */
+static const char *
+emu_template_end (const char *tmpl)
+{
+  while (*tmpl != '}' && *tmpl != '\0')
+    tmpl++;
+  if (*tmpl == '}')
+    tmpl++;
+  return tmpl;
+}
+
+/* Append the 'r' of a "{[!]M|r}" reverse form when INTEL_MNEMONIC
+   disagrees with COND, as putop does.  Returns the new length.  */
+static size_t
+emu_append_mark (char *out, size_t size, size_t i,
+		 bool intel_mnemonic, int cond)
+{
+  if (i + 1 < size && intel_mnemonic != cond)
+    {
+      out[i] = 'r';
+      return i + 1;
+    }
+  return i;
+}
+
+/* Copy the mnemonic of a float template the way putop renders it
+   for the wait-inclusive instruction: "{s|}"-style size selections
+   are kept verbatim in AT&T ("fadds") and dropped in Intel ("fadd"),
+   "{!M|r}"/"{M|}" reverse forms take their 'r' ("fsubrp" and
+   friends), the lone "C" code is consumed without output ("fldenv",
+   never "fldenvC"), and the "N" wait code is consumed without output
+   too: an emulated sequence stands for the whole wait-inclusive
+   instruction (the wait itself is emulated separately), so "fNinit"
+   renders as "finit", never "fninit".  */
+static void
+emu_mnemonic_base (const char *tmpl, char *out, size_t size,
+		   bool intel_syntax, bool intel_mnemonic)
+{
+  size_t i = 0;
+  int cond = 1;
+
+  if (tmpl == NULL)
+    tmpl = "(bad)";
+  while (i + 1 < size && *tmpl != '\0')
+    if (*tmpl == '{')
+      tmpl = emu_template_alt (tmpl, intel_syntax);
+    else if (*tmpl == '|' || *tmpl == '}')
+      tmpl = emu_template_end (tmpl);
+    else if (*tmpl == '!')
+      {
+	cond = 0;
+	tmpl++;
+      }
+    else if (*tmpl == 'M')
+      {
+	i = emu_append_mark (out, size, i, intel_mnemonic, cond);
+	tmpl++;
+      }
+    else if (*tmpl == 'C' || *tmpl == 'N')
+      tmpl++;
+    else
+      {
+	out[i] = *tmpl;
+	i++;
+	tmpl++;
+      }
+  out[i] = '\0';
+}
+
+/* Read a little-endian displacement of DISP_LEN bytes at DISP_OFF.  */
+static unsigned int
+emu_read_disp (instr_info *ins, int disp_off, int disp_len)
+{
+  int i;
+  unsigned int value = 0;
+
+  for (i = 0; i < disp_len; i++)
+    {
+      unsigned int byte;
+
+      if (!emu_peek (ins, disp_off + i, &byte))
+	return 0;
+      value |= byte << (8 * i);
+    }
+  return value;
+}
+
+/* Sign-extend a DISP_LEN-byte displacement for display, so a negative
+   register-relative offset prints as "-0x2" rather than "+0xfe".  */
+static int
+emu_signed_disp (unsigned int disp, int disp_len)
+{
+  if (disp_len == 1)
+    return (int) (int8_t) disp;
+  if (disp_len == 2)
+    return (int) (int16_t) disp;
+  return (int) disp;
+}
+
+/* Format a register-relative displacement: "+0x10" or "-0x2" joined
+   after registers in Intel syntax, "0x10" or "-0x2" before "(...)"
+   in AT&T syntax, or "" when DISP_LEN is 0.  */
+static void
+emu_format_rel_disp (char *out, size_t size, unsigned int disp, int disp_len,
+		     bool intel_syntax)
+{
+  int sdisp;
+  unsigned int mag;
+
+  if (disp_len == 0)
+    {
+      out[0] = '\0';
+      return;
+    }
+  sdisp = emu_signed_disp (disp, disp_len);
+  if (sdisp < 0)
+    {
+      mag = 0u - (unsigned int) sdisp;
+      snprintf (out, size, "-0x%x", mag);
+    }
+  else if (intel_syntax)
+    snprintf (out, size, "+0x%x", disp);
+  else
+    snprintf (out, size, "0x%x", disp);
+}
+
+/* Segment register name in the selected syntax: AT&T unless Intel
+   was requested.  SEG is 0=ES,1=CS,2=SS,3=DS.  */
+static const char *
+emu_seg_name (int seg, bool intel_syntax)
+{
+  if (intel_syntax)
+    switch (seg)
+      {
+      case 0:
+	return "es";
+      case 1:
+	return "cs";
+      case 2:
+	return "ss";
+      case 3:
+	return "ds";
+      default:
+	return "ds";
+      }
+  switch (seg)
+    {
+    case 0:
+      return "%es";
+    case 1:
+      return "%cs";
+    case 2:
+      return "%ss";
+    case 3:
+      return "%ds";
+    default:
+      return "%ds";
+    }
+}
+
+/* 32-bit register name in the selected syntax: AT&T unless Intel
+   was requested.  */
+static const char *
+emu_reg32_name (int reg, bool intel_syntax)
+{
+  if (intel_syntax)
+    switch (reg)
+      {
+      case 0:
+	return "eax";
+      case 1:
+	return "ecx";
+      case 2:
+	return "edx";
+      case 3:
+	return "ebx";
+      case 4:
+	return "esp";
+      case 5:
+	return "ebp";
+      case 6:
+	return "esi";
+      case 7:
+	return "edi";
+      default:
+	return "eax";
+      }
+  switch (reg)
+    {
+    case 0:
+      return "%eax";
+    case 1:
+      return "%ecx";
+    case 2:
+      return "%edx";
+    case 3:
+      return "%ebx";
+    case 4:
+      return "%esp";
+    case 5:
+      return "%ebp";
+    case 6:
+      return "%esi";
+    case 7:
+      return "%edi";
+    default:
+      return "%eax";
+    }
+}
+
+/* Intel memory size directive (with trailing space) for an emulated
+   operand mode, or the empty string when the instruction takes none.  */
+static const char *
+emu_size_prefix (int mode)
+{
+  switch (mode)
+    {
+    case w_mode:
+      return "WORD PTR ";
+    case d_mode:
+      return "DWORD PTR ";
+    case q_mode:
+      return "QWORD PTR ";
+    case t_mode:
+      return "TBYTE PTR ";
+    default:
+      return "";
+    }
+}
+
+/* Format a 16-bit MODRM address.  SEG is -1 or 0=ES,1=CS,2=SS,3=DS.
+   DISP_LEN is 0 when no displacement is present.  */
+static void
+emu_format_addr_16 (char *out, size_t size, bool intel_syntax,
+		    int mod, int rm, unsigned int disp, int disp_len, int seg)
+{
+  static const char * const base_att[8] =
+    { "%bx", "%bx", "%bp", "%bp", "%si", "%di", "%bp", "%bx" };
+  static const char * const base_intel[8] =
+    { "bx", "bx", "bp", "bp", "si", "di", "bp", "bx" };
+  static const char * const index_att[8] =
+    { "%si", "%di", "%si", "%di", NULL, NULL, NULL, NULL };
+  static const char * const index_intel[8] =
+    { "si", "di", "si", "di", NULL, NULL, NULL, NULL };
+  const char *base = NULL;
+  const char *index = NULL;
+
+  if (!(mod == 0 && rm == 6))
+    {
+      base = intel_syntax ? base_intel[rm] : base_att[rm];
+      index = intel_syntax ? index_intel[rm] : index_att[rm];
+    }
+  if (intel_syntax)
+    {
+      char core[96];
+      const char *seg_name = NULL;
+
+      core[0] = '\0';
+      if (seg >= 0)
+	seg_name = emu_seg_name (seg, true);
+      if (base != NULL)
+	snprintf (core + strlen (core), sizeof core - strlen (core),
+		  "%s", base);
+      if (index != NULL)
+	snprintf (core + strlen (core), sizeof core - strlen (core),
+		  "%s%s", base != NULL ? "+" : "", index);
+      if (base != NULL || index != NULL)
+	{
+	  char rel[24];
+
+	  emu_format_rel_disp (rel, sizeof rel, disp, disp_len, true);
+	  snprintf (core + strlen (core), sizeof core - strlen (core),
+		    "%s", rel);
+	}
+      else if (disp_len != 0)
+	snprintf (core + strlen (core), sizeof core - strlen (core),
+		  "%s0x%x", core[0] != '\0' ? "+" : "", disp);
+      if (core[0] == '\0')
+	snprintf (core, sizeof core, "0x%x", disp);
+      if (base != NULL || index != NULL)
+	{
+	  if (seg_name != NULL)
+	    snprintf (out, size, "%s:[%s]", seg_name, core);
+	  else
+	    snprintf (out, size, "[%s]", core);
+	}
+      else if (seg_name != NULL)
+	snprintf (out, size, "%s:%s", seg_name, core);
+      else
+	snprintf (out, size, "%s:%s", emu_seg_name (3, true), core);
+      return;
+    }
+  {
+    char segbuf[8];
+    const char *seg_pfx = "";
+
+    if (seg >= 0)
+      {
+	snprintf (segbuf, sizeof segbuf, "%s:",
+		  emu_seg_name (seg, false));
+	seg_pfx = segbuf;
+      }
+    if (base == NULL)
+      snprintf (out, size, "%s0x%x", seg_pfx, disp);
+    else
+      {
+	char rel[24];
+
+	emu_format_rel_disp (rel, sizeof rel, disp, disp_len, false);
+	if (index == NULL)
+	  snprintf (out, size, "%s%s(%s)", seg_pfx, rel, base);
+	else
+	  snprintf (out, size, "%s%s(%s,%s)", seg_pfx, rel, base, index);
+      }
+  }
+}
+
+/* Print the "int" head padded like the core disassembler pads
+   mnemonics (to 7 columns), so $0xNN aligns with neighbouring lines.  */
+static void
+emu_print_int_op (instr_info *ins, unsigned int int_no)
+{
+  bool intel_syntax = ins->intel_syntax != 0;
+
+  i386_dis_printf (ins->info, dis_style_mnemonic, "int    ");
+  if (intel_syntax)
+    i386_dis_printf (ins->info, dis_style_text, "%#x", int_no);
+  else
+    i386_dis_printf (ins->info, dis_style_text, "$%#x", int_no);
+}
+
+static void emu_format_addr_32 (char *, size_t, bool,
+				int, int, unsigned int, bool,
+				unsigned int, int, int);
+
+/* "%st(i)"/"st(i)" for an STi operand, "%st"/"st" for ST, in the
+   selected syntax: AT&T unless Intel was requested.  */
+static void
+emu_st_text (bool is_sti, int rm, bool intel_syntax, char *out, size_t size)
+{
+  if (is_sti)
+    {
+      if (intel_syntax)
+	snprintf (out, size, "st(%d)", rm);
+      else
+	snprintf (out, size, "%%st(%d)", rm);
+    }
+  else if (intel_syntax)
+    snprintf (out, size, "st");
+  else
+    snprintf (out, size, "%%st");
+}
+
+/* FPU register form (MOD==3) as comment text, WAIT form.  Operands
+   follow the selected syntax: AT&T unless Intel was requested.  Like
+   the core printer, two operands print as op[0],op[1] in Intel order
+   and reversed in AT&T order.  */
+static void
+emu_fpu_reg_text (int esc, unsigned int modrm, bool intel_syntax,
+		  bool intel_mnemonic, char *out, size_t size)
+{
+  int reg = (modrm >> 3) & 7;
+  int rm = modrm & 7;
+  const struct dis386 *dp = &float_reg[esc - 0xd8][reg];
+  char base[32];
+  char first[16];
+  char second[16];
+
+  if (dp->name == NULL)
+    {
+      const char *tmpl = fgrps[dp->op[0].bytemode][rm];
+      emu_mnemonic_base (tmpl, base, sizeof base,
+			 intel_syntax, intel_mnemonic);
+      if (esc == 0xdf && modrm == 0xe0)
+        {
+          if (intel_syntax)
+            snprintf (out, size, "%s ax", base);
+          else
+            snprintf (out, size, "%s %%ax", base);
+          return;
+        }
+      snprintf (out, size, "%s", base);
+      return;
+    }
+  emu_mnemonic_base (dp->name, base, sizeof base,
+		       intel_syntax, intel_mnemonic);
+  emu_st_text (dp->op[0].rtn == OP_STi, rm, intel_syntax,
+	       first, sizeof first);
+  if (dp->op[1].rtn == NULL)
+    {
+      snprintf (out, size, "%s %s", base, first);
+      return;
+    }
+  emu_st_text (dp->op[1].rtn == OP_STi, rm, intel_syntax,
+	       second, sizeof second);
+  if (intel_syntax)
+    snprintf (out, size, "%s %s,%s", base, first, second);
+  else
+    snprintf (out, size, "%s %s,%s", base, second, first);
+}
+
+/* FPU memory form (MOD!=3) as comment text.  SEG is -1 or segment.  */
+static void
+emu_fpu_mem_text (instr_info *ins, int esc, int modrm_off,
+		  int seg, bool is32, char *out, size_t size)
+{
+  unsigned int modrm = ins->codep[modrm_off];
+  int mod = (modrm >> 6) & 3;
+  int reg = (modrm >> 3) & 7;
+  int rm = modrm & 7;
+  int fp_indx = (esc - 0xd8) * 8 + reg;
+  char base[32];
+  char addr[128];
+  bool intel_syntax = ins->intel_syntax != 0;
+
+  emu_mnemonic_base (float_mem[fp_indx], base, sizeof base,
+		       intel_syntax, ins->intel_mnemonic);
+  if (!is32)
+    {
+      int disp_len = emu_disp_len_16 (mod, rm);
+      unsigned int disp = emu_read_disp (ins, modrm_off + 1, disp_len);
+      emu_format_addr_16 (addr, sizeof addr, intel_syntax,
+			  mod, rm, disp, disp_len, seg);
+    }
+  else
+    {
+      unsigned int sib = 0;
+      bool sib_valid = emu_sib_present_32 (mod, rm);
+      int disp_off = modrm_off + 1;
+      int disp_len;
+      unsigned int disp;
+      if (sib_valid)
+        {
+          sib = ins->codep[modrm_off + 1];
+          disp_off = modrm_off + 2;
+        }
+      disp_len = emu_disp_len_32 (mod, rm, sib_valid, sib & 7);
+      disp = emu_read_disp (ins, disp_off, disp_len);
+      emu_format_addr_32 (addr, sizeof addr, intel_syntax,
+			  mod, rm, sib, sib_valid, disp, disp_len, seg);
+    }
+  if (intel_syntax)
+    snprintf (out, size, "%s %s%s", base,
+	      emu_size_prefix (float_mem_mode[fp_indx]), addr);
+  else
+    snprintf (out, size, "%s %s", base, addr);
+}
+
+static void
+emu_fpu_text (instr_info *ins, int esc, int modrm_off,
+	      int seg, bool is32, char *out, size_t size)
+{
+  unsigned int modrm = ins->codep[modrm_off];
+  int mod = (modrm >> 6) & 3;
+  bool intel_syntax = ins->intel_syntax != 0;
+
+  if (mod == 3)
+    emu_fpu_reg_text (esc, modrm, intel_syntax, ins->intel_mnemonic,
+		      out, size);
+  else
+    emu_fpu_mem_text (ins, esc, modrm_off, seg, is32, out, size);
+}
+
+/* Format a 32-bit (or 64-bit) MODRM/SIB address.  SIB_VALID is false
+   when no SIB byte is present.  SEG is -1 or 0=ES,1=CS,2=SS,3=DS.  */
+static void
+emu_format_addr_32 (char *out, size_t size, bool intel_syntax,
+		    int mod, int rm, unsigned int sib, bool sib_valid,
+		    unsigned int disp, int disp_len, int seg)
+{
+  const char *base = NULL;
+  const char *index = NULL;
+  int scale = 1;
+  char base_buf[16];
+  char index_buf[16];
+
+  if (sib_valid)
+    {
+      int sib_base = sib & 7;
+      int sib_index = (sib >> 3) & 7;
+      scale = 1 << ((sib >> 6) & 3);
+      if (sib_index != 4)
+        {
+          snprintf (index_buf, sizeof index_buf, "%s",
+		    emu_reg32_name (sib_index, intel_syntax));
+          index = index_buf;
+        }
+      if (!(mod == 0 && sib_base == 5))
+        {
+          snprintf (base_buf, sizeof base_buf, "%s",
+		    emu_reg32_name (sib_base, intel_syntax));
+          base = base_buf;
+        }
+    }
+  else if (!(mod == 0 && rm == 5))
+    base = emu_reg32_name (rm, intel_syntax);
+  if (intel_syntax)
+    {
+      char core[96];
+      const char *seg_name = NULL;
+      core[0] = '\0';
+      if (seg >= 0)
+        seg_name = emu_seg_name (seg, true);
+      if (base != NULL)
+        snprintf (core + strlen (core), sizeof core - strlen (core),
+		  "%s", base);
+      if (index != NULL)
+        snprintf (core + strlen (core), sizeof core - strlen (core),
+		  "%s%s*%d", base != NULL ? "+" : "", index, scale);
+      if (base != NULL || index != NULL)
+	{
+	  char rel[24];
+
+	  emu_format_rel_disp (rel, sizeof rel, disp, disp_len, true);
+	  snprintf (core + strlen (core), sizeof core - strlen (core),
+		    "%s", rel);
+	}
+      else if (disp_len != 0)
+        snprintf (core + strlen (core), sizeof core - strlen (core),
+		  "%s0x%x", core[0] != '\0' ? "+" : "", disp);
+      if (core[0] == '\0')
+        snprintf (core, sizeof core, "0x%x", disp);
+      if (base != NULL || index != NULL)
+        {
+          if (seg_name != NULL)
+            snprintf (out, size, "%s:[%s]", seg_name, core);
+          else
+            snprintf (out, size, "[%s]", core);
+        }
+      else if (seg_name != NULL)
+        snprintf (out, size, "%s:%s", seg_name, core);
+      else
+        snprintf (out, size, "%s:%s", emu_seg_name (3, true), core);
+      return;
+    }
+  {
+    char base_s[32];
+    char index_s[32];
+    char segbuf[8];
+    const char *seg_pfx = "";
+    base_s[0] = '\0';
+    index_s[0] = '\0';
+    if (seg >= 0)
+      {
+        snprintf (segbuf, sizeof segbuf, "%s:",
+                  emu_seg_name (seg, false));
+        seg_pfx = segbuf;
+      }
+    if (base != NULL)
+      snprintf (base_s, sizeof base_s, "%s", base);
+    if (index != NULL)
+      snprintf (index_s, sizeof index_s, ",%s,%d", index, scale);
+    if (base == NULL && index == NULL)
+      snprintf (out, size, "%s0x%x", seg_pfx, disp);
+    else
+      {
+        char rel[24];
+
+        emu_format_rel_disp (rel, sizeof rel, disp, disp_len, false);
+        snprintf (out, size, "%s%s(%s%s)", seg_pfx, rel, base_s, index_s);
+      }
+  }
+}
+
+/* Print "int    $0xNN # <fpu>" for a decoded emulator sequence,
+   with the FPU instruction and its operands in the selected syntax
+   (AT&T unless Intel was requested).  */
+static void
+emu_print_decoded (instr_info *ins, unsigned int int_no, const char *fpu)
+{
+  emu_print_int_op (ins, int_no);
+  i386_dis_printf (ins->info, dis_style_comment_start, " # ");
+  i386_dis_printf (ins->info, dis_style_text, "%s", fpu);
+}
+
+static int
+emu_microsoft_wait (instr_info *ins, unsigned int int_no)
+{
+  if (int_no != 0x3d)
+    return 0;
+  emu_print_decoded (ins, int_no, "fwait");
+  return 2;
+}
+
+static int
+emu_microsoft_normal (instr_info *ins, unsigned int int_no, bool is32)
+{
+  int esc;
+  int disp_off;
+  int disp_len;
+  char fpu[256];
+
+  if (int_no < 0x34 || int_no > 0x3b)
+    return 0;
+  esc = 0xd8 + (int_no - 0x34);
+  if (!emu_split_args (ins, 2, is32, &disp_off, &disp_len))
+    return 0;
+  emu_fpu_text (ins, esc, 2, -1, is32, fpu, sizeof fpu);
+  emu_print_decoded (ins, int_no, fpu);
+  return disp_off + disp_len;
+}
+
+/* Segment index (0=ES,1=CS,2=SS,3=DS) coded in a Microsoft INT 3Ch
+   prime byte's top two bits (00=DS,01=SS,10=CS,11=ES).  */
+static int
+emu_seg_from_prime (unsigned int prime)
+{
+  if ((prime & 0xc0) == 0x00)
+    return 3;
+  if ((prime & 0xc0) == 0x40)
+    return 2;
+  if ((prime & 0xc0) == 0x80)
+    return 1;
+  return 0;
+}
+
+static int
+emu_microsoft_seg (instr_info *ins, unsigned int int_no, bool is32)
+{
+  unsigned int prime;
+  int esc;
+  int disp_off;
+  int disp_len;
+  char fpu[256];
+
+  if (int_no != 0x3c)
+    return 0;
+  if (!emu_peek (ins, 2, &prime))
+    return 0;
+  if ((prime & 0x38) != 0x18)
+    return 0;
+  esc = 0xd8 | (prime & 7);
+  if (!emu_split_args (ins, 3, is32, &disp_off, &disp_len))
+    return 0;
+  emu_fpu_text (ins, esc, 3, emu_seg_from_prime (prime), is32,
+		fpu, sizeof fpu);
+  emu_print_decoded (ins, int_no, fpu);
+  return disp_off + disp_len;
+}
+
+static int
+emu_try_microsoft (instr_info *ins, unsigned int int_no, bool is32)
+{
+  int len = emu_microsoft_wait (ins, int_no);
+  if (len != 0)
+    return len;
+  len = emu_microsoft_normal (ins, int_no, is32);
+  if (len != 0)
+    return len;
+  return emu_microsoft_seg (ins, int_no, is32);
+}
+
+static int
+emu_intel_normal (instr_info *ins, unsigned int int_no, bool is32)
+{
+  int esc;
+  int disp_off;
+  int disp_len;
+  char fpu[256];
+
+  if (int_no < 0x18 || int_no > 0x1f)
+    return 0;
+  esc = 0xd8 + (int_no - 0x18);
+  if (!emu_split_args (ins, 2, is32, &disp_off, &disp_len))
+    return 0;
+  emu_fpu_text (ins, esc, 2, -1, is32, fpu, sizeof fpu);
+  emu_print_decoded (ins, int_no, fpu);
+  return disp_off + disp_len;
+}
+
+static int
+emu_intel_seg (instr_info *ins, unsigned int int_no, bool is32)
+{
+  unsigned int esc_byte;
+  int disp_off;
+  int disp_len;
+  char fpu[256];
+
+  if (int_no < 0x14 || int_no > 0x17)
+    return 0;
+  if (!emu_peek (ins, 2, &esc_byte))
+    return 0;
+  if (esc_byte < 0xd8 || esc_byte > 0xdf)
+    return 0;
+  /* INT 14h-17h selects ES,CS,SS,DS in that order.  */
+  if (!emu_split_args (ins, 3, is32, &disp_off, &disp_len))
+    return 0;
+  emu_fpu_text (ins, esc_byte, 3, (int) (int_no - 0x14), is32,
+		fpu, sizeof fpu);
+  emu_print_decoded (ins, int_no, fpu);
+  return disp_off + disp_len;
+}
+
+static int
+emu_try_intel (instr_info *ins, unsigned int int_no, bool is32)
+{
+  int len = emu_intel_normal (ins, int_no, is32);
+  if (len != 0)
+    return len;
+  return emu_intel_seg (ins, int_no, is32);
+}
+
+/* True when INT_NO belongs to the convention's emulator range,
+   whether or not its arguments go on to decode.  */
+static bool
+emu_int_in_convention (int emu_kind, unsigned int int_no)
+{
+  if (emu_kind == emu8087_microsoft)
+    return int_no >= 0x34 && int_no <= 0x3d;
+  if (emu_kind == emu8087_intel)
+    return (int_no >= 0x14 && int_no <= 0x17)
+      || (int_no >= 0x18 && int_no <= 0x1f);
+  return false;
+}
+
+/* Print a bare INT with a "# ?" marker for an in-convention interrupt
+   whose emulator arguments failed to decode (truncated or corrupt),
+   so it cannot pass as a genuine plain interrupt.  Returns 2 when
+   printed, else 0.  Only runs after try_emu8087 declined the bytes.  */
+static int
+emu_failed_int (instr_info *ins, int emu_kind)
+{
+  unsigned int b0;
+  unsigned int int_no;
+
+  if (ins->nr_prefixes != 0)
+    return 0;
+  if (!emu_peek (ins, 0, &b0) || b0 != 0xcd)
+    return 0;
+  if (!emu_peek (ins, 1, &int_no))
+    return 0;
+  if (!emu_int_in_convention (emu_kind, int_no))
+    return 0;
+  emu_print_int_op (ins, int_no);
+  i386_dis_printf (ins->info, dis_style_comment_start, " # ?");
+  return 2;
+}
+
+static int
+try_emu8087 (instr_info *ins, enum emu8087_kind emu_kind, int sizeflag)
+{
+  unsigned int b0;
+  unsigned int int_no;
+  bool is32;
+
+  if (ins->nr_prefixes != 0)
+    return 0;
+  if (!emu_peek (ins, 0, &b0))
+    return 0;
+  if (b0 != 0xcd)
+    return 0;
+  if (!emu_peek (ins, 1, &int_no))
+    return 0;
+  is32 = emu_is_32bit_addr (ins->address_mode, sizeflag);
+  if (emu_kind == emu8087_microsoft)
+    return emu_try_microsoft (ins, int_no, is32);
+  if (emu_kind == emu8087_intel)
+    return emu_try_intel (ins, int_no, is32);
+  return 0;
+}
+
 static bool
 OP_ST (instr_info *ins, int bytemode ATTRIBUTE_UNUSED,
        int sizeflag ATTRIBUTE_UNUSED)