hjkhuui1999
hjkhuui1999

Reputation: 3

Unexpected behavior when calling implicitly declared function

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

Answers (2)

KamilCuk
KamilCuk

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

sepp2k
sepp2k

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

Related Questions