Memory Fundamentals
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- C byte pointers, size_t, casts, and basic function state.
What you will practice
- Trace the old and new program break for successful requests.
- Reject unrepresentable increments before a signed conversion.
- Distinguish attempted growth from successfully acquired bytes.
- Explain why raw byte growth does not provide alignment or block reuse.
In this lesson
Lesson
6 min readMemory Fundamentals: Reserve Bytes Before Building an Allocator
You are building a sequence of small components: request raw bytes, describe a block, link blocks, then allocate and reuse them. In this first module, implement sbrk-basics. It tracks a growing region; it does not yet promise aligned objects, block reuse, or free.
Check your starting point
You should be able to distinguish an address from the bytes at that address, explain size_t, and move a byte pointer by an offset. Predict this before continuing: if a region begins at address 4096 and contains 24 bytes, its last byte is 4119 and its exclusive end is 4120. An end pointer names a boundary; it is not a byte you may write.
The region model
For this Linux teaching exercise, imagine one managed region [base, break). Growing its endpoint supplies bytes to the process. Other parts of its virtual address space include code, stacks, libraries, and mappings. There is no universal C diagram in which every heap allocation belongs to this one region: for example, glibc can obtain allocations through both the program break and separate mappings. Linux malloc documentation
Separate three responsibilities:
| Layer | Question answered | Work in this course |
|---|---|---|
| Region growth | Where can we obtain more bytes? | sbrk-basics |
| Allocator metadata | Which bytes belong to which block? | block-header-design |
| Allocation policy | Which free block can satisfy a request? | free-list-operations, then allocation and reuse |
Consecutive successful growth calls are adjacent only while no other code moves the break between them. The exercise runs sequentially and assumes exclusive control of this region. Do not install these helpers as replacements for the C library allocator or mix independent allocators managing the same break.
Follow a successful request
On the supported platform, sbrk(increment) returns the old break on success; sbrk(0) queries it. Failure is (void *)-1, which is different from NULL. sbrk is a legacy interface, outside ISO C and current POSIX. Linux brk/sbrk documentation
Addresses below are invented decimal byte addresses. A successful initialization observes break 4096:
| Operation | Returned value | Break afterward | Successful requested bytes | Growth attempts |
|---|---|---|---|---|
heap_init() |
no value | 4096 | 0 | 0 |
heap_request(24) |
4096 | 4120 | 24 | 1 |
heap_request(8) |
4120 | 4128 | 32 | 2 |
heap_request(0) |
NULL |
4128 | 32 | 2 |
heap_current_break() |
4128 | 4128 | 32 | 2 |
The first request owns [4096, 4120) and the second [4120, 4128). Returning the new break would point just beyond the new storage. Queries are not growth attempts, and zero is rejected before calling sbrk.
heap_init() resets accounting and records the current boundary. It does not lower the break or release earlier storage. Consequently, the requested-byte counter is a total since initialization, not the size of every region in the process.
Make failure a state transition
A request is a transaction: validate, attempt growth, then publish successful accounting.
- Reject zero. Reject a value larger than
INTPTR_MAXbefore convertingsize_tto the signedintptr_tincrement. - Reject an accounting overflow using
size > SIZE_MAX - total_requested. Check before addition. - Attempt
sbrkonce and increment the attempt counter, even if that attempt fails. - If the result is
(void *)-1, returnNULLwith successful bytes unchanged. - Only on success, add the requested size and return the old break.
The task bounds the number of attempts below INT_MAX. Include <stdint.h> for the integer limits. A cast is not a range check: on the usual platform, converting the largest unsigned value to a signed increment can produce a negative value and request a shrink instead of growth.
For example, after the table above, suppose heap_request(64) fails. The break stays 4128, successful bytes stay 32, and attempts become 3. A subsequent successful request for 16 returns 4128, moves the break to 4144, and gives totals 48 and 4. Counting failed bytes would claim ownership of storage that was never obtained.
Complete a trace before coding
Start at break 8192 with reset counters. Request 10 successfully, request 6 successfully, reject zero, then attempt 20 and receive (void *)-1. Fill the missing row:
| Checkpoint | Break | Successful bytes | Attempts |
|---|---|---|---|
| After 10 | 8202 | 10 | 1 |
| After 6 | 8208 | 16 | 2 |
| After zero and failed 20 | ? | ? | ? |
Check your reasoning after completing the row
The row is 8208, 16, 3. Zero makes no call; the failed positive request makes one call but acquires no bytes. If heap_init() is called next, the break stays 8208 while both counters become zero.
Now predict a separate trace without the table: reset at 1000; successfully request 7, reject a size greater than INTPTR_MAX, then successfully request 9. Record both returned addresses, final break, total bytes, and attempts. Explain which branch prevents a signed conversion from changing the meaning of the request.
From raw storage to blocks
These helpers return byte storage of exactly the requested length. Two requests of 1 byte need not both start at an address suitable for a struct. The next module introduces headers; later allocation exercises define an alignment policy and checked rounding. Do not infer a general malloc alignment guarantee from successful region growth.
Likewise, freeing an interior block does not imply that its bytes can be returned by lowering the break. With live A, free B, live C in address order, lowering the endpoint through B would remove C. The allocator will track B for reuse instead.
Build and explain
Implement sbrk-basics with the failure branches above. Check initialization, two contiguous successful requests, zero, an unrepresentable increment, an injected growth failure, and reset-without-shrink. The tests include simulated failures so correctness does not depend on exhausting real memory.
Before taking the quiz, explain why the old boundary is the returned allocation, why attempts and successful bytes differ, and why these functions alone cannot implement free. After a wrong answer, revisit the linked section and retrace a different sequence before retrying.
Practice sequence
Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.
- Step 1
Understanding sbrk
- Step 2
Memory Fundamentals Quiz
Test understanding of heap memory and sbrk