struct
saddress sanitizer
sizeof
This is intended to be a practical guide (rather than an authoritative guide) to memory allocation and management in C, as implemented by clang and gcc for the x86-64 processor family under Linux.
Compiled binaries are typically relocatable, meaning they contain only guidelines about where code may be located in memory; the loader is responsible for assigning specific addresses prior to running the code.
Linux x86-64’s loaders provide the following contents of memory, with large addresses at the top:
Address range | Use |
---|---|
above 0xFFFFFFFFFFFF | kernel memory (OS runs here; if your code accesses these segments as e.g. via *(int *)-1 , your code crashes) |
below 0xFFFFFFFFFFFF | user stack (grows into smaller addresses) |
empty space for future stack growth | |
memory-mapped region (shared libraries) | |
empty space for future heap growth | |
run-time heap (grows into larger addresses) | |
read/write segments (.data for initialized globals, .bss for uninitialized globals) |
|
above 0x400000 | read-only code and data (.init run by loader, .text is your code, .rodata is string constants and such) |
0x0–0x400000 | unused segments, so that *(int *)0 and the like crashes |
All of the user-access regions may be randomized (doing so is called ASLR: Address Space Layout Randomization) to prevent certain families of security vulnerabilities.
struct
sIt is common to use a pointer to a struct
. In fact, it is uncommon to have a struct
typed variable; almost all struct
s are handled through a pointer. But this leads to a syntactic unpleasantness. Because .
has higher precedence than *
, *a.b
means *(a.b)
, so to get a field from a pointer to a struct
requires parentheses: (*a).b
.
Because of this, C has a special operator a->b
that means (*a).b
. We use it extensively in place of what Java or Python would do with a .
.
Although the compiler may optimize this by placing variables in registers, conceptually all local variables (including function parameters) are stored on the stack. Because you’ve already learned to write programs with local variables, you’ve also already learned to use stack memory.
Stack memory is automatically allocated
when a function is invoked and de-allocated
when a function returns, although this does not actually entail much work beyond changing the contents of %rsp
. Because of this, you should never return the address of a local variable.
Consider the following program:
int *makeArray() {
int answer[5];
return answer;
}
void setTo(int *array, int length, int value) {
for(int i=0; i<length; i+=1) array[i] = value;
}
int main(int argc, const char *argv[]) {
int *a1 = makeArray();
setTo(a1, 5, -2);
return 0;
}
The function makeArray
will allocate room for 5 int
s on the stack (e.g., by adding -20
to %rsp
) and return the address of those int
s. The function setTo
will then be invoked, with its first argument being a pointer to it’s own stack frame as setTo
reuses the same stack memory that makeArray
used. If setTo
stores i
on the stack (as opposed to optimizing it into a register), setTo
will not work properly. For example, we might see something like
Address | makeArray’s use | setTo’s use |
---|---|---|
…200 | return address | return address |
…1F8 | saved copy of %rbp |
saved copy of %rbp |
…1F4 | allocated as answer[4] |
pointed to by array[4] and allocated as i |
…1F0 | allocated as answer[3] |
pointed to by array[3] |
…1EC | allocated as answer[2] |
pointed to by array[2] |
…1E8 | allocated as answer[1] |
pointed to by array[1] |
…1E4 | allocated as answer[0] |
pointed to by array[0] |
… which would mean that in setTo
the values of i
will repeat an infintite loop: 0, 1, 2, 3, 4, in the usual way, but then iteration i=4
will assign to array[4]
which is address …1F4 which is also the value of i
, setting it to -2 and causing the loop to repeat as -1, 0, 1, 2, 3, 4, -1, 0, 1, … forever.
Note that this bug may become invisible if we compile with optimizations and i
is stored only in a register; we still did the wrong thing in makeArray
, so this is still a bug, but we might not see it in this program.
Global variables in C are allocated in regions of memory that are accessible to all functions, which memory is set aside when the program is compiled and thus must be of a size the compiler can determine at compile time. Use of global variables can be an efficient way to program, provided you know in advance how much memory you’ll need.
A common pattern in using global arrays is to (a) #define
a maximum size and (b) use a variable to track how much has actually been used.
The following is a simplified partial example of how one might collect a set of courses a student is interested in:
/* A function we'd need to define elsewhere that reads up to max chars *
* from the keyboard into an array, returning how many were read */
unsigned get_input(char *dest, unsigned max);
#define MAX_CLASS_SIZE 12
#define MAX_CLASSES 20
/* Note: 2D arrays are declared with sizes in the same order as indices */
char interest[MAX_CLASSES][MAX_CLASS_SIZE];
unsigned classes = 0;
int main(int argc, const char *argv[]) {
while (classes < MAX_CLASSES) {
puts("What class are you interested in? Press enter when done.");
unsigned got = get_input(interest[classes], MAX_CLASS_SIZE);
if (got == 0) break;
else classes += 1;
}
puts("You expressed interest in the following:");
for(int i=0; i<classes; i+=1) {
puts(interest[i]);
}
return 0;
}
Correct implementation of functions like get_input
will be a subject for a future part of this course.
Because global arrays are simple to program and efficient in practice, they are common in C code. Because a global array typically needs associated global information like used size, that means other global variables are also common.
A sizable (though not unanimous) majority of software engineering texts I have consulted explicitly state that global variables are bad.
One of the more readable examples I’ve found is http://wiki.c2.com/?GlobalVariablesAreBad. However, even when well written these tend to refer to topics like coupling
and namespace polution
that are difficult to motivate properly before you’ve work on large software projects yourself.
If the stack cannot be used for long-term pointers and global variables have to be allocated at compile time and may also be bad for software maintenance, how do you allocate memory as the program runs and pass it around to different functions? The answer: put it in/on1 the heap.
The heap is a region of memory where
Heap
is used in two main ways in programing. When discussing memory, the heap
is an unorganized region of memory made out of many heterogeneous chunks of memory with different purposes. When discussing data structures, a heap
is a partially-organized tree structure where small
things make their way to the top without requiring complete ordering of the whole. There is no relationship between these: the heap is not a heap, and a heap need not be stored in the heap. This course will only use it in the former way: a region of memory, not a data structure.
The operating system has final say on what memory, and how much of it, each program gets. It handles this via several concepts, including virtual addresses that ensure that two different processes cannot access one another’s memory and segments that prevent you from jumping to an array or dereferencing a pointer to unallocated memory.
Operating systems typically allocate memory in large regions called pages
. It is common today2 for pages to be 4KB – that is, 4096 bytes. Programs can ask the operating system for more pages of memory, or return pages of memory to the OS. However, programmers typically want to handle memory at finer-grained resolution, and C provides library functions to assist with this.
malloc
(memory allocate)The library function void *malloc(size_t size);
returns a pointer to the first byte of a size
-byte region of memory that is allocated on the heap and not used by any previous purpose. It does this by
The internal bookkeeping data structure allows subsequent calls to malloc
to be guaranteed not to return the same (or an overlapping) region a second time.
It is typical to malloc
a struct
with sizeof
and a pointer type cast, like
It is typical to malloc
an array with sizeof
and a multiplier, like
With very rare exceptions, we malloc to store one or more values of a given size inside the malloced memory; if you find yourself malloc
ing without a sizeof
inside, you almost certainly did something wrong.
free
The library function void free(void *);
accepts a pointer returned by malloc
and marks it as no longer in use, and hence as available for future malloc
s.
In small, short-running programs you may be able to get away with never free
ing your data structures, but in larger and longer-running programs this can cause a program to hog all available memory on the computer, slowing all operations and possibly even crashing the program or entire computer. Programs that allocate memory and then forget about it without free
ing it are said to have a Memory leak
calloc
and realloc
Two additional convenience functions can also be useful.
x = calloc(n, s);
is the same as x = malloc(n * s);
except that it (a) may be optimize for storing an array of n
distinct s
-byte values and (b) sets all bytes of allocated memory to 0
. Notably, malloc
does not erase the memory it returns.
After running the following code:
int *x = (int *)malloc(sizeof(int));
int *y = (int *)calloc(1, sizeof(int));
int a = *x;
int b = *y;
the value of a
may be anything, while b
is guaranteed to be 0
.
x = realloc(x, s);
x
to be s
bytes long,s
-byte region,x
into this new region,free(x)
, and thenAfter running the following code:
int *x = (int *)calloc(8, sizeof(int));
x[4] = 123;
int *x = (int *)realloc(x, 16*sizeof(int));
x
is a pointer to an array of 16 int
s. The first 8 elements are {0, 0, 0, 0, 123, 0, 0, 0}
and the next 8 could be anything.
Many languages do not have an equivalent to free
. They let you allocate memory, but never ask you to deallocate it. They avoid (most) memory leaks by adding to your program a garbage collector.
Memory is garbage if it is (a) allocated on the heap and (b) will never be used in the future.
Memory is unreachable if it is (a) allocated on the heap and (b) it is not part of a reachable allocated memory block. A block of memory returned by a single call to malloc
or its friends is reachable if any of the following are true:
All unreachable memory is garbage, but not all garbage is unreachable. Some sources call unreachable memory syntactic garbage
and the more general category of garbage semantic garbage
; confusingly, it is also not hard to find sources that use the word garbage
to mean unreachable
and ignore the existence of other kinds of garbage.
Consider the following code:
int bad(int a) {
int *list = calloc(a, sizeof(int));
for(int i=0; i<a; i+=1) list[i] = (i+1)*(i+1);
int sum = 0;
for(int i=0; i<a; i+=1) sum += list[i];
// midpoint
int ans = 0;
while (sum > 0) {
ans += 1;
ans >>= 1;
}
return ans;
}
The memory returned by the calloc
call becomes garbage at the comment // midpoint
because it is never used after that; it becomes unreachable after the function returns (and hence is a memory leak).
It’s not related to memory, but what mathematical function does bad(n)
compute?3
There are several well-known, well-studied, and carefully-implemented algorithms for performing garbage detection; almost all of these detect only unreachable garbage. A garbage collector is a process that detects garbage and then frees it; the most common model (technically a tracing garbage collector
) works as follows:
free
that garbageThese steps require significant bookkeeping data structures and processing power, and periodically pause the entire program to perform a garbage detection hunt4. In general, this can slow down a program, and increase the memory is uses, and cause it to pause at awkward times. As garbage collectors become more sophisticated and computer memory becomes cheaper these concerns are decreasingly important, and the ability to write code that does not need to worry about free
ing unused memory is a definite plus for software developers. However, because garbage collection always requires some space and time overhead, and because every byte and cycle always matters for some programmers somewhere, languages like C that do not have garbage collection remain common.
This section is devoted to common mistakes people make when handling memory. Some of it is a duplication of material above, re-phrased here for inclusion in the general category of bugs
.
address sanitizer
Most current compilers come with an option to compile in to the binary some additional information that can detect many common kinds of memory errors.
If you compile using clang
, you can add -fsanitize=address
to add in these checks. To get useful error messages when a problem is found with your code, you should also add -g
and -fno-omit-frame-pointer
Note that the address sanitizer inserts bug detection code into the binary at compile time, but only actually detects bugs when the compiled program is run. Because of this, bugs that exist but that your program doesn’t use (e.g., because they are in a branch of an if
statement that your test cases do not exercise) are not detected.
Both gcc
and clang
have a variety of different categories of command-line flags. Often the first letter tells you something about the flag:
-f...
enables or disables a specific feature, such as an optimization, protection, or syntax extension.-m...
changes some aspects of the ISA code is generated toward-O...
selects a group of commonly-used optimizations (some of which are individually selectable using -f...
options)-W...
controls what warning messages are displayed-g...
specifies how much debugging information should be generated and included in the binaryThe following are brief descriptions of several common memory bugs.
A memory leak occurs when you fail to free
garbage or otherwise keep un-free
d heap allocations of un-used memory.
The address sanitizer can detect this bug, but is somewhat conservative in what it looks for. Often it is necessary to explicitly change a pointer before the sanitizer notices the leak.
/** represents a mathematical expression */
typedef struct expr_s {
char kind; // '=' for literal, or '+', '-', or '*' for operators
long value; // only used by '='
struct expr_s *left; // only used by operators
struct expr_s *right; // only used by operators
} expr;
/** turns an expression of literals into a literal */
long flatten(expr *e) {
if (e->kind == '+') {
e->kind = '=';
e->value = flatten(e->left) + flatten(e->right);
/* memory leak: remove pointers without free
* asan will only notice it because of the explicit = NULL */
e->left = e->right = NULL;
} else if (e->kind == '-') {
e->kind = '=';
e->value = flatten(e->left) - flatten(e->right);\
free(e->left);
free(e->right);
e->left = e->right = NULL;
} else if (e->kind == '*') {
e->kind = '=';
e->value = flatten(e->left) * flatten(e->right);
free(e->left);
free(e->right);
e->left = e->right = NULL;
}
return e->value;
}
Memory leaks tend to make the program use more and more memory, becoming slower and slower the longer it runs. In the worst case, this can even cause your entire system to grind to a halt.
Garbage collectors are often said to remove the chance of memory leaks, but this not not strictly true: identifying all garbage is not possible in every case so they generally find and free unreachable memory instead. Even when writing in Java, Python, or other garbage-collected languages, make sure you set unused references to objects to None
/null
and otherwise don’t maintain references to data you will not reuse.
Because malloc
and realloc
do not initialize the memory they allocate, it is an error to access that memory before you initialize it. This is also true of local or global variables, structs, and arrays. Using uninitialized memory is a particularly tricky bug to notice because it is often the case that for many runs in a row the uninitialized memory just happens to be all 0
bytes, and then one time it happens to be other values instead, causing the bug to manifest itself intermittently.
If a function expects a pointer and you give it an integer instead, it will interpret the integer value as being an address. This is particularly problematic with variadic functions like printf
and scanf
that are harder for the compiler to type-check.
sizeof
It is fairly common to make mistakes with sizeof
, such as
using sizeof(T)
when you meant sizeof(T *)
int **A = (int **)malloc(sizeof(int) * n);
using sizeof(T)
when adding to a T *
failing to use sizeof
when malloc
ing
int *ten_ints = (int *)malloc(10);
Most programmers have a hard time remembering the order of operations between prefix and postfix unary operators. Is &a.b
(&a).b
or &(a.b)
? Is *a++
(*a)++
or *(a++)
? Etc.
This lack of clarity leads to programmer mistakes that can cause many kinds of problems; when it includes modifying (or failing to modify) a pointer, those problems can become memory errors.
A few suggestions to avoid these:
--
and ++
unless you actually need their return-previous-value semantics->
instead of a combination of *
and .
whenever you canAfter you free
a block of memory, using a pointer to it is an error.
The address sanitizer is usually able to detect this bug.
The following is a minimal example
More realistic examples generally hide the copying of the pointer and the freeing of its target memory inside other custom functions.
If you index past the end of a stack-allocated memory region, this is called a stack buffer overflow
. This rarely crashes a program itself, but usually messes up what it will do in the future by changing the value of some other local variable or overwriting the return address.
Changing the return address usually causes a segfault when you retq
, but stack buffer overflow can also allow malicious users to take over your program by intentionally supplying a return address that causes retq
to jump to an address of code they included in the buffer overflow or some other code you didn’t want to run.
The address sanitizer is usually able to detect this bug.
Since scanf
’s %s
format specifier reads a non-whitespace sequence of characters into word
, this will be a buffer overflow if you type sixteen or more characters without any whitespace.
If you index past the end of a heap-allocated memory region, this is called a heap buffer overflow
. This can corrupt the heap
– that is, the program continues to run, but the overflow modified some other data structure, messing up some other part of your program.
The address sanitizer is usually able to detect this bug.
If you index past the end of global memory region, this is called a global buffer overflow
. This sometimes causes a segfault, or it might overwrite a different global variable.
The address sanitizer is usually able to detect this bug.
If you return the address of a local variable, and then later use that pointer, you have a use-after-return bug.
The address sanitizer is usually able to detect this bug.
See the worked example in the section Using the stack in C above.
If you dereference a pointer that you failed to initialize, you are likely to end up in an invalid code segment and get a segfault; however, you might by random bad luck end up with a pre-initialized value that points to valid memory and end up overwriting a value some other part of the program depends on.
This is a nuance related to use-after-free.
Each set of braces and each for loop creates its own variable scope. The compiler is free to re-use that stack space after the scope ends if it wants. If you use a pointer to an out-of-scope variable, this creates a user-after-scope bug.
The address sanitizer is able to detect this bug, but requires a special additional flag during compilation to do so: -fsanitize-address-use-after-scope
.
The following code may or may not have this bug, depending on how the compiler choses to optimize it.
When a value is stored in memory that is part of the heap, it roughly equally common to refer to the value as being on the heap
or in the heap
. There appears to be a slight preference for on
to refer to the memory itself and in
to refer to the values stored using that memory, but many exceptions exist.↩︎
a.d. 2018↩︎
Answer: floor(log2((n)*(n+1)/2))+1
↩︎
There are garbage collectors that run while the rest of the program is modifying memory, but doing so has various challenges that make them more complicated and have various possible drawbacks. See https://en.wikipedia.org/wiki/Tracing_garbage_collection#Stop-the-world_vs._incremental_vs._concurrent for more.↩︎