Intro

The following is a collection of C code, which may have side effects or behavior that is not immediately apparent or obvious. It is intended to serve as a collection of interesting behaviors that may inspire folks to dig deeper into the how and why of the C standard and compilers, as well as also providing a warning of the complexities of having your code do what you intended.

Floats

What will the answer be?

#include <stdio.h>

int main() {
    float f;

    f = (1/2) + (1/2); 
    printf("ans: %f\n",f); 

    return 0;
}

The (1/2) are both int literals which evaluate to zero. The answer is zero.

Will it overrun?

Will the following code memset beyond the allocated buffer? Assume the compiler will not optimize anything out, and 2’s compliment.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int ac, char **av) {
    if (ac == 1) {
        printf("pass integer arg\n");
        exit(1);
    }

    int len = atoi(av[1]);

    char *buffer = malloc((unsigned) len);
    if (buffer == NULL) {
        printf("malloc() fail\n");
        exit(1);
    }
    memset(buffer, 0x42, len);
}

It depends on the data model in use.

We’re taking a signed int, casting it to unsigned int and then size_t for the malloc, and casting it to size_t for the memcpy.

On ILP32 (32-bit Windows, Linux, OSX) where the width of size_t is equal to the width of unsigned int, no error occurs.

On LP64 (64 bit OSX and Linux) where unsigned int is 32 bit but size_t is 64 bit, the following can occur:

  • The value -1 is passed in by a user, represented as a negative value in the int with 2’s complement as 0xFFFFFFFF
  • In the malloc, the value is cast to an unsigned int, keeping it’s value of 0xFFFFFFFF but becoming unsigned, followed by being cast from unsigned int to size_t, which is an unsigned int, so the value becomes 0x00000000FFFFFFFF.
  • In the memcpy, the value needs to go from 32 bit int to 64 bit (unsigned) size_t. The first thing that occurs is the value is cast from signed 32 bit to signed 64 bit. To represent -1 in 64-bit, a sign extension occurs, resulting in the value 0xFFFFFFFFFFFFFFFF. This value is then cast to unsigned, which does not affect the underlying bit pattern
  • The memcpy size far exceeds the allocated memory, resulting in an attempted out of bounds write

Macros

What is the result of running ./prog? what about ./prog a b c? why?

#include <stdio.h>
#define THING(x) f(x); h(x)

void f(int x) {
    printf("f(%d) called\n",x);
}

void h(int x) {
    printf("h(%d) called\n",x);
}

int main(int argc, char **argv) {
    if (argc > 1)
        THING(1337);

    return 0;
}

The #define line is essentially a match/replace. This can be observed by running gcc -E code.c.

Running ./prog results in h() and f() running.

Running ./prog a b c results in h() running, due to the lack of curly braces around the if body.