Hex-Rays Blog: IDA Pro Tutorials & Reverse Engineering Tips

IDA 9.5: 3 new decompilers and 10 platform updates

Written by Alexandru Petenchea | Oct 6, 2026

Good tooling starts with solid fundamentals. When every instruction decodes correctly and every function reads as clean pseudocode, you can trust what IDA shows and put your time into the binary itself. That holds whether the one reading the output is a person or an agent driving IDA. IDA 9.5 brings that reliability to three new architectures and sharpens it on several familiar ones.

First, the new decompilers:

  • Android DEX, the bytecode behind Java and Kotlin apps, now decompiles to Java
  • Infineon TriCore, the CPU in many automotive ECUs, now decompiles to C
  • Qualcomm Hexagon, the DSP in Snapdragon chips that runs modems, audio and camera pipelines, now decompiles to C as well

The existing decompilers gain ground too. AVX code now reads as C, ARC functions end in proper returns, and the decompiler now understands MIPS Release 6 and a handful of game consoles. On the disassembler side, Armv9 picks up a batch of new extensions.

Below, we go through each of these with real examples and, where it helps, a before-and-after.

A word on editions: the TriCore and Hexagon decompilers are available in IDA Pro. The Dalvik decompiler is available in both IDA Pro and IDA Home, so if you take apart Android apps on a Home license, it's yours too.

Dalvik: Reading APKs Like Source

Android apps written in Java or Kotlin ship their code as DEX bytecode, and in 9.5 IDA shows it as Java instead of registers and invoke-virtual. This is not a separate viewer bolted on the side. It is the same database, and you rename, retype, comment and script it the way you always have.

Open the APK

Load an APK, pick classes.dex, and press Tab. You get the class view: packages, classes, fields and methods, with every method's pseudocode in place. Multidex apps load as one database, and a call into another dex file takes you straight to its body.

Everything in that view is a real entity in the database. Rename a method and its callers follow. Retype it with "Set type", which takes Java syntax, and the callers' pseudocode updates. A virtual call also references the overrides in subclasses, so the cross-references show where a call can really land.

String Concatenation

String concatenation such as "and=" + (a & b) is usually compiled into a StringBuilder sequence: create the builder, append() each piece, call toString(). This is what the bytecode looks like:

IDA 9.5 folds the chain back into what the programmer typed:

static String looseOperands(int p0, int p1)
{
  return "and=" + (p0 & p1) + " shl=" + (p0 << p1);
}

The parentheses are there because they have to be: without them, "and=" + p0 & p1 would not even compile. Not every builder folds, though. When + would change the result, the builder stays:

static String twoNumbers(int p0, int p1)
{
  return new StringBuilder().append(p0).append(p1).toString();
}

A builder that lives across statements, such as one filled in a loop, is also left as it is.

Names That Survived the Obfuscator

Obfuscators rename fields to a, b, c. Serializers can't afford to: the JSON on the wire still says sections, so a meaningful name survives in the serializer annotation. The decompiler reads it:

class GsonModel
{
  static int requestCount;   // was c
  int sections;   // was a
  int pageLoad;   // was b
  ...

The same works for Gson, Moshi, Jackson and kotlinx.serialization, and names also come back from Kotlin metadata and DEX debug info. A serialized name that is not a valid Java identifier, such as nav-pag_name, is left as an annotation on the original field. The name is recomputed on every decompilation, and a name you set yourself always wins.

Working in the Class View

A few things worth knowing on day one:

  • Tab switches between the disassembly and the class view, and the two stay in sync
  • Ctrl+Shift+F6, or Ctrl+double-click on a class in the tree, opens a second class view, so you can read a caller and a callee side by side
  • the usual pseudocode actions are there: rename, comment, "Set type", invert if, hide casts, collapse

Everything the class view shows is also available to scripts, through a new class API in C++ and IDAPython. Dalvik is its first backend. This dumps every class of an APK to a .java file, decompiled bodies included:

import ida_hexrays

if ida_hexrays.init_hexrays_plugin():
    classes = ida_hexrays.classes_t()
    if ida_hexrays.get_classes(classes):
        for c in classes:
            if c.outer:
                continue
            ok, lines = ida_hexrays.render_class(c)
            if ok:
                with open(c.name + ".java", "w") as f:
                    f.write("\n".join(lines))

The SDK ships a longer example, list_classes.py, that also prints the outline of the class under the cursor.

TriCore: Your ECU, in C

TriCore firmware usually reaches you as a flash dump: no symbols, no headers, a few megabytes of engine control. In 9.5 it decompiles to C.

Two things about TriCore needed more than a new lifter.

Pointers Live in A Registers

TriCore has two register files: D registers for data and A registers for addresses. The calling convention follows the split. Integer arguments go in d4-d7, pointer arguments in a4-a7, and a function returns an integer in d2 but a pointer in a2. Take this function:

p arrives in a4 and n in d4. Both are "the first argument" of their own bank, so the machine code is exactly the same as for find_zero(unsigned n, int *p). The order of the arguments is simply not in the binary. 9.5 teaches the decompiler core about separate address registers. Each bank fills on its own, an argument goes on the stack when it doesn't fit in the registers of its bank, and pointer arguments and return values are recognized in A registers. The same function comes out as:

int *__fastcall find_zero(int a1, int *a2)
{
  int *result; // a2
  int v3; // a3
  ...

To change the ABI, use "Options/Compiler". To rearrange function arguments, use "Set item type" (or the Y shortcut).

Division Step Folding

The DIV instruction arrived with TriCore 1.6. Older cores, and plenty of code still built for them, divide in steps: dvinit prepares the operands, each dvstep produces eight bits of quotient, and dvadj fixes up the sign. A signed 32-bit division is therefore dvinit + four dvstep + dvadj, either written out in a row or, as Tasking prefers, as a loop that runs dvstep four times:

The decompiler unrolls the loop, recognizes the pattern, and folds it into a single division.

int __fastcall sdiv32_loop(int a1, int a2, int a3, char a4, _WORD *a5)
{
  int result; // d2

  result = 0;
  if ( a2 != 0 )
  {
    *a5 = __sha(a1 - a3, a4) / a2;
    return 1;
  }
  return result;
}

Disassembler Improvements

  • function discovery in raw flash dumps recognizes functions by their stack-frame setup, and interrupt handlers by their context save
  • the register tracker resolves 64-bit register pairs such as e4, even when the two halves were set by different instructions
  • shift counts and the BISR, SYSCALL and HVCALL immediates are decoded with their real widths

Hexagon: Everything Happens at Once

Hexagon instructions are grouped into packets of up to four, and a packet executes as if it were a single instruction. That makes the disassembly hard to read top to bottom, and in 9.5 the decompiler reads it for you.

The Puzzle

Here is time() from a Hexagon DSP image, all three packets of it:

{
    call sys_time
    r16 = r0
    memd(sp + #-8+saved_r16_r17) = r17:16
    allocframe(#8)
}
{
    p0 = cmp.eq(r16, #0)
    r17:16 = memd(sp + #8+saved_r16_r17)
    if (!p0.new) memw(r16) = r0
}
{
    dealloc_return
}

Read top to bottom, it looks like the function calls sys_time first and then copies r0 into r16. Packet 2 seems to restore r16 and then store through it. So what ends up in memory, and where?

In fact, instructions in a packet normally see the registers as they were when the packet started, and their results only land when it ends. A call written first in its packet still goes last, meaning control reaches the callee only after the rest of the packet has run. So r16 = r0 copies the argument, not the result, and the restore of r16 doesn't affect the store next to it. The exception is p0.new: the .new suffix asks for a value produced in the same packet, here the comparison right above it.

So the function stores the result of sys_time through the pointer it was given, if that pointer isn't null. The decompiler applies the same rules and gets it right:

Reading a Packet the Way the Hardware Does

To get there, the decompiler lifts a packet as a whole rather than one instruction at a time:

  • branches and calls move to the end of the packet, which also puts a call's arguments before the call
  • a register that the packet both reads and overwrites is copied aside first, so reads see the old value unless they ask for the new one with .new
  • hardware loops, whose back edge is encoded in the packet's parse bits rather than in an instruction, come out as loops

Disassembler Improvements

The disassembler got a long list of improvements along the way. GP-relative operands are now resolved and more switch idioms are recognized. The processor options can be set in ida.cfg.

AVX and SSE: Intrinsics, Lifted

9.5 lifts AVX and AVX2, and SSE instructions such as movq, movlhps and punpcklqdq are lifted more precisely. Most instructions become the intrinsics you would write by hand. Bitwise operations, scalar min/max, FMA and plain loads and stores become ordinary C, which the decompiler can then simplify.

A few before/after pairs:

Intrinsics

// 9.4
void __fastcall q__mm256_add_pd(__int64 _RCX)
{
  __asm
  {
    vmovapd ymm0, ymmword ptr [rcx]
    vaddpd  ymm0, ymm0, ymmword ptr [rdx]
  }
}

// 9.5
__m256d __fastcall q__mm256_add_pd(__m256d *m1, __m256d *m2)
{
  return _mm256_add_pd(*m1, *m2);
}

The second argument and the return type are back as well.

Folds

Compilers zero a register by XORing it with itself, and flip or clear a sign bit with a mask. Because these are lifted as plain ^ and &, the decompiler simplifies them:

vpxor   xmm0, xmm0, xmm0                       
->  return 0.0;

vxorpd  xmm0, xmm0, xmmword ptr signmask_pd    
->  *(_QWORD *)&result = *(_QWORD *)&a ^ signmask_pd;

vandpd  xmm0, xmm0, xmmword ptr absmask_pd     
->  *(_QWORD *)&result = *(_QWORD *)&a & absmask_pd;

Easier Math

vmaxss  xmm0, xmm0, xmm1

vminss  xmm0, xmm0, xmm2     
->  return fminf(fmaxf(a1, a2), a3);


vfmadd132sd xmm0, xmm1, xmm2 
->  return a1 * a3 + a2;

Strings in Vector Loads

This one is in the disassembler. Compilers often copy short string literals 16 bytes at a time with movups, and IDA used to define each piece as an xmmword. Now it sees the string:

; 9.4
movups  xmm0, cs:xmmword_140224F2C

; 9.5
movups  xmm0, xmmword ptr cs:aErrorUnknownGl ; "ERROR: UNKNOWN GLFW ERROR"

Game Consoles: Saving (and Restoring) Your Progress

Console compilers often save and restore callee-saved registers through shared helpers. A function calls one in its prolog, or ends with a jump into one. The 9.5 decompiler recognizes these helpers on PS3, Xbox 360 and Wii U, by name or, when the binary is stripped, by their shape, and leaves them out of the pseudocode.

Sony PlayStation 3 (TM)

The Cell is a 64-bit PowerPC that runs code with 32-bit pointers, and the decompiler now understands that model. Pointers are 32-bit, registers stay 64-bit, and functions end with a proper return. TLS variables resolve as well, and string literals loaded through the TOC appear directly in the pseudocode. Also, DWARF places floating-point arguments in the right PowerPC registers, so functions taking float or double get correct prototypes.

Microsoft Xbox 360 (TM)

The tail of a function that jumps into a restore helper:

// 9.4
  sub_80062000(v4);
  JUMPOUT(0x80078B0C);

// 9.5
  sub_80062000(v4);
  return v6;

AltiVec and VMX128 instructions now become intrinsics named as in the Microsoft Xbox 360(TM) SDK, instead of __asm:

do
{
  v4 = __vmaddfp(*result++, *a2++, v4);
  --a3;
}
while ( a3 != 0 );

Cache, barrier and special-register instructions become intrinsics too, and .pdata entries are decoded with the Microsoft Xbox 360(TM) layout.

Nintendo Wii U (TM)

The Green Hills toolchain has its own helpers, some of which also allocate the stack frame. 9.5 folds them into the frame like any other prolog.

Sony PSP (TM)

The ELF loader now also handles PSP PRX relocations.

Arm, MIPS and ARC: Old Friends, New Tricks

Not everything in 9.5 is a new decompiler. Three architectures IDA has supported for years got a round of updates.

Armv9

The Arm disassembler now decodes a long list of recent extensions: SVE2.1 and SME2.1, the SME2 dot products and outer products, hinted conditional branches (FEAT_HBC), compare-and-branch (FEAT_CMPBR), lookup tables (FEAT_LUT), checked pointer arithmetic (FEAT_CPA), 64-byte loads and stores (FEAT_LS64), memory copy and set (FEAT_MOPS), and the BF16, FP16 and I16I64 variants of SVE and SME.

The newest addition is FEAT_PAuth_LR from Armv9.5, which Apple's latest kernels already use. Functions sign their return address with PACIBSPPC and return with RETABSPPC, which takes the function's own address as an operand. IDA creates these functions, finds where they end, and links the return back to the function:

_bzero
        PACIBSPPC
        STP             X29, X30, [SP,#-0x10+var_s0]!
        MOV             X29, SP
        ...
        MOV             SP, X29
        LDP             X29, X30, [SP+var_s0],#0x10
        RETABSPPC       _bzero

MIPS Release 6

Release 6 cleaned up the MIPS instruction set and reused the freed opcodes for new instructions. Compact branches and jumps have no delay slot, seleqz/selnez replace movz/movn, multiply and divide write a general register instead of HI/LO, and floating-point compares leave a mask in an FP register instead of setting a condition flag. IDA 9.5 adds decoding and decompilation support for MIPS32/64 Release 6:

Release 6 is detected from the ELF header. For raw firmware images, there is a "Release 6 instruction set" checkbox in the processor options.

ARC

The ARC decompiler now handles enter_s/leave_s, the multiply, divide and remainder instructions, setcc, min/max, and the bi/bih switch jumps. It also recognizes the MetaWare millicode helpers that save and restore registers, and recovers the arguments of variadic functions.

// 9.4
void __fastcall sub_10006D9C(unsigned __int16 *a1)
...
  if ( v5[2] == 0 && v7 > 0xF1 )
    __asm { leave_s {r13,blink,pcl} }
  __asm { leave_s {r13,blink,pcl} }
}

// 9.5
int __fastcall sub_10006D9C(_WORD *a1)
...
    if ( v1[2] != 0 || v3 <= 0xF1 )
      return 132 * (unsigned __int16)v3 + 13;
    else
      return 108 * (unsigned __int16)v3 + 5821;

Try It Out

Three new decompilers, and sharper output for the ones you already use. We hope you enjoy it.

IDA 9.5 is currently in beta, and the release will be available from the customer portal at https://my.hex-rays.com. We welcome your feedback on the beta. Please share it with our support team at https://get.support.hex-rays.com/ or on the community forum at https://community.hex-rays.com/.