Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 6

Pointer Size and System Architecture

Recommended resources

Hello! Let's dive into our next lesson.

In our last session, we mastered the syntax for declaring and initializing pointers, emphasizing the importance of setting them to a valid address or NULL to prevent undefined behavior. We established that a declaration like int *p_value; creates a pointer variable named p_value intended to hold the address of an int.

Today, we shift our focus from the data a pointer points to, to the pointer variable itself. How much memory does a pointer variable like p_value occupy? You might assume a simple answer, but the reality is nuanced and deeply connected to the underlying hardware. Our learning outcome is to explain pointer size and its relationship to system architecture (32-bit vs. 64-bit).

Understanding this is crucial in your work. Whether you're estimating the memory footprint of a data structure on an 8-bit microcontroller or designing a communication gateway on a 64-bit System-on-Chip (SoC), knowing how pointers consume memory is fundamental.

The Basic Principle: Pointers Hold Addresses

A pointer's fundamental job is to store a memory address. Therefore, the size of a pointer variable must be large enough to hold any possible address within the system's address space. This is where the system's architecture comes into play.

  • A 32-bit system typically uses 32-bit addresses. This allows it to access unique memory locations, which is 4,294,967,296 bytes, or 4 Gibibytes (GiB). To store a 32-bit address, a pointer variable needs to be 32 bits, or 4 bytes, wide.
  • A 64-bit system uses 64-bit addresses. This provides a theoretical address space of bytes, or 16 Exbibytes (EiB) — a vast amount of memory. Consequently, to store a 64-bit address, a pointer variable needs to be 64 bits, or 8 bytes, wide.

The following resource explains the architectural basis for this.

64-bit computing

To understand what '64-bit' means in the context of system architecture, please watch this short segment from the video '32-bit vs. 64-bit as Fast As Possible'.

Watch the beginning of the video from 0:00 to 1:04. Focus on the explanation of how the 'bit-ness' of a processor relates to the amount of memory it can address.

This leads to a common rule of thumb: sizeof(pointer) is 4 on a 32-bit machine and 8 on a 64-bit machine. While often true on modern desktops, this is not a C-standard guarantee and can be misleading, especially in the embedded world.


When the Rule of Thumb Breaks Down

The C language standard does not rigidly define the size of a pointer. It's determined by the compiler and the Application Binary Interface (ABI) for a given platform. An ABI is a set of rules that governs how compiled code interfaces with the system, including type sizes and function calling conventions.

This flexibility allows for optimizations and adaptations to specific hardware. Let's explore some scenarios where the "rule of thumb" doesn't apply.

size of pointers and architecture - c++

The following discussion on Stack Overflow by several experienced developers provides excellent insight into the real-world complexities of pointer sizes.

First, read the original question to set the context. Then, read the top-voted answer by Eric Postpischil, which introduces the idea that pointer size is a compiler choice for optimization. Finally, read the answer by Martin Rosenau, which gives concrete examples from microcontroller architectures where pointer sizes can vary within the same program.

As you read, consider these key points:

  1. Memory Optimization: On a 64-bit system, every pointer costs 8 bytes. For software with millions of pointers (like complex data structures in a gateway or ADAS fusion algorithm), this adds up. The x32 ABI for Linux is a prime example of an ABI that uses 32-bit pointers on 64-bit hardware. This saves memory while still benefiting from the other advantages of a 64-bit processor, like more registers.
  2. Segmented Architectures: Historically, systems like 16-bit x86 used segmented memory. They had different kinds of pointers (near, far, huge) with different sizes to access different parts of memory. A near pointer might be 16 bits, while a far pointer would be 32 bits to include a segment selector.
  3. Specialized Microcontrollers: As seen in the STM8A example, an 8-bit automotive microcontroller might use different pointer sizes to address different memory regions (e.g., a smaller pointer for on-chip RAM and a larger one for flash or extended memory).

The crucial takeaway is that all data pointers in a C program are guaranteed to have the same size (e.g., sizeof(int*) == sizeof(char*)), but this size is not necessarily tied to the "bit-ness" of the CPU. Function pointers can sometimes have a different size than data pointers, but we will explore that in a later module.


Data Models: The Link Between Architecture and C Types

So, if it's not a hard rule, how does a compiler for a 64-bit system decide the size of pointers and other types like int and long? This is defined by a data model. For 64-bit systems, two are very common:

  • LP64: long and pointer are 64-bit. int is 32-bit. This is used by most Unix-like operating systems, including Linux and macOS.
  • LLP64: long long and pointer are 64-bit. int and long are 32-bit. This is used by 64-bit Windows.

This distinction is a classic source of portability bugs. Code that assumes sizeof(long) == sizeof(void*) will work on Linux but fail on Windows, where a 64-bit pointer can be truncated when cast to a 32-bit long.

The following resource provides a clear table and explanation of these data models.

64-bit computing

The Wikipedia article on 64-bit computing has an excellent section that clarifies these data models.

Please read the section titled '64-bit data models'. Pay close attention to the table comparing the sizes of types across different models, especially the differences between LP64 and LLP64.


Best Practices for Portable and Safe Code

Given this complexity, how do you write robust code that's compliant with standards like MISRA C and works reliably across different targets?

  1. Never Hardcode Sizes: Don't assume sizeof(void*) is 4 or 8. Always use the sizeof operator to get the actual size on the target platform.

    printf("The size of a pointer on this system is %zu bytes.\n", sizeof(void*));
    
  2. Use static_assert for Assumptions: If your code's correctness truly depends on a specific pointer size, make that assumption explicit. This turns a potential runtime bug into a compile-time error, which is much safer.

    #include <assert.h> // For C11 _Static_assert
    
    // This code will fail to compile if pointers are not 8 bytes.
    static_assert(sizeof(void*) == 8, "This module requires a 64-bit LP64/LLP64 environment.");
    
  3. Use Portable Types for Pointer-Integer Conversions: Never cast a pointer to a standard integer type like int or long. The C standard provides specific types in <stdint.h> for this exact purpose:

    • uintptr_t: An unsigned integer type guaranteed to be large enough to hold a pointer.
    • intptr_t: A signed integer type guaranteed to be large enough to hold a pointer.

    Using these ensures that your code will not truncate an address, regardless of the underlying data model.

Test your understanding!

You are reviewing a piece of code written for an AUTOSAR ECU that runs on a 64-bit Linux-based OS (using the LP64 data model). You find this snippet, which stores a pointer's value in a long for logging purposes:

void log_signal_address(float *p_signal_value) {
    long address_as_integer = (long)p_signal_value;
    // ... code to log the 'address_as_integer' value ...
}

Your team is considering porting this module to a new ECU that runs on a 64-bit version of Windows (which uses the LLP64 data model).

  1. Is this code snippet portable? Why or why not?
  2. How would you rewrite it to be fully portable and safe according to modern C standards?
Show answer
  1. No, this code is not portable.

    • On the original 64-bit Linux (LP64) system, both long and pointers are 64 bits wide, so the cast works without data loss.
    • On a 64-bit Windows (LLP64) system, long is only 32 bits wide, while the pointer is 64 bits. Casting the 64-bit address to a 32-bit long will truncate the upper 32 bits of the address, resulting in an incorrect, garbage value being logged. This is a critical bug.
  2. The correct, portable solution is to use uintptr_t from <stdint.h>:

    #include <stdint.h>
    
    void log_signal_address(float *p_signal_value) {
        // uintptr_t is guaranteed to be wide enough to store a pointer value
        uintptr_t address_as_integer = (uintptr_t)p_signal_value;
        // ... code to log the 'address_as_integer' value ...
    }
    

    This version will work correctly on any C-compliant system, regardless of its data model.


Conclusion

Today we've dismantled the simple assumption about pointer sizes and replaced it with a more accurate, professional understanding.

Key Takeaways:

  • A pointer's size is determined by the platform's ABI to be large enough for the address space, but this can be tailored for performance or memory optimization.
  • While sizeof(pointer) is often 4 bytes on 32-bit systems and 8 bytes on 64-bit systems, this is not a universal rule.
  • 64-bit systems follow different data models (like LP64 and LLP64), which define the sizes of C types and have significant implications for portability.
  • To write robust and portable code, always use sizeof(), check assumptions with static_assert, and use uintptr_t for casting pointers to integers.

In our next lesson, we will complete this foundational module by bringing everything together. We will use our knowledge of memory layout, addresses, dereferencing, and type sizes to trace pointer operations through memory diagrams for simple programs. This will give you a visual and concrete mental model of how pointers work under the hood.