Search code examples
cgcctype-promotionimplicit-declaration

What does C or GCC do when implicit function declaration exists


#include <stdio.h>

int main(void)
{
  float a, b;
  a = 0.0f, b = 1.0f;
  printf("%d\n", plus(a, b));

  return 0;
}

int plus(double a, double b)
{
  printf("%lf %lf\n", a, b);
  return a + b;
}

gcc .\p1.c --std=c89 -o main

the output is:

0.000000 1.000000
1

But when i changed the type from float to int

#include <stdio.h>

int main(void)
{
  int a, b;
  a = 0, b = 1;
  printf("%d\n", plus(a, b));

  return 0;
}

int plus(double a, double b)
{
  printf("%lf %lf\n", a, b);
  return a + b;
}
gcc .\p1.c --std=c89 -o main

the output is:

32019693379112340.000000 622696491526558860000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.000000
-2147483648

In the first stage, the default argument promotion changes float to double, and everything is right.

In the second, int -> unsigned int, why does it also call plus function i defined, and why the value of argument in the plus function is out of expectation

I tried to use following to simulate the process:

#include <stdio.h>

int main(void)
{
  unsigned int a1, b1;
  a1 = 0u, b1 = 1u;
  double da = a1;
  double db = b1;
  printf("%d\n", plus(da, db));
  return 0;
}

int plus(double a, double b)
{
  printf("%lf %lf\n", a, b);
  return a + b;
}

gcc .\p1.c --std=c89 -o main

the output is:

0.000000 1.000000
1

What confuses me even more is this:

#include <stdio.h>

int main(void)
{
  unsigned int a1, b1;
  a1 = 0u, b1 = 1u;
  double da = a1;
  double db = b1;
  printf("%d\n", plus(da, db));

  int a, b;
  a = 0, b = 1;
  printf("%d\n", plus(a, b));

  double x = 3.0;
  printf("%d\n", add5(x));
  return 0;
}

int plus(double a, double b)
{
  printf("%lf %lf\n", a, b);
  return a + b;

gcc .\p1.c --std=c89 -o main

the output is:

0.000000 1.000000
1
0.000000 0.000000
0

Solution

  • GCC documents what happens in this case.

    From Function Declarations:

    The old-fashioned form of declaration, which is not a prototype, says nothing about the types of arguments or how many they should be:

    rettype function ();
    

    Warning: Arguments passed to a function declared without a prototype are converted with the default argument promotions (see Argument Promotions. Likewise for additional arguments whose types are unspecified.

    […]

    Calling a function that is undeclared has the effect of an creating implicit declaration in the innermost containing scope, equivalent to this:

    extern int function ();
    

    This declaration says that the function returns int but leaves its argument types unspecified. If that does not accurately fit the function, then the program needs an explicit declaration of the function with argument types in order to call it correctly.

    Implicit declarations are deprecated, and a function call that creates one causes a warning.

    So your function call is going to be interpreted as a call to a function that lacks a prototype and returns int. The arguments will be converted according to GCC’s default argument promotions, which essentially will convert integral types narrower than int to int, and also convert float to double.

    So in the first case, you pass (double, double) to plus after conversion, and everything is good.

    In the second case, you pass (int, int) to plus after conversion. Since you’re now passing the wrong argument types to a function, the result is undefined behaviour, and anything can happen. In this case, you get nonsense.

    Why this specific nonsense, though? That depends on the compiler, compiler settings (e.g. higher optimization levels may lead to more or less nonsensical behaviour) and function call ABI.

    Under Microsoft’s standard x64 calling convention, for example, double arguments get passed in the xmm SSE registers (xmm0, xmm1, …) while integral arguments are passed in the general-purpose registers (rcx, rdx, …). So if you’re on Windows running on a 64-bit Intel CPU, or a platform with a similar calling convention, your second case would be passing two integers in rcx, rdx but the function would be reading xmm0 and xmm1 and seeing whatever nonsense happened to be in there.

    In fact, the first argument, 32019693379112340.0, is encoded as the bytes 65736b746f705c43 - which happens to read esktop\C in ASCII. This is very likely to be a fragment of a file path left over in xmm0 by the C startup routine that runs before main().