HackBlue

Tools · Ghidra · 15 / 22

Ghidra: stack frames and where locals live

The stack frame is the function’s private storage: locals, saved registers, and any arguments that did not fit in registers. Ghidra’s frame lists each slot’s size and where it lives. Reading that list is how param_ and local_ names become real objects.

15 min read · practice only with authorization

The frame is the function’s storage list

Every function object has a stack frame, even when the compiler kept most values in registers. The frame is a table of variables: parameters, locals, and registers the function saved so it could restore them. Each row has a size and a storage location. Storage might be a register, a stack offset, or both if the compiler spilled a register onto the stack partway through the function. The decompiler’s names are a view of this table. The stack editor is the table itself.

Open it from the function’s menu, Edit Stack Frame, when you want the sizes without the decompiler’s prose. A 128-byte local next to a call whose length argument is 128 is a buffer of that length. A 64-byte region addressed through a pointer parameter is the caller’s buffer, and it may not be a local of the callee at all. Read the storage before you decide the variable is “on the stack.”

Offsets and the names that encode them

On x86 and x86-64 the stack grows toward lower addresses. Compilers typically address locals at negative offsets from the frame pointer, or from the stack pointer when there is no frame pointer. Arguments that truly live on the stack appear at positive offsets. Ghidra’s placeholder local_14 is telling you an offset, in hexadecimal, and nothing about purpose. param_1 is telling you an ordinal. Both are useful until you know more, and both are in the way afterward.

Renaming does not discard the offset. The frame still shows storage. You can name the 128-byte slot line and later come back to see that it sits at local_80 or whatever address your build used. The notebook can record the offset once. The database should record the role, because the role is what you forget.

Read a frame against the calls

The calls are what make slots legible. fgets of a 128-byte local means the slot is a line buffer. fopen’s return stored into a pointer local means that slot is the FILE *. A pointer parameter that is written through, and that the caller passed as the address of an int, is the port out-parameter. You are matching storage to use. Where the match is ambiguous, leave the placeholder. A wrong name in the frame is harder to spot later than a local_ you still owe an explanation.

A stack check, if this build has one, shows up as an extra slot set near the start and tested near the return, often beside a call to __stack_chk_fail on the failure path. Name it as the canary if you are sure, or leave it and comment the check. The size of line is the fact a reviewer cares about: this function keeps a 128-byte line on the stack and asks fgets to stop at that size. In source you own, that length argument is the bound. The frame is how you see the bound from the binary.

Lab: name the slots in the file-opening function

Open the stack editor for the function that calls fopen and fgets. Make a row in the notebook for each parameter and each local: current placeholder, size, storage, and the role you are willing to assign. You should be able to justify path, the line buffer, the file pointer, and the port out-parameter from the calls. If a slot has no use you understand, write “unknown” in the role column and leave the placeholder in Ghidra.

Retype and rename only the rows you justified. Look at the decompiler once more. It should read as a function that opens a path, fills a line, and writes a port, with the buffer sizes visible. Save the program. The frame is part of the database you are about to hand to the bookmark and version-tracking lessons.

Snippet
Slots you should be able to find in load_config

path      pointer     first argument, register
host      64 bytes    caller-provided buffer, pointer argument
line      128 bytes   local buffer passed to fgets
port      integer     pointer argument, written before return
file      pointer     local, the FILE* from fopen

Diagram

A parameter in a register and a buffer on the stack

  1. Frame

    Each function object owns a frame: parameters, locals, and saved registers, with sizes and storage.

  2. Editor

    The stack editor lists those slots. Open it from the function, or read the same facts in the decompiler’s variable list.

  3. Offsets

    On x86 the stack grows downward, so locals often appear at negative offsets and stack-passed arguments at positive ones.

  4. Names

    local_14 encodes an offset. Replace it when you know the role. The offset was only a bookmark.

Storage is part of the variable

path

Role. The config path pointer, first argument.

Storage. RDI on the System V call. The frame may show a register, not a stack offset.

Name. path, typed char *, after you have seen it arrive and leave for fopen.

line

Role. The 128-byte buffer fgets fills.

Storage. A stack slot large enough for 128 bytes, often at a negative offset.

Name. line. The size is the fact. The name records what the call uses it for.

Slots you should be able to find in load_config
path      pointer     first argument, register
host      64 bytes    caller-provided buffer, pointer argument
line      128 bytes   local buffer passed to fgets
port      integer     pointer argument, written before return
file      pointer     local, the FILE* from fopen

Check yourself

1. The decompiler calls a local local_88 and the frame shows a 128-byte slot at that offset. fgets is called with that address and the length 128. What do you rename it?

2. The path parameter’s storage in the frame is the register RDI, with no stack offset. What does that mean?

3. You want a single list of each local’s size and storage for the function you are reading. Where is that list kept?