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?
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?
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.
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