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:
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.
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.
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 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.
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.
A few things worth knowing on day one:
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 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.
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).
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;
}
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.
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:
To get there, the decompiler lifts a packet as a whole rather than one instruction at a time:
.newThe 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.
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:
// 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.
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;
vmaxss xmm0, xmm0, xmm1
vminss xmm0, xmm0, xmm2
-> return fminf(fmaxf(a1, a2), a3);
vfmadd132sd xmm0, xmm1, xmm2
-> return a1 * a3 + a2;
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"
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.
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.
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.
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.
The ELF loader now also handles PSP PRX relocations.
Not everything in 9.5 is a new decompiler. Three architectures IDA has supported for years got a round of updates.
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
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.
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;
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/.