Skip to Content
Course content

214: Unsafe Code and Pointers

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Most of the time, C# is like a luxury car with an automatic transmission and a dozen safety sensors. You don't have to worry about where the engine is actually putting the fuel; the .NET runtime handles the memory for you. But occasionally, you need to pop the hood, reach into the machinery, and move things manually. That's where "unsafe" code comes in.

I want to show you this by trying to do something simple: I have a buffer of bytes, and I want to XOR every byte with a key to obfuscate some data. Normally, I'd just use a for loop with an index. But let's see what happens when we try to do this using pointers, C-style.

Wait, why is this illegal?

I'll start by trying to get the address of the first element of an array. I'm thinking, "I'll just grab a pointer to the start and increment it." Here is my first attempt:

byte[] data = { 0x10, 0x20, 0x30, 0x40 };
byte* ptr = &data[0]; // I'm trying to get the address of the first byte
*ptr = 0xFF;

The compiler immediately stops me. It doesn't even let me run the code. It tells me that the * operator is only allowed in an "unsafe context." C# isn't saying it's impossible; it's just saying, "Are you sure you want to do this? Because you're about to bypass all the safety guards I've spent years building for you."

The Compiler's Guardrails

To get past that first error, I have to explicitly tell the compiler that I know what I'm doing. I'll wrap the logic in an unsafe block. Let's try again:

unsafe
{
    byte[] data = { 0x10, 0x20, 0x30, 0x40 };
    byte* ptr = &data[0]; 
    *ptr = 0xFF;
}

Still a failure. Now the error is different: "A pointer to a managed type cannot be declared."

Here is the "aha!" moment. The C# Garbage Collector (GC) is an active manager. It doesn't just allocate memory and leave it alone; it moves objects around in memory to defragment the heap. If I grab the address of data[0] and the GC decides to move that array to a different memory slot a millisecond later, my pointer ptr would be pointing at total garbage—or worse, memory being used by another variable. That's how you get those legendary "Heisenbugs" that crash your app randomly.

The Moving Target Problem

To fix this, I need to "pin" the memory. I need to tell the GC, "Hey, don't move this specific array until I'm done with it." We do this using the fixed statement. Watch how the code changes:

byte[] data = { 0x10, 0x20, 0x30, 0x40 };

unsafe
{
    fixed (byte* ptr = data) 
    {
        // Inside this block, 'data' is pinned. The GC won't move it.
        *ptr = 0xFF; 
    }
}

Now it compiles. And notice that I didn't even need &data[0]. When you use fixed on an array, it automatically gives you a pointer to the first element. This is the "unsafe" way to handle buffers: pin it, manipulate it, and then let it go.

Walking the Memory

Now that we can actually access the memory, let's implement that XOR obfuscation I mentioned. Instead of using an index like data[i], I'm going to use pointer arithmetic. I'll start at the beginning and just "walk" the pointer forward.

byte[] data = { 0x10, 0x20, 0x30, 0x40, 0x50 };
byte key = 0xAA;

unsafe
{
    fixed (byte* ptr = data)
    {
        byte* current = ptr; // Copy the starting address
        
        for (int i = 0; i < data.Length; i++)
        {
            // Dereference the pointer, XOR the value, and store it back
            *current = *current ^ key;
            
            // Move the pointer to the next byte in memory
            current++; 
        }
    }
}

Why would I ever do this? In a small array like this, it's overkill. But when you're processing a 100MB image buffer or streaming packets from a network card, avoiding the "bounds checking" that C# normally does on every single array access (checking if i < length) can provide a noticeable performance boost.

Just remember: you've now disabled the safety netting. If you increment current past the end of the array, C# won't throw an IndexOutOfRangeException. It will simply read or write to whatever random memory happens to be next, which is a great way to crash your entire process or create a security vulnerability.




📋 Practical Task

Exercise: High-Performance Byte-Swap Buffer

In high-performance networking, you often need to swap the "endianness" of a buffer (switching the order of bytes). Your task is to create a method that swaps every two adjacent bytes in a byte array using unsafe code.

Requirements:

  • Create a method public unsafe void SwapBytes(byte[] buffer).
  • Use a fixed block to pin the buffer.
  • Use a pointer to iterate through the array.
  • Swap buffer[0] with buffer[1], buffer[2] with buffer[3], and so on.
  • Ensure your loop handles arrays with an odd number of elements without crashing (the last byte should remain untouched).

Test Case:

Input: { 0x01, 0x02, 0x03, 0x04, 0x05 }
Expected Output: { 0x02, 0x01, 0x04, 0x03, 0x05 }

Rating
0 0

There are no comments for now.

to be the first to leave a comment.