Sunday, March 20, 2016

Pointer casts in C

The C standard has more restrictions on pointers than what was discussed in the previous blog post. This post covers pointer casts.

Pointer casts and alignment

Casting a pointer invokes undefined behavior if the resulting pointer is not correctly aligned. The standard is written in this way in order to support architectures that use different formats for different kinds of pointers, and such architectures do exist — see for example this mail to the GCC development list about a mainframe architecture that was recently commercially supported with a GCC 4.3 port.

The compiler may use this to optimize memory accesses for processors with strict alignment requirements (such as old ARM processors). Consider for example
void foo(void *p)
{
    memset(p, 0, 4);
}
that clears 32 bits of data. This could be generated as a 32-bit store, but the alignment of p is unknown, so the compiler must generate this as four byte operations if it want to inline the memset
mov     r3, #0
strb    r3, [r0, #0]
strb    r3, [r0, #1]
strb    r3, [r0, #2]
strb    r3, [r0, #3]
Consider now
void bar(int *);

void foo(void *p)
{
    memset(p, 0, 4);
    bar(p);
}
The call to bar will convert p to an int* pointer, and this invokes undefined behavior if p is not 32-bit aligned. So the compiler may assume p is aligned, and the memset can now be generated as a 32-bit store
mov     r3, #0
str     r3, [r0, #0]
This example illustrates two things that often cause confusion with regard to undefined behavior
  • The effect of undefined behavior may go back in time — the undefined behavior is invoked when calling bar, but the compiler may use this to optimize code executed before bar in a way that would make it misbehave if p was not correctly aligned.
  • The compiler just use the undefined behavior of misaligned conversion to determine that p must be aligned, which then affects each use of p in the same way as if the alignment had been known by some other means — i.e. the compiler developers do not go out of their way implementing evil algorithms doing obscure transformations back in time based on undefined behavior.

Function pointers

It is not allowed to cast between a function pointer and a pointer to object type (i.e. a "normal" pointer). The reason is that they may be very different on a hardware level, and it may be impossible to represent data pointers as function pointers, or vice versa. One trivial example is when function pointers and data pointers have different width, such as the MS-DOS "medium" memory model that use 32-bit pointers for code but only 16-bit pointers for data.

Casting between integers and pointers

Casting between integers and pointers are implementation defined, so the compiler may choose to handle this in any way it want (although the standard has a footnote saying that the intention is that the mapping between pointers and integers should "be consistent with the addressing structure of the execution environment").

The only thing that is guaranteed to work on any implementation is casting between the integer value 0 and pointers:
An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.
I have read far too many confused discussions about if the value of NULL is guaranteed to be 0. In some sense it is, as (void*)0 == NULL evaluates to true, but the value of (void*)NULL does not need to be 0 when stored in memory — the implementation may for example choose to implement the cast operator as flipping the most significant bit,1 which means that the code
union {
    uintptr_t u;
    void *p;
} u;
u.p = 0;
printf("0x%" PRIxPTR "\n", u.u);
prints 0x80000000 on a 32-bit machine.

One other thing to note is that NULL is defined as being a "null pointer constant", which does not need to be of a pointer type per the quoted text above! This may cause problems when passing NULL to a va_arg function.


1. This is not a completely stupid example. Consider a small embedded processor that has some hardware mapped at address 0. The platform does not have much memory, so 0x80000000 is guaranteed not to be a valid address, and the implementation use this for NULL. There are in general better ways of handling this, but I have seen this done for real hardware...

3 comments:

  1. I had an alignment problem with a MSP430 based wireless node: http://stackoverflow.com/questions/20410495/ti-cc2530-convert-a-float-to-uint8-for-sending-and-then-convert-back-to-floa

    ReplyDelete
  2. I tried this under Ubuntu and could not get gcc to complain..:
    (-Wall -Wextra -Wconversion)
    typedef void (*callback)(int, char);

    void func1(int i, char c) {
    printf("%s %d,%c\n", __func__, i, c);
    }

    int main(int argc, const char *argv[])
    {
    callback cb = func1;
    int i = (int)cb;
    printf("cb=%p\n", cb);
    printf("i=0x%x\n", i);
    int* p = (int*)cb;
    printf("p=%p\n", p);

    ...
    }

    ReplyDelete
    Replies
    1. GCC does in most cases only warn for things that may be problematic in reality, and this works on "all" machined used in reality.

      So this is the same as the "missing" warning for the UB in your printfs -- printf %p is UB unless the argument is void*,
      so you would need to write
      printf("p=%p\n", p);
      as
      printf("p=%p\n", (void*)p);
      But GCC does not warn about this either, even though -Wall is supposed to check that the printf arguments have the appropriate type, as this always work in reality, and users would be annoyed by these useless (but correct!) warnings...

      Delete

Note: Only a member of this blog may post a comment.