3.1. Process Address Space (CS:APP 7.9, 9.7.2)

3.1.1. Segment Layout

Segments

From low to high virtual addresses:

SegmentContents
.text, .rodatacode, string literals, const globals
.datainitialized globals and statics
.bsszero-initialized and uninitialized globals and statics
heapmalloc memory, grows upward
shared librarieslibc etc. (mmap region)
stackframes, grows downward
kernelinaccessible to user code

Note: Code → .text segment is read only and executable

  • is not Writable & executable for security reasons

Example: Where each object lives:

Note: with this layout: stack addresses are bigger than heap addresses always!

  • thus int a = 0 (on stack) and int *b = malloc(...) then &a > b because of that
int g = 1;            // .data
int h;                // .bss
const char *m = "hi"; // m in .data, "hi" in .rodata
int main(void) {
    static int c = 0; // .bss
    int l;            // stack or register
    int *p = malloc(4); // p on stack/register, *p on heap
}

→ Address Space Layout Randomization randomizes the base of the stack, the shared libraries. It is a defense against buffer-overflow attacks.

  • makes debugging tougher

.bss section: holds writable static-duration objects whose initial value is all zero bytes.

  • thus the binary doesn’t waste space storing all zeros explicitly
  • int a[1000000] costs nothing to store, only the size!

3.2. Run-Time Stack (CS:APP 3.7.1)

Stack Frame

Each active procedure call owns a frame, created on entry and freed on return. The stack grows toward lower addresses, and %rsp points to its top.

  • call pushes the return address.
  • The callee’s frame holds saved registers, locals that need memory, and arguments 7 and beyond.
  • The first six arguments go in %rdi, %rsi, %rdx, %rcx, %r8, %r9; the result is returned in %rax.

Note: not all locals are always on the stack → compiler is free to keep locals and arguments only in registers

Example Stack Frame. The fp points to high-address of current frame, sp to low address.

Note: when we pop a frame, we don’t zero it, we just keep track of the stack bottom.

How do we know frame size? (i.e. by how much do we need to decrease / increase stack / frame pointer):

  • the frame size is known at compile time
    • unknown size objects are put on heap
  • thus code gets addi sp, sp, -32 on entry and on exit +32 inserted as constants.

Also note, frame pointer is technically not necessary, -O2 omits it.

3.3. Pointers and Memory (CS:APP 3.10.1, 3.8.2, 9.9)

3.3.1. Pointer Basics

Pointer

A pointer is a variable whose value is the address of an object. NULL is guaranteed never to point to an object

Important: dereferencing NULL pointer segfaults on Linux.

  • it’s literally just 0x00000...

Address-of and Dereference Are Inverse

If x : T then &x : T *, and if p : T * then *p : T. Hence *&x is x and &*p is p.

Inverse operators:

  • &x (address-of) gives you a pointer to x.
    • if x has type T, then &x has type T*
  • *p (dereference) gives you the object a pointer points to.
    • if p has type T*, then *p has type T

Note: *&x ist just x and &*p is just p

Example:

int x = 5;
int *p = &x;
int **pp = &p; // pointer to pointer to int
**pp = 7;                    // x == 7
printf("%p\n", (void *) p);  // %p expects void *

Note: we can technically have int *******...****p very long pointer chains, nothing prevents this.

  • useful for 3d arrays for example: a[i] = *(a + i)
    • thus a[i][j][k] = *(*(*(a + i) + j) + k)
  • especially useful for non-regular arrays

3.3.2. Pointer Arithmetic

Pointer Arithmetic

For T *p, the expression p + n points bytes further.
p - q counts elements (type ptrdiff_t).

Note: Can only add integers to a pointer! Nothing else (except if it casts automatically).

Note: Casting a pointer changes the stride.

  • Arithmetic on void * is not standard C (GCC uses stride 1 as an extension). There are no bounds checks.

→ sizeof is evaluated at compile time to size in bytes!

Example: With int *p holding 0x1000:

  • p + 1 → 0x1004
  • (char *)p + 1 → 0x1001 (as char is 1 byte)
  • (long *)p + 1 → 0x1008

For int arr[3] = {1, 2, 3}, the bytes seen through char *c = (char *)arr on little-endian x86 are c[0..3] and c[4] .

  • little endian so 1is [1, 0, 0, 0] (4 byte integers = 32 bits)

Note: there’s no checks on where a pointer points to → we can just ready anything (segfault possible though).

3.3.3. Dynamic Allocation

malloc, calloc, free

  • malloc(n) returns n uninitialized bytes, or NULL on failure.
  • calloc(len, size) returns len * size zeroed bytes and fails if that product overflows.
  • free(p) releases a block. Each block must be freed exactly once; forgetting it leaks memory, freeing twice is UB.

We can malloc some memory in the following way: malloc(length * (sizeof(T)));

  • this gives us length times the amount of memory needed by type T
    → we then should check that the pointer returned by this is != NULL

Note: not guaranteed that malloc’ed memory is actually s, we need to memset:

  • memset(data, 0, length * (sizeof(T)));

Note: Malloc returns 8 or 16 byte-boundary aligned memory usually.

Example:

T *data = malloc(length * sizeof(T));
if (data == NULL) { /* handle error */ }
memset(data, 0, length * sizeof(T));
// equivalent, and overflow-safe:
T *data2 = calloc(length, sizeof(T));
free(data);
free(data2);

3.4. Arrays vs Pointers

3.4.1. Decay Rules

Subscript Is Pointer Arithmetic

A[i] is defined as *(A + i). Hence A[i] i[A].

This is just syntax sugar for us. Note:

  • for int a[10] we have assert(a == &(a[0])) i.e. &a == a
  • it really is just the pointer to the first element.

Array Decay

An array expression is converted to a pointer to its first element, except:

  1. as the operand of sizeof: sizeof(a) is the size of the whole array
  2. as the operand of &: &a has the same address but type T (*)[N]

Note: Arrays are not assignable: a = b is a compile error.

Example: For int a[10]:

  • sizeof(a) = 40 = 10 * bytes
  • sizeof(&a[0]) = 8 → pointer size, since &a[0] is int *
  • a + 1 is address
  • &a + 1 is address
    • since sizeof(a) = 40! and &p gives us pointer to object int[10].

Note: as a string-literal initializer: char a[] = "..." is a pointer to stack and modifiable.

  • char *b = "Hello" is pointer to read only section of memory (.data, not modifiable)

Note: 5[a] == a[5] is valid, since addition is commutative!

3.4.2. Parameters and Pass-by-Reference

Array Parameters Are Pointers

As function parameters, T a[], T a[42] and T *a are identical. Inside the function, sizeof(a) . The length must be passed separately.

C passes every argument by value.

  • for complex types, that value is the value of the pointer to that object
    • pointer to array, pointer to struct
  • A function can modify the caller’s objects only through pointers to them.

Example:

void broken_swap(int a, int b) { int t = a; a = b; b = t; } // swaps copies
void swap(int *a, int *b)      { int t = *a; *a = *b; *b = t; }
swap(&x, &y);

q

Note: We cannot assign array1 = array2 (array1 is not modifiable lvalue, but to be LHS of = we need a modifiable one).

  • int array1[42] is an object of type int[42] → really is an array!
  • this is cursed, but int array[] decays to being int * array pointer. And = array2 always decays to &array[0].

This would work, but it leaves array1 untouched:

int (*p)[42] = &array1; // p points to the whole array1
p = &array2; // fine: p is a modifiable lvalue, types match

TODO

3.5. Function Pointers

Function Pointer

int (*f)(int) declares a pointer to a function that takes an int and returns an int. A function name decays to a pointer, just like an array name does. f(x) and (*f)(x) are equivalent calls.

Reading Declarations

Start at the identifier, read right while possible ([], ()), then left (*, the type), and step outward through parentheses.

Example:

DeclarationMeaning
int *p[13]array of 13 pointers to int
int (*p)[13]pointer to an array of 13 int
int *f(void)function returning int *
int (*f)(void)pointer to a function returning int

Example: A qsort comparator. The comparison form avoids the overflow that x - y could cause:

int cmp_int(const void *a, const void *b) {
    int x = *(const int *)a, y = *(const int *)b;
    return (x > y) - (x < y);
}
qsort(arr, n, sizeof(int), cmp_int);

Extra

Reading Pointer Expressions

The golden rule is: declaration mirrors use.

This means that int *p reads “the expression *p is an int”.

Box-and-Arrow Diagrams

We can draw the current state of the program’s memory, denoting pointers by arrows pointing to other memory locations.
This helps figure out what is going on.

Example: