Eric Yu
Eric Yu

Reputation: 23

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

Upvotes: 2

Views: 129

Answers (2)

nneonneo
nneonneo

Reputation: 179392

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().

Upvotes: 4

phoxis
phoxis

Reputation: 61910

I guess the answer is in C89 which allows implicit function declarations.

C89 Section 6.3.2.2

If the expression that precedes the parenthesized argument list in a function call consists solely of an identifier. and if no declaration is visible for this identifier, the identifier is implicitly declared exactly as if. in the innermost block containing the function call. the declaration

extern int identifier ();

appeared.[38]

Footnote

  1. That is. an identifier with block scope declared to have external linkage with type function without parameter information and returning an int If in tact it is not defined as having type "function returning int," the behavior is undefined.

Therefore, essentially because your function int plus (int, int) does not match extern int identifier (), as it does not match the argument list, it is undefined behaviour.

Therefore, if you run it in different systems at different times, you will get different results based on how it is being handled in that corresponding system exactly.

Please note that this is not present in C99 or further standards and implicit function declarations are illegal.

If compile your second code block (in my case with -mno-sse to just use the floating point unit and not SSE instructions) with implicit declaration and disassemble then it looks like:

0000000000401126 <main>:
  401126:   55                      push   %rbp
  401127:   48 89 e5                mov    %rsp,%rbp
  40112a:   48 83 ec 10             sub    $0x10,%rsp
  40112e:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
  401135:   c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
  40113c:   8b 55 f8                mov    -0x8(%rbp),%edx
  40113f:   8b 45 fc                mov    -0x4(%rbp),%eax
  401142:   89 d6                   mov    %edx,%esi
  401144:   89 c7                   mov    %eax,%edi
  401146:   b8 00 00 00 00          mov    $0x0,%eax
  40114b:   e8 18 00 00 00          call   401168 <plus>
  401150:   89 c6                   mov    %eax,%esi
  401152:   bf 10 20 40 00          mov    $0x402010,%edi
  401157:   b8 00 00 00 00          mov    $0x0,%eax
  40115c:   e8 cf fe ff ff          call   401030 <printf@plt>
  401161:   b8 00 00 00 00          mov    $0x0,%eax
  401166:   c9                      leave
  401167:   c3                      ret

Whereas if you include a function prototype to the same code, compile (in my case with -mno-sse to just use the floating point unit and not SSE instructions) and disassemble then it will look like:

0000000000401126 <main>:
  401126:   55                      push   %rbp
  401127:   48 89 e5                mov    %rsp,%rbp
  40112a:   48 83 ec 10             sub    $0x10,%rsp
  40112e:   c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
  401135:   c7 45 f8 01 00 00 00    movl   $0x1,-0x8(%rbp)
  40113c:   db 45 f8                fildl  -0x8(%rbp)
  40113f:   db 45 fc                fildl  -0x4(%rbp)
  401142:   d9 c9                   fxch   %st(1)
  401144:   48 8d 64 24 f8          lea    -0x8(%rsp),%rsp
  401149:   dd 1c 24                fstpl  (%rsp)
  40114c:   48 8d 64 24 f8          lea    -0x8(%rsp),%rsp
  401151:   dd 1c 24                fstpl  (%rsp)
  401154:   e8 1c 00 00 00          call   401175 <plus>
  401159:   48 83 c4 10             add    $0x10,%rsp
  40115d:   89 c6                   mov    %eax,%esi
  40115f:   bf 10 20 40 00          mov    $0x402010,%edi
  401164:   b8 00 00 00 00          mov    $0x0,%eax
  401169:   e8 c2 fe ff ff          call   401030 <printf@plt>
  40116e:   b8 00 00 00 00          mov    $0x0,%eax
  401173:   c9                      leave
  401174:   c3                      ret

Note that when you do not have the function prototype, it does not perform any conversion from the integer to double precision. flid instruction of the floating point unit does this conversion and takes the contents of the variable from general purpose registers to the floating point registers registers.

Therefore, I think that your variable a = 1 bit pattern is directly interpreted as a double.

Upvotes: 0

Related Questions