Reinterpreting Memory

Learn more about the inner workings of memory by learning to reinterpret data types.

Introduction

We know by now that memory is a linear sequence of bits or bytes.

We also know the following:

  • When we create a variable on the stack, say int x, we allocate a block of memory of 4 bytes (the size of int). We change the stack pointer esp register by subtracting 4 (pushing the stack).
  • When we create a variable on the heap, we use malloc or calloc. These functions only need to know the size of the memory block to allocate it.

From the points mentioned above, we can deduce that no matter how we allocate data, the only important thing is how many bytes we need for that particular data type.

The memory itself has no concept of data types. It’s just a sequence of 0 and 1. The memory doesn’t care if 0 and 1 come from an int or a double. We choose how to interpret the data depending on which data type we use in our code.

For example:

malloc(sizeof(int));
malloc(4 * sizeof(char));

Both malloc calls will allocate a memory block of 4 bytes. It’s up to us if we want to interpret these 4 bytes as one integer or as an array of four characters. The following interpretations are valid:

int* p1 = malloc(sizeof(int));
char* p2 = malloc(4 * sizeof(char));

We can even mix them:

char* p1 = malloc(sizeof(int));
int* p2 = malloc(4 * sizeof(char));

Sure, doing so is confusing, and it isn’t something we should do in practice. But the important thing is that it works. Both sizeof(int) and 4 * sizeof(char) yield a block of 4 bytes, and we can choose how to interpret it, either as an integer or as an array of four characters.

See the following memory drawing:

We can exploit the fact that the memory doesn’t know anything about data types to perform clever memory manipulation or memory reinterpretation.

The power of char*

Since the size of char is 1 byte, we can use a char* to traverse any other data type byte by byte.

Note that this reinterpretation is well-defined in C and doesn’t cause undefined behavior. See the following quote from the standard:

Remember: When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object. Successive increments of the result, up to the size of the object, yield pointers to the remaining bytes of the object.

We can try to use a char to traverse the bytes of an integer individually. We create an integer and set its value to 0xDEADBEEF (line 5). It’s a hexadecimal value, with the equivalent binary value 11011110101011011011111011101111.

Then, let’s cast a pointer from x to unsigned char to obtain an unsigned char pointer, pointing to the memory block of the integer (line 7).

Note: We use unsigned char and not char to avoid issues with the sign of the values. Otherwise, when printing, we may get a negative value. The negative value will be the two’s complement of the positive (and expected) value. The two’s complement is how computers store negative numbers. In short, it may try to interpret our positive number as a negative unless we make it clear that we’re dealing with unsigned values (which can only be positive).

We then can access ptr[0], ptr[1], ptr[2] and ptr[3], using the for loop (line 8). Since the type of ptr is unsigned char, every incrementation will add 1 (the size of unsigned char) to the address. If we used an int pointer, it would add 4 bytes, so we wouldn’t be able to access the individual bytes.

Let’s run the code and see the values:

C
#include <stdio.h>
int main()
{
int x = 0xDEADBEEF;
unsigned char* ptr = (unsigned char*)&x;
for(int i = 0; i < sizeof(int); i++)
{
printf("byte %d value %u\n", i, ptr[i]);
}
return 0;
}

Output:

byte 0 value 239
byte 1 value 190
byte 2 value 173
byte 3 value 222

Recall that 0xDEADBEEF in binary is 11011110101011011011111011101111. These 32 bits get grouped into 4 bytes. So, let’s separate them.

  • The first byte contains the first 8 bits, 11011110.
  • The second byte contains the following 8 bits, 10101101.
  • The third byte is 10111110.
  • The last byte is 11101111.

Now, let’s use the calculator and convert the binary values to decimal.

  • 11011110 = 222
  • 10101101 = 173
  • 10111110 = 190
  • 11101111 = 239

We can see that the output of the code is correct. The only difference is that we got the values in the reversed order. It’s unimportant for now, but we’ll soon find out why.

The number 0xDEADBEEF gets constructed in memory by interpreting together the 4 bytes mentioned above.

To summarize, we used an unsigned char pointer with the size of 1 byte, to iterate over the 4 bytes of an integer. We can do this because we know that when incrementing a char pointer, the address increases by 1.

Implementing memcpy

Recall that memcpy is a function from the C standard library that copies memory from one block to another.

The prototype is as follows:

void* memcpy(void* destination, const void* source, size_t num);

It must work for every data type, so it has to accept the arguments as void*.

Let’s write our own memcpy implementation. Since the size of char is 1 byte, any other data type has a size multiple of 1.

Then, no matter the type of destination and source, we can iterate over their memory byte by byte, as we did for the integer above. To achieve this:

  • Convert both pointers to char* (lines 6 and 7).
  • Then, copy num bytes from sourcePtr to destPtr (line 11).

The function will work correctly as long as the user correctly passes the number of bytes as an argument. For example, for an integer array of five elements, we must pass 5 * sizeof(int), the size of the array in bytes. If we pass just 5, the function will attempt to copy 5 bytes, which in turn means we copy the first array element (4 bytes) and 1 byte from the second array element. Obviously, this isn’t correct.

C
#include <stdio.h>
#include <string.h>
void* my_memcpy(void* destination, const void* source, size_t num)
{
char* destPtr = (char*)destination;
const char* sourcePtr = (char*)source;
for(int i = 0; i < num; i++)
{
*(destPtr + i) = *(sourcePtr + i);
}
return destination;
}
int main()
{
//Copy an integer
int x = 125;
int xCopy;
my_memcpy(&xCopy, &x, sizeof(int));
printf("xCopy = %d\n", xCopy);
//Copy an array of integers
int arr[5] = {0, 1, 2, 3, 4};
int arrCopy[5];
my_memcpy(arrCopy, arr, sizeof(int) * 5);
for(int i = 0; i < 5; i++)
{
printf("arrCopy[%d] = %d\n", i, arrCopy[i]);
}
return 0;
}

The output of the code is as follows:

xCopy = 125
arrCopy[0] = 0
arrCopy[1] = 1
arrCopy[2] = 2
arrCopy[3] = 3
arrCopy[4] = 4

We first make a copy of an integer, and we see that xCopy is 125, which is correct. We then copy an array of five integers. The output is again correct.

In the case of the integer, the memcpy code will iterate over 4 bytes and copy them to the destination buffer. In the case of the array, the code will iterate over 20 bytes and copy them to the destination buffer. As we can see, memcpy has no notion of the structure of the memory. The memory may be one single variable or an array of multiple elements, but memcpy doesn’t care. It simply treats it as a sequence of bytes. In other words, we reinterpreted the memory as a sequence of char (bytes).