AR

CMPSC 311 – Memory Management Vocabulary

Visualizing Memory: Box-and-Arrow Diagrams

  • Purpose: provide an intuitive, spatial view of how C variables live in memory (names, values, addresses, pointer relationships).
  • Demonstrated with small main containing:
    • Scalar (int x = 1)
    • Fixed array (int arr[3] = {2,3,4})
    • Pointer into the array (int *p = &arr[1])
  • Key observations shown by successive slides (Pages 2-6):
    • Local variables reside in one contiguous stack frame; addresses decrease upward (typical x86).
    • Consecutive array elements are laid out contiguously; pointer p stores address of middle element.
    • Printing addresses with %p illustrates pointer values; printing contents with %d confirms data.
    • A stack frame diagram labels columns “name”, “value”, “address”.
    • Helps answer: “Where is a pointer variable itself stored vs. where does it point?”

Double Pointers (**)

  • Concept: pointer to a pointer; enables indirect updates of the pointer itself.
  • Example exercise0.c (Pages 7 & 15):
    • char hi[6] = "hello\0";
    • char *p = &hi[0];
    • char **dp = &p; ( dp points to p, which points to first element).
  • Execution steps:
    1. *p and **dp both yield 'h' because they ultimately dereference to same byte.
    2. After p += 1, pointer moves to 'e'; but *dp still unchanged (points to old address), so *p**dp.
    3. *dp += 2; modifies the pointer through double indirection; now both point to 'l'.
  • Take-away: char * vs char ** differ in level of indirection: the latter lets you change where the former points.

Pointer Arithmetic (Typed Scaling)

  • Rule: adding n to pointer of type T* advances by n\times\text{sizeof}(T) bytes.
  • Demonstration program pointerarithmetic.c (Pages 17-31):
    • int *int_ptr vs char *char_ptr pointing to same address initially.
    • On 32-bit little-endian x86, int is 4 bytes so int_ptr+1 steps 4 bytes; char_ptr+1 steps 1 byte.
    • Reading beyond array bounds shows garbage (-1073745224)—undefined behaviour.
  • Illustrated memory dump: byte sequence 01 00 00 00 02 00 00 00 03 00 00 00 and how each pointer interpret/strides through it.
  • Equation: \text{new_address}=\text{old_address}+k\times\text{sizeof}(\text{referenced type})

Buffers: Definition & Typeless Pointers

  • Buffer = temporary holding place; simply a region of memory often accessed through a pointer.
  • void * used for generic buffers (compiler lacks type info). Cast required before dereference.
    • Example (Page 33): cast to char * yields 'a'; cast to int32_t * interprets 4 bytes as integer 1684234849 ( ASCII "abcd" ).

Copy / Fill / Compare Routines

  • memcpy(dest,src,n) – byte-wise copy; no terminator; overlapping undefined.
    • Examples: whole-buffer copy; buffer splicing by passing &buf1[2] as destination.
  • memset(buf,c,n) – fills n bytes with constant c\&(0xFF).
    • Beware when filling non-byte arrays: int b[4]; memset(b,0,4); only zeroes first element!
  • memcmp(buf1,buf2,n) – lexical byte comparison; returns

Memory Classes in C

  1. Static (globals): allocated at load, freed at process exit (int counter).
  2. Automatic (stack): lifetime is function call (int x;).
  3. Dynamic / Heap: manual management via allocator.
    • Needed when: lifetime not tied to call, size unknown, or too large for stack.

Dynamic Allocation API

  • void *malloc(size_t\ size); – raw bytes, uninitialised.
  • void *calloc(size_t\ n, size_t\ sz); – zero-initialised n elements.
  • void free(void *ptr); – releases; good practice: ptr=NULL; after call to avoid *dangling pointer*.
  • void *realloc(void *ptr,size_t\ new_size); – resize in place if possible, else move & copy, returns new pointer.

Example Patterns

  • Allocate struct (Page 46):
  typedef struct { double real, imag; } Complex;
  Complex *AllocComplex(double r,double i){
     Complex *p=malloc(sizeof(Complex));
     if(p){ p->real=r; p->imag=i; }
     return p;
  }
  • Array copy demo (arraycopy.c, Pages 49-60): allocate inside helper, return pointer, free in caller.

Heap & Process Address Space

  • Segments (low→high addresses):
    • Text (*.text, *.rodata) – code, constants (read-only)
    • Data/BSS – global vars
    • Heap – grows upward via program break
    • Shared libs
    • Stack – grows downward
  • malloc/free implemented in libc using brk() / sbrk() or anonymous mmap() to move program break.
  • Example (Page 73): calling malloc(0x1000) 1024 times raised break by 4\,190\,208\ (≈4\,096\,000) bytes, freeing lowered it.

NULL Pointer

  • Defined as 0x0 on Linux; guaranteed invalid. Dereference → segmentation fault.
  • Best practice: set pointer to NULL immediately after free to fail fast.

Memory Hazards

  • Corruption: out-of-bounds writes (b[2]=5), misuse after free, wrong pointer arithmetic, double free, freeing non-heap pointer.
  • Leak: forgetting to free unreachable block (Page 64). Long-running processes may exhaust memory – potential DoS.
  • In rare scenarios leaking is safer than corrupted free.

Debugging Tools

  • Purify, Valgrind, AddressSanitizer (gcc -fsanitize=address) – detect leaks, use-after-free, buffer over/underflow.

Manual vs Automatic Memory Management

  • C/C++/Rust: manual (Rust has borrow-checker aiding safety).
  • Java/Python/Go: garbage collection performs reachability analysis; pros (safety) vs cons (pause/unpredictability).

Summary Cheat-Sheet

  • Allocation family (Page 67): malloc, calloc, realloc, free. All live in user-space; many alternative allocators (dlmalloc, tcmalloc, jemalloc).
  • Pointer arithmetic obeys element size.
  • Use memcpy, memset, memcmp for raw byte operations; avoid mixing with string routines unless NUL-terminated.
  • Always match every malloc/calloc/realloc with one free.
  • After free: ptr=NULL; → safer crashes.
  • Employ sanitizers in development & CI to catch leaks/corruption early.