Reputation: 3
I am learning C and wrote the following code:
#include <stdio.h>
int main()
{
double a = 2.5;
say(a);
}
void say(int num)
{
printf("%u\n", num);
}
When I compile this program, the compiler gives following warnings:
test.c: In function ‘main’:
test.c:6:2: warning: implicit declaration of function ‘say’ [-Wimplicit-function-declaration]
6 | say(a);
| ^~~
test.c: At top level:
test.c:9:6: warning: conflicting types for ‘say’
9 | void say(int num)
| ^~~
test.c:6:2: note: previous implicit declaration of ‘say’ was here
6 | say(a);
| ^~~
Running the program unexpectedly leads to a 1 being printed. From my limited understanding, because I did not add a function prototype for the compiler, the compiler implicitly creates one from the function call on line 6, expecting a double as a parameter and warns me about this implicit declaration. But later I define the function with a parameter of type int. The compiler gives me two warnings about the type mismatch.
I expect argument coercion, meaning the double will be converted to an integer. But in that case, the output should be 2, and not a 1. What exactly is going on here?
Upvotes: 0
Views: 191
Reputation: 140970
What exactly is going on here?
From the C standard perspective it's undefined behavior.
What exactly is going on here?
I am assuming you are using x86_64
architecture. The psABI-x86_64 standard defines how variables should be passed to functions on that architecture. double
arguments are passed via %xmm0
register, and edi
register is used to pass 1st argument to function.
Your compiler most probably produces:
main:
push rbp
mov rbp, rsp
sub rsp, 16
movsd xmm0, QWORD PTR .LC0[rip]
movsd QWORD PTR [rbp-8], xmm0
mov rax, QWORD PTR [rbp-8]
movq xmm0, rax ; set xmm0 to the value of double
mov eax, 1 ; I guess gcc assumes `int say(double, ...)` for safety
call say
mov eax, 0
leave
ret
say:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], edi ; read %edi
mov eax, DWORD PTR [rbp-4]
mov esi, eax ; pass value in %edi as argument to printf
mov edi, OFFSET FLAT:.LC1
mov eax, 0
call printf
nop
leave
ret
Ie. main
set's %xmm0
to the value of double
. Yet say()
reads from %edi
register that was nowhere set in your code. Because there is some left-over value 1
in edi
, most probably from crt0 or such, you code prints 1
.
@edit The leftover value actually comes from main
arguments. It's int main(int argc, char *argv[])
- because your program is passed no arguments, argc
is set to 1
by startup code, which means that the leftover value in %edi
is 1
.
Well, you can for example "manually" set the %edi
value to some value by calling a function that takes int
before calling say
. The following code prints the value that I put in func()
call.
int func(int a) {}
int main() {
func(50); // set %edi to 50
double a = 2.5;
say(a);
}
void say(int num) {
printf("%u\n", num); // will print '50', the leftover in %edi
}
Upvotes: 6
Reputation: 370122
I expect argument coercion
If you had declared the function properly, that's what you'd get. But as you correctly pointed out, you didn't declare the function and got an implicit declaration that takes a double
as an argument. So when the compiler sees the function call it sees a function call where the argument is a double and the function takes a double. Therefore it has no reason to coerce anything. It just generates the usual code for calling a function with a double as an argument.
What exactly is going on here?
In terms of the C language, it's undefined behaviour and that's it.
In terms of implementation, what's likely happening is that, as I said, the compiler will generate the usual code for calling a function with a double. On a 64-bit x86 architecture using the usual calling conventions, this will mean putting the value 2.5 into the XMM0 register and then calling the function. The function itself will assume that the argument is an int
, so it will read its value from the EDI register (or ECX using Microsoft's calling convention), which is the register used to pass the first integer argument. So the argument is written into one register and then read from a totally different register, so you'll get whatever happened to be in that register.
Still, what exactly would qualify it as [undefined behaviour]?
The fact that you (implicitly) declared the function using one type, but then defined it using another. If the declaration and definition of a function don't match, that causes undefined behaviour.
Upvotes: 3