roadmap
week 3 · day 20

Function Pointers & Callbacks

When code addresses become data

Medium 30 min 180 xp
After this you can
  • 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:

fp.cc
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) -> 7
7op = sub;
8r = op(3, 4); // now -> sub(3,4) -> -1

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

Key idea
At the machine level, a normal call is 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 — qsort takes a
  • comparison function pointer; event systems take handlers.
table.cc
1int add(int,int); int sub(int,int); int mul(int,int);
2
3int (*ops[3])(int,int) = { add, sub, mul }; // a dispatch table
4
5int run(int which, int a, int b) {
6 return ops[which](a, b); // pick a function by index, then call it
7}

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.

Predict first
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?
Note
This is why modern defenses (Control-Flow Integrity, and later CET's indirect-branch tracking) focus so hard on *indirect* calls and jumps: a direct 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.
finished reading?
Your task, you write the code

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.

deliverable: dispatch.c (+ the indirect-call disassembly)
build & run
$ gcc -O0 -g -Wall dispatch.c -o dispatch && ./dispatch
$ objdump -d -M intel dispatch
self-review before running
  • 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
stretchUse a real callback: call the standard `qsort`, passing your own comparison function pointer, to sort an int array. Then reason: if a bug overwrote one entry of your ops[] table with an arbitrary address, what would the next dispatch do?

Self-check

01What is a function pointer?
02A call through a function pointer compiles to:
03How is a dispatch table (array of function pointers) indexed?
04Why are function pointers a security-sensitive target?
0/4 correct · 0/4 checked