Skip to main content Link Menu Expand (external link) Document Search Copy Copied

C/C++ Recitation

In this class, we’ll be heavily relying on the low-level C and C++ languages. Using C and C++ allows us to manipulate the hardware at a very fine granularity, allowing us to pull off very powerful microarchitectural attacks in later labs. This is in contrast with higher-level languages, such as python, which make reasoning about the exact instructions executed by a program difficult.

In this recitation we’ll primarily focus on learning how to write code in C, since C++ is a superset of C’s syntax (with limited exceptions).

Crash Course

We’ll first walk through a quick code example to get you familiar with the format of C.

#include <stdio.h>
#define MAGIC_NUM 5

void sayHello(int helloNum) {
  printf("Hello World! The addition sum is: %d\n", helloNum);
}

int main(void) {
  int result = 1 + MAGIC_NUM;
  sayHello(result);
  return 0;
}

A few things to note about this code:

  • #include and #define are pre-processor directives, which are resolved prior to compilation.
    • #include tells the pre-processor to include the contents of the listed file (e.g. stdio.h) when compiling this file.
    • #define tells the pre-processor to replace all instances of the listed term with the following value (e.g. MAGIC_NUM is replaced with 5 everywhere in the code prior to compilation)
  • C is a strongly typed language, hence variables must be declared with their type (e.g. int).
  • printf prints a message to console. You can include variables in your printout by including a format specifier such as %d for integers.
    • Warning: printf is fairly heavy duty, so be cautious when calling it when measuring microarchitectural behaviours!

Memory Management

An extremely powerful tool used in C programs to interact with memory are pointers and arrays.

Pointers

Pointers are variables that contain a memory address, rather than a value directly.

void example_method() {
  int a = 1234;
  int *b = &a;

  *b = 9876;
}

In this code, we assign the integer a to store 1234, and the pointer b to store the address of a. As shown, we can find the address of a variable a by writing &a (& is called the address-of operator).

We can also read or write the value pointed to by a pointer by dereferencing it. As shown, we write to the value pointed to by b (i.e. a) by writing *b = 9876.

Arrays

C arrays are much like arrays in other languages!

void example_method() {
  int a[2];

  a[0] = 0;
  a[1] = 1;

  printf("The first element of a is: %d\n", a[0]);
  printf("The second element of a is: %d\n", a[1]);

  printf("The first element of a is: %d\n", *(a));
  printf("The second element of a is: %d\n", *(a + sizeof(int)));  
}

Observe that the array variable (i.e. a) is just a pointer to the first element in the array (remember that C arrays start at 0). Specifically, ptr[index] treats ptr as an array and retrieves the entry at index index. This is the same as *(ptr+index * sizeof(type)). If ptr points to an 8-bit character, then sizeof(type) is 1 byte.

Strings

In memory, a string of “Hi!” looks like this

Address 0x0 0x1 0x2 0x3 0x4 0x5
Data ‘H’ ‘i’ ’!’ ‘\0’ - -

In C, strings are just character arrays in disguise!

void example_method() {

  char string[6] = "Hi!";

  printf("String stored: %s\n", string);

  // Print it character-by-character
  int i = 0;
  while(string[i] != '\0') {
    printf("Character %d of string: %c\n", i, string[i]);
    i++;
  }

}

Since a string is encoded as a fixed length array (e.g. 6 characters in our case), we need a way to indicate the end of the text. We use the null terminator (e.g. 0x00) to denote the end of the string (this is automatically inserted for you when you write “Hi!”).

The above character-by-character code is actually quite dangerous! Think about what happens if you accidentally forget the null terminator here.

Dynamically Allocated Memory

You may have noticed that we were statically sizing our data structures in the previous sections. Sometimes you may want to allocate a dynamic amount of memory at runtime. This can be done using malloc() and free():

void example_method() {

  int *array = malloc(2*sizeof(int));

  if (array == NULL) {
    printf("malloc failed! \n");
    return -1;
  }

  array[0] = 1;
  array[1] = 2;

  free(array);

}

On success, malloc returns a pointer to the beginning of some newly allocated memory. To avoid a memory leak, make sure to deallocate malloc’d memory regions after you’re done with them using free().

Casting

Occasionally you may be required to change the data type of a variable. For instance, you may want to access a specific address at a given 64-bit integer value. To do this, you can cast a variable into another as such:

void example_method() {
    uint64_t x = 0x12345678; //x is a 64-bit unsigned integer
    uint8_t *y = (uint8_t *) x; // cast x to a pointer (treat it as an address)
    uint8_t z = *y; // access the data pointed to by pointer y.
    // z now contains the data from address 0x12345678
}

CTF

With knowledge of these constructs, let’s try some challenges!

Advanced Topics

For later labs in the course, some advanced knowledge of C/C++ constructs may be required. We list some notes below for your reference.

C++ Maps (std::map)

C++ is a slight departure from C – it is an object-oriented programming language which shares a majority of its syntax with C, and is backwards compatible with C (apart from some minor exceptions).

C++ maps are very similar to Python dictionaries, with key-value pairs being assigned in a very similar fashion. A full description of this data structure can be found here.

An example of using C++ maps is shown below.

std::map<uint64_t, uint64_t> cpp_map;
uint64_t key = 0xDEAD;
uint64_t value = 0xBEEF;

// Add or modify a key-value pair
cpp_map[key] = value;

// Retrieve a value for a key
uint64_t key2 = 0xBAAD;
uint64_t value2 = cpp_map[key2]
if (value2 == 0){
    assert("Key does not exist!\n");
}