HackBlue

Tools · Ghidra · 07 / 22

Ghidra: functions as the unit of reading

A function in Ghidra is an object: an entry address, a body, a signature, a calling convention, and a stack frame. You read programs one of those objects at a time. Creating or clearing one is an edit to the database.

16 min read · practice only with authorization

A function is more than a stretch of instructions

When Ghidra says something is a function, it has created an object at an entry address. The object owns a body, a name, a signature, a calling convention, and a stack frame. The Symbol Tree lists these objects under Functions. The decompiler will only print a C-like view for addresses that belong to one. Instructions that sit in a gap, with no function claiming them, still exist in the Listing. They are simply not in the unit the rest of the tool reasons about.

The signature is the part beginners trust too soon. param_1 with type undefined8 means “something is passed, and the width is a guess.” It does not mean the program’s author named a first argument. You improve signatures in the function signature editor, usually by right-clicking the function’s name in the decompiler and choosing Edit Function Signature. Changing the signature reprints the function. It still does not modify the executable.

Bounds are a decision

A good body starts at the entry and includes every instruction that runs as part of that routine, including the targets of its local branches. It stops before the next routine’s entry. Analysis gets this right on ordinary compiler output and wrong on data that looked like a prologue, on hand-written code, and on functions that share a return instruction. The symptom of a bad end is a decompiler view that suddenly starts a second prologue, or a Listing where one function’s background color runs into the next label.

Select the instructions that should be the body and use Create Function. The Listing action is also bound to F in a default keymap. To undo a bad function, clear it. The instructions remain, the object is gone, and you can create a tighter one. Afterward, one-shot the stack and reference analyzers if the frame or the calls look stale. User names you attached to other functions stay.

Thunks and the function graph

A thunk is a function whose body transfers to another function and does nothing else. On ELF, calls often land on a procedure-linkage stub that jumps to the external symbol. The decompiler hides that stub and shows printf(...) once it is confident. The Listing shows the stub. Both are right. In a note, name the external. Open the thunk only when you are checking that the stub really does jump there.

Window > Function Graph draws basic blocks as nodes and branches as edges. A loop is a cycle. An if is a split and a join. Use the graph when the decompiler’s control flow feels too clean and you want to see the blocks it summarized. Use the decompiler when the graph is a maze of blocks that the C-like view has already turned into a loop. They are two drawings of the body you already have.

Lab: the function that owns the default path

From the check.conf string, follow the reference into code. Create or accept the function that contains that instruction, if analysis has not already. Fill in the five-line note in the sample: entry address, current name, number of callers, one library or internal call you can see, and whether the body is a thunk. For this function the thunk answer should be no. The body chooses a path and calls another function.

Open the function graph long enough to point at the branch that picks the default path. You want to see two blocks leave the compare and meet again at the call. If the graph and the decompiler tell that same story, the function object is sound enough to name in the next lesson.

Snippet
What to write down the first time you open a function

Entry address:
Placeholder or real name:
How many callers the reference list shows:
One library call inside the body, if any:
Whether the body is a thunk (single jump to an external):

Diagram

A real function and a thunk

  1. Entry

    The function object starts at one address. Calls and the Symbol Tree use that address as the name’s target.

  2. Body

    The body is the set of addresses Ghidra believes belong to the routine. A bad end address pulls in the next function or stops short.

  3. Signature

    The signature is a theory about arguments and the return value. The decompiler reprints the body from that theory.

  4. Graph

    Window > Function Graph draws the body’s basic blocks and the branches between them.

What the body is allowed to contain

A function you will read

Entry. One start address, usually a prologue or the first real instruction.

Body. The instructions that belong to this routine, ending in a return or a tail call.

Signature. Return type, calling convention, and parameters. Placeholders until you improve them.

A thunk

Entry. A tiny function the compiler inserted as a trampoline.

Body. Often a single jump to an external symbol such as printf.

Reading. Follow the jump to the external name. The thunk itself has no further logic.

What to write down the first time you open a function
Entry address:
Placeholder or real name:
How many callers the reference list shows:
One library call inside the body, if any:
Whether the body is a thunk (single jump to an external):

Check yourself

1. The decompiler shows a call to printf, and the Listing shows a call to a tiny function whose only instruction jumps to an external. What is that tiny function?

2. Analysis left a region of real instructions with no function, between two functions you trust. What creates the missing object?

3. A function’s body continues into the next routine, so one signature covers two prologues. What is the careful repair?