GetPC Thunk / call $+5 pop
Position-independent code recovers its own runtime address with a call/pop pair, spawning a phantom call xref that confuses static analysis.
Shellcode and packed stages rarely know where they will be mapped, so they need
a way to learn their own runtime address before referencing embedded strings,
keys, or import tables. The classic GetPC (get-program-counter) idiom does
this with a call to the very next instruction followed by a pop: the call
pushes the return address — which is the current location — onto the stack,
and the pop lifts it into a register. It is the foundation of nearly every
x86 decoder stub, including the Metasploit call $+4/call $+5 thunks and the
Shikata Ga Nai polymorphic encoder.
The side effect is an anti-disassembly one. A linear or naive recursive
disassembler treats every call rel32/call rel8 as a function invocation and
creates a code cross-reference to the target. But a GetPC call targets the
middle of its own basic block — the instruction immediately after it — so the
tool emits a phantom sub-function and a bogus xref, fragments the block, and
often mislabels the pop as a separate routine. The "called" address is never a
real function; the call is just an arithmetic trick to read EIP.
How it works
The canonical thunk is five bytes for the call and one for the pop. The
call displacement is 0 (target = the next byte), so it does nothing but push
its own end address:
; Bytes: E8 00 00 00 00 5B 81 C3 ...
00: E8 00 00 00 00 call 0x05 ; rel32 = 0 -> target is the next insn (0x05)
05: 5B pop ebx ; EBX = 0x05 (the runtime address of this byte)
06: 81 C3 27 10 00 00 add ebx, 0x1027 ; rebase EBX to the embedded data blobA linear sweep sees call 0x05 and dutifully records a cross-reference to
0x05, the same address it is about to decode anyway — so the byte at 0x05
becomes both "fall-through from the call" and "target of the call," and many
tools split the block, attach a spurious sub_5 label, and lose the fact that
pop ebx is simply consuming the pushed PC. The call/pop pair is pure
self-reference: no control transfer of consequence occurs, yet the static xref
graph now contains an invented edge.
A compact variant overlaps the displacement with real code to compound the confusion:
; Bytes: EB 03 E8 ... / ... E8 F8 FF FF FF 59
00: EB 03 jmp short 0x05 ; skip the next 3 bytes
05: E8 F8 FF FF FF call 0x02 ; rel32 = -8 -> backward into the skipped region
0A: 59 pop ecx ; ECX = 0x0A, recovered from the backward callHere the call jumps backward into bytes the jmp already skipped, so the
disassembler's forward sweep and the real call target disagree, while the pop
still harvests the program counter for rebasing.
Detection & analysis
Static analysis:
- The fingerprint is a
callwhoserel8/rel32displacement is0,-5, or another tiny value pointing into or just past itself, immediately followed by apop reg. IDA/Ghidra will frequently show a one-instruction "function" or a red xref arrow that loops back inside the same block. - Treat any
E8 00 00 00 00(call $+5) as a GetPC marker, not a call; undefine the bogus sub-function and re-form the block so thepopand subsequentadd reg, imm(the rebase) read as a single prologue.
Dynamic analysis:
- Single-step the stub: after the
call/popthe recovered register holds the live address of the thunk, which you can subtract back to a file offset to locate the embedded blob the code is about to decode or rebase against. - Emulators (Unicorn, IDA's appcall, speakeasy) execute the thunk and expose the resolved base register, letting you follow the decoder loop to plaintext.
Detection rule hint:
Flag the byte pattern E8 00 00 00 00 followed by any 5x/58–5F (pop r32),
and the jmp-over-then-backward-call-then-pop variant. A call with a
near-zero self-referential displacement directly feeding a pop is a
high-confidence GetPC indicator: legitimate compilers reach a PC via
lea/RIP-relative addressing or __x86.get_pc_thunk helper calls, not via a
zero-displacement call consumed immediately by a pop.