0

Is there any difference between

JMP EDI 

and

PUSH EDI
RET

what kind of c code will be disassembled this way? a dynamically resolved function pointer, then being called?

Some programmer dude
  • 400,186
  • 35
  • 402
  • 621
  • 2
    @Someprogrammerdude Not really – the effect of these two is the same except that `push edi; ret` overwrites a word on the stack. No sane C compiler would generate code like this though; it just doesn't make sense. – fuz Feb 27 '20 at 12:52

1 Answers1

0

If you're looking for a ROP gadget, volatile int tmp = 0xffe7ffe4 will compile to a mov whose immediate contains the machine code bytes for both JMP EDI and JMP ESP. But of course when executed normally, it's just mov dword [esp], 0xffe7ffe4 or whatever that the compiler emitted. Any other use of that 0xffe7 constant may compile to machine code that includes it as an immediate.

Compilers will never emit push / ret. It breaks the return-address predictor stack leading to mispredicts in later returns up the callstack. Even a retpoline normally only uses call/modify stack/ret. (GCC will emit retpolines automatically with -mindirect-branch=thunk). You could of course get that 3-byte sequence as an immediate, though.


jmp edi could possibly come up as a normal part of compiler output (not just embedded as part of another instruction). But it's unlikely. In 32-bit calling conventions, edi is call-preserved so you won't find jmp edi implementing a tailcall; registers have to restored before a return or tailcall. If you get GCC to keep a function pointer in a register, it will tailcall with mov eax, edi / pop edi / jmp eax to restore its callers value before tailcalling. (Godbolt).

A call edi is not that hard to come by with local function pointers, if you use enough other locals that the compiler's register allocator chooses EDI. But not jmp.

In x86-64 System V, RDI is an arg-passing register and call-clobbered, but of course pointers are 64-bit so you'll always get jmp rdi. In the ILP32 variant (the x32 ABI), the calling convention still apparently requires pointer args to be zero-extended to 64-bit because GCC -mx32 still emits jmp rdi, not jmp edi. (Clang truncates to 32-bit with mov eax,edi / jmp rax). Disabling optimization is no use; compilers will then just store to the stack and reload into EAX.

Working example for gcc9.2

One sure bet is a GNU C computed-goto to the address of a local label:

// function arg and target both need call-preserved registers to survive across ext()
int use_local_label(int *p)
{
    void *volatile target_launder = &&label1;
    void *target = target_launder;       // defeat constant-propagation

 label1:
    ext();  // non-inline function call forces using call-preserved registers
    if (++*p == 0) target = &&label2;
    goto *target;

    return 0;

 label2:
    return 1;
}

Godbolt gcc9.2 -O2 -m32 -fcall-used-esi (Instead of using a 3rd more local var, I just told the compiler that ESI is call-clobbered to stop it from picking that before EDI. p and target end up in EBX and EDI, respectively.)

use_local_label:
        push    edi                                # save call-preserved regs
        push    ebx
        sub     esp, 20
        mov     DWORD PTR [esp+12], OFFSET FLAT:.L4   # volatile init
        mov     ebx, DWORD PTR [esp+32]               # p = function arg
        mov     edi, DWORD PTR [esp+12]               # target = volatile reload
.L4:
        call    ext
        add     DWORD PTR [ebx], 1
        je      .L5                # if = conditional jump over the goto
        jmp     edi                # goto *target, normally to .L4
.L5:
        add     esp, 20
        mov     eax, 1
        pop     ebx
        pop     edi
        ret
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847