Latest available version: IDA and decompilers v8.4.240320 see all releases
Hex-Rays logo State-of-the-art binary code analysis tools
email icon

Comparisons of ARM disassembly and decompilation

Here are some side-by-side comparisons of disassembly and decompiler for ARM. Please maximize the window too see both columns simultaneously.

The following examples are displayed on this page:

  1. Simple case
  2. 64-bit arithmetics
  3. Conditional instructions
  4. Conditional instructions - 2
  5. Complex instructions
  6. Compiler helper functions
  7. Immediate constants
  8. Position independent code

Simple case

Let's start with a very simple function. It accepts a pointer to a structure and zeroes out its first three fields. While the function logic is obvious by just looking at the decompiler output, the assembly listing has too much noise and requires studying it.

The decompiler saves your time and allows you to concentrate on more exciting aspects of reverse engineering.

Assembler code
; struct_result *__fastcall sub_210DC(struct_result *result) var_10 = -0x10 var_4 = -4 MOV R12, SP STMFD SP!, {R0} STMFD SP!, {R12,LR} SUB SP, SP, #4 LDR R2, [SP,#0x10+var_4] MOV R3, #0 STR R3, [R2] LDR R3, [SP,#0x10+var_4] ADD R2, R3, #4 MOV R3, #0 STR R3, [R2] LDR R3, [SP,#0x10+var_4] ADD R2, R3, #8 MOV R3, #0 STR R3, [R2] LDR R3, [SP,#0x10+var_4] STR R3, [SP,#0x10+var_10] LDR R0, [SP,#0x10+var_10] ADD SP, SP, #4 LDMFD SP, {SP,LR} BX LR ; End of function sub_210DC
Pseudocode
struct_result *__fastcall sub_210DC(struct_result *result) { result->dword0 = 0; result->dword4 = 0; result->dword8 = 0; return result; }

64-bit arithmetics

Sorry for a long code snippet, ARM code tends to be longer compared to x86 code. This makes our comparison even more impressive: look at how concise is the decompiler output!

Assembler code
; bool __cdecl uh_gt_uc() EXPORT _uh_gt_uc__YA_NXZ _uh_gt_uc__YA_NXZ ; DATA XREF: .pdata:$T7452o var_2C = -0x2C var_28 = -0x28 var_24 = -0x24 var_20 = -0x20 var_1C = -0x1C var_18 = -0x18 var_14 = -0x14 var_10 = -0x10 var_C = -0xC var_8 = -8 var_4 = -4 STR LR, [SP,#var_4]! ; $M7441 ; $LN8@uh_gt_uc SUB SP, SP, #0x28 $M7449 BL uh STR R1, [SP,#0x2C+var_24] STR R0, [SP,#0x2C+var_28] BL uc STRB R0, [SP,#0x2C+var_20] LDRB R3, [SP,#0x2C+var_20] STR R3, [SP,#0x2C+var_1C] LDR R1, [SP,#0x2C+var_1C] LDR R3, [SP,#0x2C+var_1C] MOV R2, R3,ASR#31 LDR R3, [SP,#0x2C+var_28] STR R3, [SP,#0x2C+var_18] LDR R3, [SP,#0x2C+var_24] STR R3, [SP,#0x2C+var_14] LDR R3, [SP,#0x2C+var_18] STR R3, [SP,#0x2C+var_10] STR R1, [SP,#0x2C+var_C] LDR R3, [SP,#0x2C+var_14] CMP R3, R2 BCC $LN3_8 loc_6AC BHI $LN5_0 loc_6B0 LDR R2, [SP,#0x2C+var_10] LDR R3, [SP,#0x2C+var_C] CMP R2, R3 BLS $LN3_8 $LN5_0 MOV R3, #1 STR R3, [SP,#0x2C+var_8] B $LN4_8 ; --------------------------------------------------------------------------- $LN3_8 ; uh_gt_uc(void)+68j MOV R3, #0 STR R3, [SP,#0x2C+var_8] $LN4_8 LDR R3, [SP,#0x2C+var_8] AND R3, R3, #0xFF STRB R3, [SP,#0x2C+var_2C] LDRB R0, [SP,#0x2C+var_2C] ADD SP, SP, #0x28 LDR PC, [SP+4+var_4],#4 ; End of function uh_gt_uc(void)
Pseudocode
bool __fastcall uh_gt_uc() { unsigned __int64 v0; // ST04_8@1 v0 = uh(); return v0 > uc(); }

Conditional instructions

The ARM processor has conditional instructions that can shorten the code but require high attention from the reader. The case above is very simple, just note that there is a pair of instructions: MOVNE and LDREQSH. Only one of them will be executed at once. This is how simple if-then-else looks in ARM.

The pseudocode shows it much better and does not require any explanations.

A quiz question: did you notice that MOVNE loads zero to R0? (because I didn't:)

Also note that in the disassembly listing we see var_8 but the location really used is var_A, which corresponds to v4.

Assembler code
; int __cdecl ReadShort(void *, unsigned __int32 offset, int whence) ReadShort whence = -0x18 var_A = -0xA var_8 = -8 STMFD SP!, {R4,LR} SUB SP, SP, #0x10 ; whence MOV R4, #0 ADD R3, SP, #0x18+var_8 STRH R4, [R3,#-2]! STR R2, [SP,#0x18+whence] ; whence MOV R2, R3 ; buffer MOV R3, #2 ; len BL ReadData CMP R0, R4 MOVNE R0, R4 LDREQSH R0, [SP,#0x18+var_A] ADD SP, SP, #0x10 LDMFD SP!, {R4,PC} ; End of function ReadShort
Pseudocode
int __cdecl ReadShort(void *a1, unsigned __int32 offset, int whence) { int result; // r0@2 __int16 v4; // [sp+Eh] [bp-Ah]@1 v4 = 0; if ( ReadData(a1, offset, &v4, 2u, whence) ) result = 0; else result = v4; return result; }

Conditional instructions - 2

Look, the decompiler output is longer! This is a rare case when the pseudocode is longer than the disassembly listing, but it is a for a good cause: to keep it readable. There are so many conditional instructions here, it is very easy to misunderstand the dependencies. For example, did you notice that the first MOVEQ may use the condition codes set by CMP? The subtle detail is that CMPNE may be skipped and the condition codes set by CMP may reach MOVEQs.

The decompiler represented it perfectly well. I renamed some variables and set their types, but this was an easy task.
Assembler code
; signed int __fastcall get_next_byte(entry_t *entry) get_next_byte ; DATA XREF: sub_3BC+30o ; LDR R2, [R0,#4] CMP R2, #0 LDRNE R3, [R0] LDRNEB R1, [R3],#1 CMPNE R1, #0 MOVEQ R1, #1 STREQ R1, [R0,#0xC] MOVEQ R0, 0xFFFFFFFF MOVEQ PC, LR SUB R2, R2, #1 STR R2, [R0,#4] STR R3, [R0] MOV R0, R1 RET ; End of function get_next_byte
Pseudocode
signed int __fastcall get_next_byte(entry_t *entry) { signed int chr; // r1@0 unsigned __int8 *ptr; // r3@0 int count; // r2@1 char done; // zf@1 signed int result; // r0@4 count = entry->count; done = count == 0; if ( count ) { ptr = entry->ptr + 1; chr = *entry->ptr; done = chr == 0; } if ( done ) { entry->done = 1; result = -1; } else { entry->count = count - 1; entry->ptr = ptr; result = chr; } return result; }

Complex instructions

Conditional instructions are just part of the story. ARM is also famous for having a plethora of data movement instructions. They come with a set of possible suffixes that subtly change the meaning of the instruction. Take STMCSIA, for example. It is a STM instruction, but then you have to remember that CS means "carry set" and IA means "increment after".

In short, the disassembly listing is like Chinese. The pseudocode is longer but requires much less time to understand.

Assembler code
; void __fastcall sub_2A38(list_t *ptr, unsigned int a2) sub_2A38 ; CODE XREF: sub_5C8+48p ; sub_648+5Cp ... MOV R2, #0 STMFD SP!, {LR} MOV R3, R2 MOV R12, R2 MOV LR, R2 SUBS R1, R1, #0x20 loc_2A50 ; CODE XREF: sub_2A38+24j STMCSIA R0!, {R2,R3,R12,LR} STMCSIA R0!, {R2,R3,R12,LR} SUBCSS R1, R1, #0x20 BCS loc_2A50 MOVS R1, R1,LSL#28 STMCSIA R0!, {R2,R3,R12,LR} STMMIIA R0!, {R2,R3} LDMFD SP!, {LR} MOVS R1, R1,LSL#2 STRCS R2, [R0],#4 MOVEQ PC, LR STRMIH R2, [R0],#2 TST R1, #0x40000000 STRNEB R2, [R0],#1 RET ; End of function sub_2A38
Pseudocode
void __fastcall sub_2A38(list_t *ptr, unsigned int a2) { char copybig; // cf@1 unsigned int size; // r1@1 list_t *v4; // r0@3 int remains; // r1@4 int final; // r1@8 copybig = a2 >= 0x20; size = a2 - 32; do { if ( !copybig ) break; ptr->dword0 = 0; ptr->dword4 = 0; ptr->dword8 = 0; ptr->dwordC = 0; v4 = ptr + 1; v4->dword0 = 0; v4->dword4 = 0; v4->dword8 = 0; v4->dwordC = 0; ptr = v4 + 1; copybig = size >= 0x20; size -= 32; } while ( copybig ); remains = size << 28; if ( copybig ) { ptr->dword0 = 0; ptr->dword4 = 0; ptr->dword8 = 0; ptr->dwordC = 0; ++ptr; } if ( remains < 0 ) { ptr->dword0 = 0; ptr->dword4 = 0; ptr = (list_t *)((char *)ptr + 8); } final = 4 * remains; if ( copybig ) { ptr->dword0 = 0; ptr = (list_t *)((char *)ptr + 4); } if ( final ) { if ( final < 0 ) { LOWORD(ptr->dword0) = 0; ptr = (list_t *)((char *)ptr + 2); } if ( final & 0x40000000 ) LOBYTE(ptr->dword0) = 0; } }

Compiler helper functions

Sorry for another long code snippet. Just wanted to show you that the decompiler can handle compiler helper functions (like __divdi3) and handles 64-bit arithmetic quite well.

Assembler code
EXPORT op_two64 op_two64 ; CODE XREF: refer_all+31Cp ; main+78p anonymous_1 = -0x28 var_20 = -0x20 anonymous_0 = -0x18 var_10 = -0x10 arg_0 = 4 000 MOV R12, SP 000 STMFD SP!, {R4,R11,R12,LR,PC} 014 SUB R11, R12, #4 014 SUB SP, SP, #0x18 02C SUB R4, R11, #-var_10 02C STMDB R4, {R0,R1} 02C MOV R1, 0xFFFFFFF0 02C SUB R12, R11, #-var_10 02C ADD R1, R12, R1 02C STMIA R1, {R2,R3} 02C LDR R3, [R11,#arg_0] 02C CMP R3, #1 02C BNE loc_9C44 02C MOV R3, 0xFFFFFFF0 02C SUB R0, R11, #-var_10 02C ADD R3, R0, R3 02C SUB R4, R11, #-var_10 02C LDMDB R4, {R1,R2} 02C LDMIA R3, {R3,R4} 02C ADDS R3, R3, R1 02C ADC R4, R4, R2 02C SUB R12, R11, #-var_20 02C STMDB R12, {R3,R4} 02C B loc_9D04 ; --------------------------------------------------------------------------- loc_9C44 ; CODE XREF: op_two64+30j 02C LDR R3, [R11,#arg_0] 02C CMP R3, #2 02C BNE loc_9C7C 02C MOV R3, 0xFFFFFFF0 02C SUB R0, R11, #-var_10 02C ADD R3, R0, R3 02C SUB R4, R11, #-var_10 02C LDMDB R4, {R1,R2} 02C LDMIA R3, {R3,R4} 02C SUBS R3, R1, R3 02C SBC R4, R2, R4 02C SUB R12, R11, #-var_20 02C STMDB R12, {R3,R4} 02C B loc_9D04 ; --------------------------------------------------------------------------- loc_9C7C ; CODE XREF: op_two64+68j 02C LDR R3, [R11,#arg_0] 02C CMP R3, #3 02C BNE loc_9CB8 02C MOV R3, 0xFFFFFFF0 02C SUB R0, R11, #-var_10 02C ADD R3, R0, R3 02C SUB R2, R11, #-var_10 02C LDMDB R2, {R0,R1} 02C LDMIA R3, {R2,R3} 02C BL __muldi3 02C MOV R4, R1 02C MOV R3, R0 02C SUB R12, R11, #-var_20 02C STMDB R12, {R3,R4} 02C B loc_9D04 ; --------------------------------------------------------------------------- loc_9CB8 ; CODE XREF: op_two64+A0j 02C LDR R3, [R11,#arg_0] 02C CMP R3, #4 02C BNE loc_9CF4 02C MOV R3, 0xFFFFFFF0 02C SUB R0, R11, #-var_10 02C ADD R3, R0, R3 02C SUB R2, R11, #-var_10 02C LDMDB R2, {R0,R1} 02C LDMIA R3, {R2,R3} 02C BL __divdi3 02C MOV R4, R1 02C MOV R3, R0 02C SUB R12, R11, #-var_20 02C STMDB R12, {R3,R4} 02C B loc_9D04 ; --------------------------------------------------------------------------- loc_9CF4 ; CODE XREF: op_two64+DCj 02C MOV R3, 0xFFFFFFFF 02C MOV R2, 0xFFFFFFFF 02C SUB R4, R11, #-var_20 02C STMDB R4, {R2,R3} loc_9D04 ; CODE XREF: op_two64+5Cj ; op_two64+94j ... 02C SUB R12, R11, #-var_20 02C LDMDB R12, {R0,R1} 02C SUB SP, R11, #0x10 014 LDMFD SP, {R4,R11,SP,PC} ; End of function op_two64
Pseudocode
signed __int64 __fastcall op_two64(signed __int64 a1, signed __int64 a2, int a3) { signed __int64 v4; // [sp+0h] [bp-28h]@2 switch ( a3 ) { case 1: v4 = a2 + a1; break; case 2: v4 = a1 - a2; break; case 3: v4 = a1 * a2; break; case 4: v4 = a1 / a2; break; default: v4 = -1LL; break; } return v4; }

Immediate constants

Since ARM instructions cannot have big immediate constants, sometimes they are loaded with two instructions. There are many 0xFA (250 decimal) constants in the disassembly listing, but all of them are shifted to the left by 2 before use. The decompiler saves you from these petty details.

Also a side: the decompiler can handle ARM mode as well as Thumb mode instructions. It just does not care about the instruction encoding because it is already handled by IDA.

Assembler code
loc_110D6 ; CODE XREF: sub_10E38+43Cj ; sub_10E38+442j ... LDR R1, =(tmin_ptr - 0x1CDB8) LDR R2, =(tmax_ptr - 0x1CDB8) LDR R0, =(aRttMinAvgMaxMd - 0x1CDB8) LDR R6, [R7,R1] LDR R5, [R7,R2] MOVS R3, #0xFA LDR R4, [R6] LSLS R1, R3, #2 LDR R6, [R5] ADDS R5, R7, R0 ; "rtt min/avg/max/mdev = %ld.%03ld/%lu.%0"... MOVS R0, R4 BLX __aeabi_idiv MOV R8, R0 MOVS R0, R4 MOVS R4, #0xFA LSLS R1, R4, #2 BLX __aeabi_idivmod LDR R3, =0 LDR R2, =0x3E8 MOVS R4, R1 LDR R0, [SP,#0x78+var_40] LDR R1, [SP,#0x78+var_40+4] BLX __aeabi_ldivmod LDR R3, =0 LDR R2, =0x3E8 STR R0, [SP,#0x78+var_50] STR R1, [SP,#0x78+var_4C] LDR R0, [SP,#0x78+var_40] LDR R1, [SP,#0x78+var_40+4] BLX __aeabi_ldivmod MOVS R1, #0xFA MOVS R0, R6 LSLS R1, R1, #2 STR R2, [SP,#0x78+var_78] BLX __aeabi_idiv STR R0, [SP,#0x78+var_74] MOVS R0, R6 MOVS R6, #0xFA LSLS R1, R6, #2 BLX __aeabi_idivmod MOVS R2, #0xFA STR R1, [SP,#0x78+var_70] LDR R0, [SP,#0x78+var_38] LSLS R1, R2, #2 BLX __aeabi_idiv MOVS R3, #0xFA STR R0, [SP,#0x78+var_6C] LSLS R1, R3, #2 LDR R0, [SP,#0x78+var_38] BLX __aeabi_idivmod MOVS R0, R5 ; format STR R1, [SP,#0x78+var_68] MOVS R2, R4 MOV R1, R8 LDR R3, [SP,#0x78+var_50] BLX printf
Pseudocode
printf( "rtt min/avg/max/mdev = %ld.%03ld/%lu.%03ld/%ld.%03ld/%ld.%03ld ms", tmin / 1000, tmin % 1000, v27 / 1000, v27 % 1000, tmax / 1000, tmax % 1000, v28 / 1000, v28 % 1000);

Position independent code

In some case the disassembly listing can be misleading, especially with PIC (position independent code). While the address of a constant string is loaded into R12, the code does not care about it. It is just how variable addresses are calculated in PIC-code (it is .got-someoffset). Such calculations are very frequent in shared objects and unfortunately IDA cannot handle all of them. But the decompiler did a great job of tracing R12.

Assembler code
sub_65768 ; DATA XREF: .data:007E37A4o var_18 = -0x18 var_14 = -0x14 var_10 = -0x10 arg_0 = 0 PUSH {LR} LDR.W R12, =aResponsetype ; "responseType" SUB SP, SP, #0x14 ADR.W LR, loc_65774 loc_65774 ; DATA XREF: sub_65768+8o ADD R12, LR LDR.W LR, [SP,#0x18+arg_0] STR.W LR, [SP,#0x18+var_18] MOV.W LR, #0x10 STR.W LR, [SP,#0x18+var_14] LDR.W LR, =0xFFF0883C ADD R12, LR STR.W R12, [SP,#0x18+var_10] BL sub_65378 ADD SP, SP, #0x14 POP {PC} ; End of function sub_65768
Pseudocode
int __fastcall sub_65768(int a1, int a2, int a3, int a4, int a5) { return sub_65378(a1, a2, a3, a4, a5, 16, (int)myarray); }