Function Pointers & Callbacks
When code addresses become data
- › Explain that functions live in memory and have addresses, like data
- › Declare, assign, and call through a function pointer
- › Use function pointers to build dispatch tables and callbacks
- › See why overwriting a function pointer hands control to an attacker
Code has addresses too
You already know a function is just bytes in memory — you've been disassembling them, and call
target jumps to a function's address (day 12). So a function *has an address*, exactly like a
variable does. And if it has an address, you can store that address in a variable and call through
it later. That variable is a function pointer: a pointer whose pointee is code, not data. The
line between "code" and "data" is thinner than it looks — both are addressable memory, a theme that
returns with force in exploitation.
Declaring the scary-looking thing
The syntax is notorious, but it's mechanical once you read it right. A pointer to a function taking two ints and returning an int:
1int add(int a, int b) { return a + b; }2int sub(int a, int b) { return a - b; }3 4int (*op)(int, int); // op: pointer to (function taking (int,int) returning int)5op = add; // a function name decays to its address (like an array name)6int r = op(3, 4); // call through the pointer -> add(3,4) -> 77op = sub;8r = op(3, 4); // now -> sub(3,4) -> -1Read int (*op)(int, int) from the inside out: op is a pointer (*op), to a function taking
(int, int), returning int. Assigning op = add stores add's *address* (a function name decays
to its address, just like an array name decays to its first element — day 17). Then op(3, 4) emits
a call to whatever address op holds. Same call machinery as always; the target just came from a
variable instead of being fixed in the instruction.
call add (a fixed target baked into the instruction). A
call through a function pointer is call rax (an indirect call — jump to the address currently
in a register). That single difference — target fixed in code vs target taken from a value in
memory/registers — is why function pointers are powerful *and* why they're a security-sensitive
target: change the value, change where control goes.What they're for: dispatch and callbacks
Function pointers turn "which code runs" into data you can choose at runtime:
- Dispatch tables: an array of function pointers, indexed by an opcode or command — this is how
- interpreters, state machines, and virtual method tables (C++ vtables) work.
- Callbacks: hand a function to another function so it can call you back —
qsorttakes a - comparison function pointer; event systems take handlers.
1int add(int,int); int sub(int,int); int mul(int,int);2 3int (*ops[3])(int,int) = { add, sub, mul }; // a dispatch table4 5int run(int which, int a, int b) {6 return ops[which](a, b); // pick a function by index, then call it7}ops[which](a, b) reads a function pointer out of the array ([base + index*scale], day 17,
scale 8 because pointers are 8 bytes) and does an indirect call to it. Adding a new operation is just
adding an entry — no giant switch. This is elegant engineering *and* an attacker's dream: if a bug
lets someone overwrite an entry in ops, the next dispatch calls *their* address.
ops is an array of function pointers at address 0x4000; pointers are 8 bytes. A call is
ops[which](...). Which memory address does the machine read to get the function pointer when
which = 2? And what kind of call instruction (direct or indirect) results?call add can't be redirected, but call [rax] /
call rax can, if an attacker controls the value. You're meeting, three months early, exactly the
mechanism those mitigations exist to protect.Build a dispatch table
In dispatch.c, write three small functions with the same signature (e.g. add, sub, mul for ints), put their addresses in an array of function pointers, and a run(int which, int a, int b) that calls ops[which](a,b). Demonstrate calling each via its index. Then compile at -O0, disassemble run(), and find the indirect call (call through a register) and the [base + index*8] that loaded the function pointer from the array. Uses only this lesson plus days 12/17.
$ gcc -O0 -g -Wall dispatch.c -o dispatch && ./dispatch$ objdump -d -M intel dispatch- › Your array of function pointers dispatches to the right function per index
- › You found the indirect call (call reg / call [mem]) in run()'s disassembly
- › You found the [base + index*8] that fetched the function pointer
- › You can explain the difference between a direct and an indirect call