Memory and Addresses

Created

Memory (RAM) is a giant array of 1 byte-sized storage locations.

Each byte storage location has an integer address.

  • 0 would be the lowest address, but we don't want programs to access address 0. -E.g., in C, when we create a null pointer (int *p = NULL), it is typically represented by address 0. This way, we can make NULL mean that it is inaccessible/non-existent.
  • The highest address is determined by number of address bits processor uses:
    • 32-bit processors ⇒ addresses have 32 bits, so it can directly address 2322^{32} bytes (4GB).
    • 64-bit processors ⇒ addresses have 64 bits, so it can directly access 2642^{64} bytes = 16,777,216 TB
      • This is a huge address space, more than what actual systems have. However, tens-hundreds of GB of physical memory is not uncommon.

Alignment

To store the value of a n-bit word in memory, n/8 contiguous bytes are used.

The address of the first byte is the address of the overall word. It's like how a pointer points to the first byte of a series of bytes.

"Natural" Alignment: Typically, an n-byte word must have an address that is an exact multiple of n.

  • E.g., when you have an 8 byte data type, the first byte of that variable (the address) should be a multiple of 8.
  • A char (1 byte) can be placed anywhere.
  • When variables with different alignment requirements are defined, the compiler may insert padding to ensure correct alignment.
    • In C/C++, this is done automatically. In assembly, you do it yourself.
    • When you define fields, the data is allocated in the order that the compiler reads it.

Attempt to load or store an n-byte word at an address that is not a multiple of n is an unaligned access.

  • Best case: The x86 apporach is that the access works, but there is reduced performance.
  • Worst case: runtime exception that kills the program. The OS kernal will forcibly terminate the progam.

C Pointer

A pointer in C is just a memory addresses.

  • The address-of operator (&), when applied to a variable, yields a pointer to the variable (i.e., the address of the first memory byte that is part of the variable's storage).
  • The dereference operator (*), when applied to a pointer value (address), refers to the variable whose storage location is indicated by the address
#include <stdio.h>
#include <stdlib.h>

long g;

int main(void) {
  long* p = malloc(sizeof(long));
  long x;
  int a, b;
  short c, d, e, f;
  scanf("%ld %ld %ld %d %d %hd %hd %hd %hd",

        p, &g, &x, &a, &b, &c, &d, &e, &f);
  long sum = *p + g + x + a + b + c + d + e + f;
  printf("%ld\n", sum);
  printf("%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n",

         p, &g, &x, &a, &b, &c, &d, &e, &f);
  return 0;

}

Remember, in C you can store variables at different addresses

  • global (long g;)
  • heap (long* p = malloc(sizeof(long));)
    • Stored pretty close to global
  • stack (long x;)
    • The addresses of the int variables will differ by 4 while the short variables differ by 2.

Bitwise Operations

Bitwise operations operate on the binary (bit-level) representation of an integer data value

  • Logical operations: and (&), or (|), exclusive or (^), complement (~)
  • Shifts: left shift (<<), right shift (>>)

Bitwise operations are useful because they allow precise manipulations of data values at the level of individual bits. Used when:

  • Selecting arbitrary bits
  • Clearing or setting arbitrary bits

Some important bitwise idioms:

  • Set bit n of variable x to 1: x |= (1 << n);
  • Set bit n of variable x to 0: x &= ~(1 << n);
  • Get just the lowest n bits of variable x: x & ~(~0U << n)