The x86 decompiler plugin incorrectly NOPs necessary assembly instructions, leading to incorrect C pseudocode generation

.386
.model flat, C
.code

; Call the existing exported Hello (naked) implemented in C++.
; The symbol is _Hello for C linkage.
extrn Hello_impl:PROC

PUBLIC _Hello2
_Hello2 PROC
    ; Caller passes x->EAX, y->EBX, psz->ECX
    ; Push arguments for _Hello_impl (right-to-left): psz, y, x
    push ecx
    push edx
    push eax
    call Hello_impl
    ; move return (EAX) into ECX as required by new convention
    mov ecx, eax
    ; clean up stack (3 dwords)
    add esp, 12
    ret
_Hello2 ENDP

END

If you compile the above code into a DLL, you will see mostly correct C pseudocode:

int __usercall _Hello2@<eax>(char *pszCaption@<ecx>, int y@<edx>, int a3@<eax>)
{
  return Hello_impl(a3, y, pszCaption);
}

Despite a minor error (its original intention was to use ecx as the return value), the logic is largely preserved. Here is its microcode:

; Maturity: MMAT_GENERATED
int __usercall _Hello2@<eax>(char *pszCaption@<ecx>, int y@<edx>, int@<eax>)

0.BLT_NONE
; STKD=C MINREF=0/END=C ARGS: OFF=10/MINREF=10/END=110/SHADOW=0
; ????-BLOCK 0 PROP FAKE [START=100010C4 END=100010C4] MINREFS: STK=0/ARG=10, MAXBSP: 0
; ---------------------------------------------------------------------------

1.BLT_NONE
; ????-BLOCK 1 PROP PUSH [START=100010C4 END=100010CC] MINREFS: STK=0/ARG=10, MAXBSP: C
; USE: rax.8,ecx.4,esp.4,cs.2,(ebx.4,ebp.4,rdi.8,st0.8,st1.8,st2.8,st3.8,st4.8,st5.8,st6.8,st7.8,mm0.8,mm1.8,mm2.8,mm3.8,mm4.8,mm5.8,mm6.8,mm7.8,xmm0.16,xmm1.16,xmm2.16,xmm3.16,xmm4.16,xmm5.16,xmm6.16,xmm7.16,xmm8.16,xmm9.16,xmm10.16,xmm11.16,xmm12.16,xmm13.16,xmm14.16,xmm15.16,GLBLOW,LVARS,ARGS,GLBHIGH)
; DEF: esp.4,LVARS,(cf.1,zf.1,sf.1,of.1,pf.1,rax.8,ecx.4,fps.2,st7.8,fl.1,c0.1,c2.1,c3.1,df.1,if.1,xmm0.16,GLBLOW,RET,ARGS,GLBHIGH)
; DNU: esp.4
1. 1            nop            ; 100010C4 u=          
1. 2            push   ecx.4   ; 100010C4 u=ecx.4,esp.4 d=esp.4,sp+8..
1. 3            nop            ; 100010C5 u=          
1. 4            push   edx.4   ; 100010C5 u=edx.4,esp.4 d=esp.4,sp+4.4
1. 5            nop            ; 100010C6 u=          
1. 6            push   eax.4   ; 100010C6 u=eax.4,esp.4 d=esp.4,sp+0.4
1. 7            mov    #0x100010CC.4, ett.4 ; 100010C7 u=           d=ett.4
1. 8            nop            ; 100010C7 u=          
1. 9            nop            ; 100010C7 u=          
1.10            mov    cs.2, seg.2 ; 100010C7 u=cs.2       d=seg.2
1.11            mov    #0x10001010.4, eoff.4 ; 100010C7 u=           d=eoff.4
1.12            call   $_Hello_impl ; 100010C7 u=(rax.8,rcx.8,ebp.4,rdi.8,st0.8,st1.8,st2.8,st3.8,st4.8,st5.8,st6.8,st7.8,mm0.8,mm1.8,mm2.8,mm3.8,mm4.8,mm5.8,mm6.8,mm7.8,xmm0.16,xmm1.16,xmm2.16,xmm3.16,xmm4.16,xmm5.16,xmm6.16,xmm7.16,xmm8.16,xmm9.16,xmm10.16,xmm11.16,xmm12.16,xmm13.16,xmm14.16,xmm15.16,GLBLOW,LVARS,ARGS,GLBHIGH) d=(cf.1,zf.1,sf.1,of.1,pf.1,rax.8,ecx.4,fps.2,st7.8,fl.1,c0.1,c2.1,c3.1,df.1,if.1,xmm0.16,ALLMEM)
; ---------------------------------------------------------------------------

2.BLT_NONE
; ????-BLOCK 2 PROP PUSH [START=100010CC END=100010D2] MINREFS: STK=0/ARG=10, MAXBSP: C
; USE: eax.4,esp.4,cs.2,RET
; DEF: cf.1,zf.1,sf.1,of.1,pf.1,ecx.4,esp.4
; DNU: cf.1,zf.1,sf.1,of.1,pf.1,ecx.4,esp.4
2. 1            mov    eax.4, ecx.4 ; 100010CC u=eax.4      d=ecx.4
2. 2            mov    #0xC.4, et1.4 ; 100010CE u=           d=et1.4
2. 3            cfadd  et1.4, esp.4, cf.1 ; 100010CE u=esp.4,et1.4 d=cf.1
2. 4            ofadd  et1.4, esp.4, of.1 ; 100010CE u=esp.4,et1.4 d=of.1
2. 5            add    et1.4, esp.4, ett.4 ; 100010CE u=esp.4,et1.4 d=ett.4
2. 6            setz   ett.4, #0.4, zf.1 ; 100010CE u=ett.4      d=zf.1
2. 7            setp   ett.4, #0.4, pf.1 ; 100010CE u=ett.4      d=pf.1
2. 8            sets   ett.4, sf.1 ; 100010CE u=ett.4      d=sf.1
2. 9            mov    ett.4, esp.4 ; 100010CE u=ett.4      d=esp.4
2.10            nop            ; 100010D1 u=          
2.11            pop    eoff.4  ; 100010D1 u=esp.4,RET  d=esp.4,eoff.4
2.12            mov    cs.2, seg.2 ; 100010D1 u=cs.2       d=seg.2
2.13            ijmp   seg.2, eoff.4 ; 100010D1 u=eoff.4,seg.2
; ---------------------------------------------------------------------------

3.BLT_STOP

As you can see, push ecx.4 and push edx.4 are preserved perfectly. However, if we replace push edx with push ebx in the original assembly, the compiled DLL can no longer be decompiled correctly by IDA. Here is its microcode:

; Maturity: MMAT_GENERATED
int __usercall _Hello2@<eax>(int@<eax>)

0.BLT_NONE
; STKD=4 MINREF=0/END=C ARGS: OFF=10/MINREF=10/END=110/SHADOW=0
; SAVEDREGS: ebx.4
; ????-BLOCK 0 PROP FAKE [START=100010C4 END=100010C4] MINREFS: STK=0/ARG=10, MAXBSP: 0
; ---------------------------------------------------------------------------

1.BLT_NONE
; ????-BLOCK 1 PROP PUSH [START=100010C4 END=100010CC] MINREFS: STK=0/ARG=10, MAXBSP: 4
; USE: eax.4,esp.4,cs.2,(edx.4,rcx.8,ebp.4,rdi.8,st0.8,st1.8,st2.8,st3.8,st4.8,st5.8,st6.8,st7.8,mm0.8,mm1.8,mm2.8,mm3.8,mm4.8,mm5.8,mm6.8,mm7.8,xmm0.16,xmm1.16,xmm2.16,xmm3.16,xmm4.16,xmm5.16,xmm6.16,xmm7.16,xmm8.16,xmm9.16,xmm10.16,xmm11.16,xmm12.16,xmm13.16,xmm14.16,xmm15.16,GLBLOW,LVARS,ARGS,GLBHIGH)
; DEF: esp.4,sp+0.4,(cf.1,zf.1,sf.1,of.1,pf.1,rax.8,ecx.4,fps.2,st7.8,fl.1,c0.1,c2.1,c3.1,df.1,if.1,xmm0.16,GLBLOW,sp+4..,RET,ARGS,GLBHIGH)
; DNU: esp.4
1. 1            nop            ; 100010C6 u=          
1. 2            push   eax.4   ; 100010C6 u=eax.4,esp.4 d=esp.4,sp+0.4
1. 3            mov    #0x100010CC.4, ett.4 ; 100010C7 u=           d=ett.4
1. 4            nop            ; 100010C7 u=          
1. 5            nop            ; 100010C7 u=          
1. 6            mov    cs.2, seg.2 ; 100010C7 u=cs.2       d=seg.2
1. 7            mov    #0x10001010.4, eoff.4 ; 100010C7 u=           d=eoff.4
1. 8            call   $_Hello_impl ; 100010C7 u=(rax.8,rcx.8,ebp.4,rdi.8,st0.8,st1.8,st2.8,st3.8,st4.8,st5.8,st6.8,st7.8,mm0.8,mm1.8,mm2.8,mm3.8,mm4.8,mm5.8,mm6.8,mm7.8,xmm0.16,xmm1.16,xmm2.16,xmm3.16,xmm4.16,xmm5.16,xmm6.16,xmm7.16,xmm8.16,xmm9.16,xmm10.16,xmm11.16,xmm12.16,xmm13.16,xmm14.16,xmm15.16,GLBLOW,LVARS,ARGS,GLBHIGH) d=(cf.1,zf.1,sf.1,of.1,pf.1,rax.8,ecx.4,fps.2,st7.8,fl.1,c0.1,c2.1,c3.1,df.1,if.1,xmm0.16,ALLMEM)
; ---------------------------------------------------------------------------

2.BLT_NONE
; ????-BLOCK 2 PROP PUSH [START=100010CC END=100010D2] MINREFS: STK=0/ARG=10, MAXBSP: C
; USE: eax.4,esp.4,cs.2,RET
; DEF: cf.1,zf.1,sf.1,of.1,pf.1,ecx.4,esp.4
; DNU: cf.1,zf.1,sf.1,of.1,pf.1,ecx.4,esp.4
2. 1            mov    eax.4, ecx.4 ; 100010CC u=eax.4      d=ecx.4
2. 2            mov    #0xC.4, et1.4 ; 100010CE u=           d=et1.4
2. 3            cfadd  et1.4, esp.4, cf.1 ; 100010CE u=esp.4,et1.4 d=cf.1
2. 4            ofadd  et1.4, esp.4, of.1 ; 100010CE u=esp.4,et1.4 d=of.1
2. 5            add    et1.4, esp.4, ett.4 ; 100010CE u=esp.4,et1.4 d=ett.4
2. 6            setz   ett.4, #0.4, zf.1 ; 100010CE u=ett.4      d=zf.1
2. 7            setp   ett.4, #0.4, pf.1 ; 100010CE u=ett.4      d=pf.1
2. 8            sets   ett.4, sf.1 ; 100010CE u=ett.4      d=sf.1
2. 9            mov    ett.4, esp.4 ; 100010CE u=ett.4      d=esp.4
2.10            nop            ; 100010D1 u=          
2.11            pop    eoff.4  ; 100010D1 u=esp.4,RET  d=esp.4,eoff.4
2.12            mov    cs.2, seg.2 ; 100010D1 u=cs.2       d=seg.2
2.13            ijmp   seg.2, eoff.4 ; 100010D1 u=eoff.4,seg.2
; ---------------------------------------------------------------------------

3.BLT_STOP

As you can observe, the expected push ecx.4 and push ebx.4 instruction sequence is missing entirely. They seem to have been erroneously optimized away, leaving only SAVEDREGS: ebx.4 in the comments.

This incorrect microcode optimization directly leads to faulty C pseudocode generation:

int __usercall _Hello2@<eax>(int a1@<eax>)
{
  int v3; // [esp+0h] [ebp-8h]
  const char *v4; // [esp+4h] [ebp-4h]

  return Hello_impl(a1, v3, v4);
}

Even if we forcefully correct the function prototype to: int __usercall __spoils<eax,ecx> _Hello2@<eax>(int a1@<eax>, int a2@<ebx>, int a3@<ecx>);
The C pseudocode merely turns into:

int __usercall __spoils<eax,ecx> _Hello2@<eax>(int a1@<eax>, int a2@<ebx>, int a3@<ecx>)
{
  int v4; // [esp+0h] [ebp-8h]
  const char *v5; // [esp+4h] [ebp-4h]

  return Hello_impl(a1, v4, v5);
}

Variables v4 and v5 still trigger warnings:

100010C7: variable 'v4' is possibly undefined
100010C7: variable 'v5' is possibly undefined

There is no way to automatically resolve these undefined variables. However, in IDA 6.1, the generated pseudocode was perfectly correct, as shown below:

int __usercall _Hello2<eax>(const char *pszCaption<ecx>, int a2<eax>, int a3<ebx>)
{
  return Hello_impl(a2, a3, pszCaption);
}

Therefore, I strongly suspect this is a regression (a bug) introduced in newer versions of the IDA decompiler. This bug creates a severe issue where it is impossible to generate correct C pseudocode for inline assembly or standalone assembly files. I earnestly request the Hex-Rays team to look into this.


A Second Critical Issue: Missing __spoils with LTCG

I recently discovered another serious issue. For x86 files compiled with Link-Time Code Generation (/LTCG or /GL), there is a vast amount of direct cross-function register reuse. However, IDA does not seem to generate the __spoils (spoiled register list) for these functions. I am unsure if this is a bug in the data flow analysis phase or if the attribute simply isn’t being set, but the impact is massive.

This results in a flood of variable 'xx' is possibly undefined errors (the VALUE MAY BE UNDEFINED; hover warnings). This severely degrades the quality of the generated C pseudocode, making it extremely difficult to recompile. Unless every single one of these VALUE MAY BE UNDEFINED; warnings is resolved, treating the C pseudocode as compilable source code is impossible.

Conversely, if this issue were fixed, we could generate accurate PDBs, debug via Visual Studio, and perform global cross-function code analysis much more easily. From there, it would just be a matter of using AI to semantically rename functions, variables, and structs.

Of course, this entirely depends on IDA’s data flow and control flow analysis being flawless—accurately determining which registers and stack segments a function uses and spoils, avoiding the generation of incorrect if/else/while/for/switch blocks, and properly supporting SEH (__try/__except) and C++ try/catch restoration (issues I have reported in the past).


Final Thoughts (Feedback)

To be completely honest, I sometimes struggle to understand why Hex-Rays doesn’t dedicate its efforts to perfecting the disassembly and decompilation of a core architecture (like x86/x64) before rushing to expand support to numerous new architectures (like MIPS, V850) and languages (Rust, Go). Is this purely to catch market hype and open new revenue streams?

While I can understand this from a business perspective, competitors like Binary Ninja, Ghidra, and JEB are aggressively catching up. With the explosive rise of AI-assisted development, I genuinely worry that IDA’s 35-year “moat” of decompilation supremacy might be breached.

I truly want IDA to keep striving for perfection so it can help more reverse engineers. My apologies for the slight rant—it’s just that whenever I use IDA deeply, I run into various issues. It leaves me a bit baffled since IDA is widely regarded as the absolute “Swiss Army Knife” of native C/C++ decompilation.

Thank you for your time and for reading my report.