Explanation of multiple INTERR codes

Hey,

I’m working on an AVX lifter and constantly run into issues with hexrays probably stumbling over my emitted microcode.

Concrete example from a small program I wrote to test AVX coverage in my lifter:

; =============== S U B R O U T I N E =======================================


; __int64 test_horizontal(void)
                public test_horizontal
test_horizontal proc near               ; CODE XREF: main+32↑p
; __unwind {
                endbr64
                vxorps  xmm0, xmm0, xmm0
                mov     edi, 2
                mov     eax, 1
                lea     rsi, aHorizontalTest ; "Horizontal test: %f\n"
                vcvtss2sd xmm0, xmm0, dword ptr cs:ymmword_2260
                jmp     ___printf_chk
; } // starts at 1280
test_horizontal endp

The following code of the lifter is responsible for vcvtss2sd instructions:

merror_t lift_conversion(codegen_t &cdg)
{
    TRACE_FUNC;
    // Case: vcvtsi2ss xmm1, xmm2, r/m32 OR r/m64
    // ...

    // Case: vcvtss2sd xmm1, xmm2, xmm3/m32
    if (cdg.insn.itype == NN_vcvtss2sd)
    {
        mreg_t src_vec1 = load_operand_safe(cdg, 1, SZ_XMM); // Op2 (xmm2) - Pass through
        mreg_t src_vec2 = load_operand_safe(cdg, 2, SZ_XMM); // Op3 (xmm3/mem) - Converted
        mreg_t dst      = reg2mreg(cdg.insn.Op1.reg);

        if (src_vec1 == mr_none || src_vec2 == mr_none || dst == mr_none) return MERR_INSN;

        tinfo_t vec_type_s = get_vector_type(SZ_XMM, false, false); // __m128
        tinfo_t vec_type_d = get_vector_type(SZ_XMM, false, true);  // __m128d

        IntrinsicBuilder ib(cdg, "_mm_cvtss_sd");
        ib.add_arg_reg(src_vec1, vec_type_d); // First arg is __m128d (pass-through upper)
        ib.add_arg_reg(src_vec2, vec_type_s); // Second arg is __m128 (source scalar)
        ib.set_return_type(vec_type_d);
        if (ib.emit(dst) == mr_none) return MERR_INSN;
        return MERR_OK;
    }

    return MERR_INSN;
}

Annoyingly, even with lots of debugging I keep stumbling over the same error:

[AVX] Match found for itype 1114 at 1284
[AVX] >> Enter apply (EA: 1284)
[AVX] >> Enter lift_logic (EA: 1284)
[AVX] >> Enter load_operand_safe (EA: 1284)
[AVX] [TRC] 1284: Loading Op 1: Type=1, Dtype=8
[AVX] [TRC] 1284:   -> Register 64 mapped to mreg 480
[AVX] << Exit load_operand_safe (EA: 1284)
[AVX] >> Enter load_operand_safe (EA: 1284)
[AVX] [TRC] 1284: Loading Op 2: Type=1, Dtype=8
[AVX] [TRC] 1284:   -> Register 64 mapped to mreg 480
[AVX] << Exit load_operand_safe (EA: 1284)
[AVX] Resolving type: __m128
[AVX] Found named type: __m128 (Size: 16)
[AVX] [TRC] 1284: IntrinsicBuilder ctor: _mm_xor_ps
[AVX] [TRC] 1284: Adding arg reg: 480
[AVX] [TYPE] Arg Type: __m128 (Size: 16, Empty: 0)
[AVX] [TRC] 1284: Adding arg reg: 480
[AVX] [TYPE] Arg Type: __m128 (Size: 16, Empty: 0)
[AVX] [TYPE] Return Type: __m128 (Size: 16, Empty: 0)
[AVX] >> Enter emit (EA: 1284)
[AVX] [TRC] 1284: Constructing m_call instruction...
[AVX] [TRC] 1284: Constructing m_mov instruction to dst 480 (size 16)...
[AVX] [TRC] 1284: Inserting into block @ 0x5f6e7b05cc60
[AVX] [INF] 1284: Emitted intrinsic _mm_xor_ps -> mreg 480
[AVX] << Exit emit (EA: 1284)
[AVX] [TRC] 1284: IntrinsicBuilder dtor: Ownership transferred for _mm_xor_ps
[AVX] << Exit lift_logic (EA: 1284)
[AVX] << Exit apply (EA: 1284)
[AVX] Match found for itype 810 at 1299
[AVX] >> Enter apply (EA: 1299)
[AVX] >> Enter lift_conversion (EA: 1299)
[AVX] >> Enter load_operand_safe (EA: 1299)
[AVX] [TRC] 1299: Loading Op 1: Type=1, Dtype=8
[AVX] [TRC] 1299:   -> Register 64 mapped to mreg 480
[AVX] << Exit load_operand_safe (EA: 1299)
[AVX] >> Enter load_operand_safe (EA: 1299)
[AVX] [TRC] 1299: Loading Op 2: Type=2, Dtype=2
[AVX] [TRC] 1299:   -> Memory operand loaded to mreg 176
[AVX] << Exit load_operand_safe (EA: 1299)
[AVX] Resolving type: __m128
[AVX] Found named type: __m128 (Size: 16)
[AVX] Resolving type: __m128d
[AVX] Found named type: __m128d (Size: 16)
[AVX] [TRC] 1299: IntrinsicBuilder ctor: _mm_cvtss_sd
[AVX] [TRC] 1299: Adding arg reg: 480
[AVX] [TYPE] Arg Type: __m128d (Size: 16, Empty: 0)
[AVX] [TRC] 1299: Adding arg reg: 176
[AVX] [TYPE] Arg Type: __m128 (Size: 16, Empty: 0)
[AVX] [TYPE] Return Type: __m128d (Size: 16, Empty: 0)
[AVX] >> Enter emit (EA: 1299)
[AVX] [TRC] 1299: Constructing m_call instruction...
[AVX] [TRC] 1299: Constructing m_mov instruction to dst 480 (size 16)...
[AVX] [TRC] 1299: Inserting into block @ 0x5f6e7b05cc60
[AVX] [INF] 1299: Emitted intrinsic _mm_cvtss_sd -> mreg 480
[AVX] << Exit emit (EA: 1299)
[AVX] [TRC] 1299: IntrinsicBuilder dtor: Ownership transferred for _mm_cvtss_sd
[AVX] << Exit lift_conversion (EA: 1299)
[AVX] << Exit apply (EA: 1299)
1280: INTERR 50836

I also stumbled over another INTERR which I can’t replicate right now.

For one:

  1. What does 50836 mean exactly? From reverse engineering my understanding is that this represents an invalid instruction?
  2. For the love of god, please document all of these error codes because it’s making my life an absolute agony.

Thanks!

Agree, these error codes must be part of IDA SDK with some human friendly comments.

But that specific one you are asking about is already in IDA SDK in verify.cpp:

case m_shl:
case m_shr:
case m_sar:
  if ( r.size != 1 )
    INTERR(50835); // wrong operand size
  if ( r.t == mop_n )
  {
    int shm = mvm.get_shift_mask(l.size);
    if ( shm != 0 && uint8(r.nnn->value) > shm )
      INTERR(52118); // wrong shift value
  }
  // no break
case m_ldc:
case m_mov:
case m_neg:
case m_bnot:
case m_fneg:
  if ( l.size != d.size )
    INTERR(50836); // wrong operand sizes
  break;
1 Like

Thank you very much.

In the mean time I have seen the other INTERR again: 50312. Any lead on what that means?

Most error codes are visible in verify.cpp and cverify.cpp, we normally ship them together with the decompiler or the SDK.
The error 50312 means that the bitset::add function was called with wrong arguments (most likely the second argument, the number of bits to add is too high and there was an integer overflow). Again, probably related to a wrong operand size.
Unfortunately we cannot document all interr codes because understanding them is impossible without a context, in other words understanding of the decompiler internals.

1 Like

Thank you very much, although I must say that working with microcode is significantly hindered by both the lack of documentation (which makes it feel like bruteforce reverse engineering) and these error conditions coming together.

Is there any documentation by any chance aside from various priors in plugins and examples that could help establish a better grasp on what to look out for with regards to microcode?

For instance, it is not clear to me if lifting AVX512 would currently even be possible in a sound and accurate manner due to the specific requirements in operand sizes into something that can be properly processed and simplified by hexrays?

I’ll try to cover as much as I can of the smaller AVX extension instructions, and maybe I get acquainted enough that I figure out myself how straight forward all of this actually is.

That being said, I think your reply is a perfect example of how this could be handled – you wouldn’t need to document all and every internal error with full context if - for all I care - it comes down to “wrong operand size”.

I mean, at least acknowledge the existence of an error code. It’s a very frustrating feeling to deal with an error that I stumble upon by chance, without context, and without a feeling for the severity of an error (i.e. do I have to scrap my entire approach, or do I simply need to adjust something here and there).

If there was a list that said “50312 - Unspecified overflow - Usually caused by wrong operand size”… that would be absolute :chefskiss: because it reassures and gives general guidance.

I was in the position to give you a hint about the wrong operand size just because of the context. Otherwise I’d be as clueless as you are.

I did not understand you about acknowledging the existence of the error code. What do you mean by that?

As about feasibility of AVX512 or any other extension: we don’t know. AFAIK nobody has tried to do it. I don’t see any inherent limitations of the microcode that would hinder it but surely you may meet some rough edges along your way. Pioneers meet the unseen.

If you have any specific questions about microcode, we’d be happy to reply. However, please read the existing docs too. For example, the SDK comes with the file named readme_decompiers.txt, which contains a reference to verify.cpp. Also, hexrays.hpp has a reference to verify(), which you should use extensively.

By the way, at the bottom of verify.cpp we have a short list of frequent interrs that other users encountered. Currently the list is short but it will grow over time.

Anyway, thanks for your post. Keep them coming.

About AVX, operand size and INTERR. Could you please look into this piece of code

How do you comment it? Can the problem be solved another way?

128-bit arithmetic operations can be displayed in pseudocode because we support integral types up to 128 bit. However bigger integral types are not supported by any C/C++ compiler AFAIK. This explains why XMM_SIZE works but YMM_SIZE doesn’t.

To avoid the error, you could try to use a 256-bit type (a struct) and mark the operands as UDT instances. As visible from the code in verify.cpp, there is a check for UDTs (is_udt).

Thanks a lot, I’ll try it

Simple set_udt() like these

mop_t* l = new mop_t(reg2mreg(cdg.insn.Op2.reg), op_size);
mop_t* r = new mop_t(r_reg, op_size); 
mop_t* d = new mop_t(d_reg, op_size); 
l->set_udt();
r->set_udt();
d->set_udt();

cause INTERR 50757. I see !is_udt() check in verify.cpp for this INTERR, but it seems the OPROP_UDT flag has been lost somewhere.

This looks like a bug, we’ll try to repro and fix it. Thanks!

jfyi in order to speed up my lifter iteration I wrote this tool that analyzes a binary, prints microcode, disassembly and pseudocode: IDA CLI Dump · GitHub – maybe it’s useful for anyone.

Just add it where idacli is in ida-sdk/src/idalib/examples

Could you send us a sample idb file where this interr shows up? I cannot find one so far.

Sample idb is attached to ticket SUPPORT-7074
Thank you.

Edit:

ZMM (512-bit) memory operands: bypassing load_operand() internal verificationSOLVED

Following up on the UDT discussion above regarding YMM/ZMM operand sizes and INTERR 50757. Documenting the solution for anyone else building AVX-512 lifters.

Context: AVX-512 lifter plugin, lifting ZMM instructions to _mm512_* intrinsics.

What worked initially: ZMM register-to-register operations. Setting set_udt() on all operands > 8 bytes bypasses size validation in mop_t::verify():

// call result + mov destination
if (size > 8) {
    call_insn->d.set_udt();
    mov_insn->l.set_udt();
    mov_insn->d.set_udt();
}

// mcallarg_t
if (ca.size > 8) {
    ca.set_udt();
}

What failed: ZMM instructions with memory operands, e.g. vaddps zmm0, zmm1, [rdi].

Root cause: codegen_t::load_operand() emits m_ldx and internally calls minsn_t::verify() before returning. The 64-byte destination lacks UDT flag at verification time → INTERR 50757.


Solution: Bypass load_operand() entirely for 64-byte operands

Key insight: codegen_t::emit() does NOT internally verify. Verification occurs later at mba->verify(). So:

  1. Use load_effective_address() to compute pointer (pointer-sized, verifier-safe)

  2. Manually emit m_ldx/m_stx via emit() with UDT flag pre-set

mreg_t emit_zmm_load(codegen_t &cdg, int opidx, int size)
{
    mba_t *mba = cdg.mba;
    const int asz = mba->stkoff_oprnd.size; // address size

    // 1. Compute effective address (pointer-sized, no verification issue)
    mreg_t ea_reg = cdg.load_effective_address(opidx);
    if (ea_reg == mr_none)
        return mr_none;

    // 2. Allocate destination kreg
    mreg_t dst_reg = mba->alloc_kreg(size);
    if (dst_reg == mr_none)
        return mr_none;

    // 3. Build operands with UDT flag SET BEFORE emit
    mop_t seg, off, dst;
    seg.make_reg(mr_ds, 2);           // segment, size=2
    off.make_reg(ea_reg, asz);        // offset, pointer-sized
    dst.make_reg(dst_reg, size);      // destination, 64 bytes
    dst.set_udt();                    // critical: bypass 50757

    // 4. emit() does NOT verify internally
    minsn_t *ldx = cdg.emit(m_ldx, &seg, &off, &dst);
    if (!ldx)
        return mr_none;

    return dst_reg;
}

mreg_t emit_zmm_store(codegen_t &cdg, int opidx, mreg_t src_reg, int size)
{
    mba_t *mba = cdg.mba;
    const int asz = mba->stkoff_oprnd.size;

    mreg_t ea_reg = cdg.load_effective_address(opidx);
    if (ea_reg == mr_none)
        return mr_none;

    mop_t seg, off, src;
    seg.make_reg(mr_ds, 2);
    off.make_reg(ea_reg, asz);
    src.make_reg(src_reg, size);
    src.set_udt();

    minsn_t *stx = cdg.emit(m_stx, &seg, &off, &src);
    return stx ? src_reg : mr_none;
}

Integration: Route ZMM memory operands through these helpers instead of load_operand():

mreg_t load_operand_udt(codegen_t &cdg, int opidx, int size)
{
    if (size > 32) // ZMM
        return emit_zmm_load(cdg, opidx, size);
    return cdg.load_operand(opidx); // XMM/YMM use standard path
}

Verification requirements (from verify_segoff):

  • seg.size == 2

  • off.size == addrsize(mvm) (4 or 8)

  • d.size > 0 and d.is_udt() for sizes > 8

Result: ZMM memory operands now lift correctly. The emitted microcode passes verification since UDT flag is set before any verifier sees the 64-byte operand.

Remaining caveat: Hex-Rays decompiler still emits “unsupported processor register ‘zmm0’” warnings for ZMM. Functions partially decompile with lifted intrinsics where possible.

Sharing in case useful for others working on vector instruction lifting.

Related commit in my lifter for reference: ida-lifter / 78101d0.

Can you please elaborate on these INTERR codes and their actual meaning:

Code Location Meaning I inferred from context (?)
50311 (IDA internal) AVX-512 EVEX instructions not fully supported
50420 mba_t::free_kreg Freeing a kreg that’s still in use
52368 cfunc_t::verify Type mismatch during C-tree generation

Here you are:

50311 bitset_t::add() received negative bit number.

50420 attempt to free invalid kernel register.

52368 mismatch between the type and location of a register argument.
even a type that matches the slot size (uint64 for 64bit apps)
did not match.

1 Like

Great, I feel like my lifter works reliably for almost all AVX / AVX2 cases on most binaries. I tried replicating lots of simple to complex mixtures of instructions, and for some AVX2-heavy games I fixed the last remaining errors. Only issue still left is that IDA seems to be throwing a lot more “bad function frame” errors compared to earlier versions.

Would love if anyone could try it out, pretty happy about the result. Maybe someone could even have a try at expanding the AVX512 support :slight_smile:

Not sure if I go with AVX-10 next, or with NEON instructions…