Course / 14 Pwn / Lesson 01 of 04

Memory and the stack, as a picture

Estimated time: 18 min reading · 16 min activity

Before this stage, read Reversing’s Compiled and interpreted before Enough C and assembly. Those lessons distinguish source code, machine instructions and conceptual tool views. Here you apply that distinction to storage: a drawing can teach a boundary without being a captured memory layout.

A memory address identifies a location. A buffer is storage with a capacity, the number of positions it can hold. An index selects a position relative to its beginning; this lesson starts indices at zero. A stack frame is the storage and bookkeeping associated with a function call. The model places a local buffer there while that call is active.

Before you begin

Estimated time
18 min reading, 16 min activity. Setup and optional practice may take longer.
Where you work
Use the printed memory and source models in a desktop or mobile browser, with paper or a text editor. No compilation, debugger, pwntools installation, mitigation change or executable is needed. The layouts are conceptual, not measured process memory. Optional later tools need a separately checked owned environment and budget.
Without a lab
All evidence needed for the activity appears on this page. Copy it into notes if downloads are unavailable. Read the labeled model and predict each result; a paper trace practises the reasoning while tool operation waits for a lab. Reading needs no JavaScript. Answer checking needs JavaScript and a secure context; saving progress also needs usable browser storage.

After this lesson you can:

  • Mark indices 0 through 5 on the six-position note() model.
  • Mark indices 0 through 4 on the five-position tag() card.
  • Say that tag()'s row is that call's storage only while the call is active.

01Example

Worked conceptual note() layout. The later guided tag() has a different capacity.

Buffer slot: 6 byte positions
Valid indices: 0,1,2,3,4,5
Last valid index: 5
Neighbor count: drawn for a visible boundary, not a compiler prediction
Lifetime in this model: while note() is active
Labels model:1000 etc.: row names, not observed addresses

02Where a value lives

Capacity counts positions, while an index names one position. With zero-based indexing the highest valid index is capacity minus one. A count of six therefore ends at index five; index six would already be outside this buffer.

Lifetime describes when an object exists and can be used. In this model the local array belongs to the active call; returning ends that lifetime. A stored address does not keep its local object alive. Real compilers may keep some values in processor registers, reorder storage or remove it entirely, so the drawing is not a machine layout.

Other storage has different lifetimes: dynamically allocated storage, often called heap storage, must be released according to its allocation rules; a static object can last for the whole program. This page models only a local buffer, without predicting its actual address or neighboring values.

Count a buffer’s valid positions and explain when a local object’s storage can be used.Model. A teaching model drawn by hand. It shows the idea, not real tool output.

Read the full explanation below.

note() buffer · 6 positions · active

0—
1—
2—
3—
4—
5—

Needed bytes, including the terminator

Neighbor count; boundary drawing only · unchanged
pending

0 positions needed; 6 available.

Six modeled positions have valid indices 0–5; 5 is the last valid index.

Follow the explanation

  1. Six modeled positions have valid indices 0–5; 5 is the last valid index.
  2. After note() returns, this local storage is unavailable for later use.
Read every state and value

1. Six modeled positions have valid indices 0–5; 5 is the last valid index.

Object
note() buffer
Sample
Not established
Bytes shown
None
Needed
0
Fit
pending
Lifetime
active
Selected Index
5

2. After note() returns, this local storage is unavailable for later use.

Object
note() buffer
Sample
Not established
Bytes shown
None
Needed
0
Fit
pending
Lifetime
returned
Selected Index
Not established
Capacity and index differ; local lifetime ends on return, and the neighbor/address drawing predicts no compiler layout.

03A picture with an edge

The drawing and address table both describe slot’s six positions and a modeled neighboring integer count. The text table supplies the complete interpretation if the figure is unavailable.

Read the rows as a diagram, not a stack-growth direction or a promise that count will follow slot in compiled memory. The labels prefixed model: are intentionally invented. No matched binary/debugger map establishes this layout. The next lesson compares capacity with requested written bytes.

Six buffer positions and a modeled neighbor
Six buffer positions and a modeled neighborModel. A teaching model drawn by hand. It shows the idea, not real tool output.Six boxes numbered 0 through 5 represent the buffer named slot while note() is active. A separate box below is labeled count, a neighbor in this reading model. The positions and arrangement illustrate capacity; they are not addresses, a captured stack layout, or a guaranteed direction of memory growth.

Six slots and one neighbor

Label: conceptual model for note(). Not a captured map, disassembly, or debugger session.
No binary was built. No address was observed. The hex-shaped labels are row names.

Call: note()
Buffer: slot, 6 positions, indexed from 0.
Highest valid index on this card: 5.
Neighbor in the model: count. It is the next row so the picture has an edge. It is not a saved return from a compiler.

model label | row            | what it holds in this picture | lifetime in this model
model:1000  | slot index 0   | one position                  | while note() is the active call
model:1001  | slot index 1   | one position                  | while note() is the active call
model:1002  | slot index 2   | one position                  | while note() is the active call
model:1003  | slot index 3   | one position                  | while note() is the active call
model:1004  | slot index 4   | one position                  | while note() is the active call
model:1005  | slot index 5   | one position                  | while note() is the active call
model:1006  | count          | neighboring integer           | while note() is the active call

After note() returns, this model says these rows are no longer that call's storage.
A compiler may keep a value in a register instead. This table does not show a register.

Download the accessible address table for note()

04Trace tag

The guided call tag() has five positions. Apply the rule without importing the six-position example’s result.

Label: activity card for tag(). Same kind of model as note(). Different size.
Not a captured map.

Call: tag()
Buffer: tag, 5 positions, indexed from 0.
Count the last index from the zero-based rule, then compare it with the table and check below.

model label | row
model:2000  | index 0
model:2001  | index 1
model:2002  | index 2
model:2003  | index 3
model:2004  | index 4

Lifetime sentence on this card: the row belongs to tag() only while that call is active.
When the call returns, the model does not keep the row as tag()'s storage.

Check you can make from this card, before the question:
- five positions
- first index 0
- last index 4
- the row ends as this call's storage when tag() returns

Download the five-position activity card

  1. Copy capacity five and starting index zero from tag-activity.txt.
  2. List its five indices as 0,1,2,3,4 and identify four as the last valid index.
  3. Add its lifetime: these local positions belong to tag() while that call is active; returning ends that modeled object lifetime.
  4. Stop with count, last index and lifetime. Do not invent addresses or compile source to complete this trace.

05Optional challenge

For about ten optional minutes, imagine a function returning a pointer to its local array. A pointer is a value holding an address. Explain why retaining that address after return does not extend the array’s lifetime, even if the old bytes have not visibly changed. This is a source-reading question, not a program to run.

06Fresh cases for the checks

Use these fictional cases for the checks below. They have different inputs from the worked examples. Read them here or make notes; no installation or external service is needed.

Memory case A: conceptual capacityOwned fixture. An original file made for this course. Follow the activity's instructions in your own lab.

Function label() has a local byte array with 7 positions, indexed from 0.
Each position stores one byte. No write is requested.
The local array belongs to this call while label() is active.
The diagram label model:2400 is a row name, not an observed address.

Memory case B: independent small arrayOwned fixture. An original file made for this course. Follow the activity's instructions in your own lab.

Function tag_new() has 4 byte positions indexed from 0.
Rows to classify: position 0; position 3; position 4.
This is a conceptual layout, not compiler output.

07Explain your evidence

Optional written self-check: explain one decision using the supplied case. No tool run is required.

Check your explanation: count zero-based indices; separate capacity from final index; connect storage lifetime to the active call; identify conceptual row labels and missing machine-layout evidence.

08Common mistakes

If the last index equals capacity, list every position starting at zero and count them. The count and final label have different jobs.

If a drawing’s neighbor becomes an actual compiler layout, restore its conceptual label; a real build and matched inspection would be needed.

If an address seems to keep a local object alive, compare the object’s lifetime with the time the pointer is used.

References