Function call with more parameters than expected
Asked Answered
J

2

5

I was reviewing some code and I came across something similar to this.

File foo.c:

int bar(int param1)
{
    return param1*param1;
}

File main.c:

#include <stdio.h>

int bar(int param1, int unusedParam);

int main (void)
{
    int param = 2, unused = 0;
    printf("%d\n", bar(param, unused));
}

Running gcc main.c foo.c -Wall --pedantic -O0 it compiles, links and works properly without throwing a single warning in the process. Why is that?

Thanks!

Jaeger answered 22/11, 2018 at 15:25 Comment(15)
Because you didn't include a header file foo.hcontaining the function prototype int bar(int param1);.Diaz
This may have to do with linking to DLLs or shared librariesDamaraland
I get that it may work properly, but I don't know why the linker does not find this inconsistency and warn you about it.Jaeger
Put the content of foo.c before main in main.c and compile that, you'll get a at least a warning, but more likely an error.Diaz
does it print 4 or 0Malone
Actually is your question "why is there no single warning" or "why does it work properly" or both?Diaz
Actually there is no reason why the compiler doesn't warn -- it has all the information there since both source files are compiled in one go. (That's important when you try to inline functions across TUs.) Try one of the "whole program optimization" (or link time optimization) options in gcc and see if it sees the mismatch then. Perhaps throw in a "-O3" so that the compiler bothers to look.Melvinamelvyn
Actually the question would be "Is it impossible for the toolchain to detect this and warn you about it?"Jaeger
@PeterA.Schneider are you sure that compiling in one go (gcc main.c foo.c ...) is not actually the same thing as compiling each file separately (gcc -c main.c, gcc -c foo.c, gcc foo.o main.o?Diaz
I think I'm starting to get it. Both files can compile independently so main.c thinks that the function has two parameters and pushes them onto the stack. The linker does not really care about parameters just solving symbols. Am I right?Jaeger
@Jaeger correct. Modern compilers can annotate object files though so that a smart linker is able to extract functions for inlining etc. See gcc.gnu.org/wiki/LinkTimeOptimization (which is ancient -- no idea what's the current state). -- Edit: gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html (look for -flto) seems to be more elaborate.Melvinamelvyn
@Diaz I remember reading it somewhere. The gcc docs seem to imply that it's just an easier way to list them all instead of separate compilation and then an explicit link stage. Ah, gcc.gnu.org/onlinedocs/gcc-3.4.5/gcc/Optimize-Options.html: -funit-at-a-time works across multiple files (without -flto, that is, wihtout a smart linker driver!) if you provide them.Melvinamelvyn
@Diaz ... Looks as if -funit-at-a-time is on by default in current gccs. And if you provide all sources, you can simply say "-fwhole-program" to optimize without -flto.Melvinamelvyn
@Jaeger For the most part, yes, unless the size of the argument list is cooked into the function name (see the edit to my answer)Shrunken
Interesting, running with -flto actually throws some warnings: main.c:3:5: warning: type of ‘bar’ does not match original declaration [-Wlto-type-mismatch] [...]Jaeger
S
8

This really depends on the calling convention and architecture. For example, with cdecl on x86, where arguments are pushed right to left and the caller restores the stack, the presence of an additional parameter is transparent to the function bar:

push    11
push    10
call    _bar
add     esp, 8

bar will only "see" the 10, and will function as expected with that parameter, returning 100. The stack is restored afterwards so there is no misalignment in main either; if you had just passed the 10 it would have added 4 to esp instead.

This is also true of the x64 calling conventions for both MSVC on Windows and the System V ABI, where the first few1 integral arguments are passed in registers; the second argument will be populated in its designated register by the call in main, but not even looked at by bar.

If, however, you tried to use an alternate calling convention where the callee is responsible for cleaning up the stack, you would run into trouble either at the build stage or (worse) at runtime. stdcall, for example, decorates the function name with the number of bytes used by the argument list, so I'm not even able to link the final executable by changing bar to use stdcall instead:

error LNK2019: unresolved external symbol _bar@8 referenced in function _main

This is because bar now has the signature _bar@4 in its object file, as it should.

This gets interesting if you use the obsolete calling convention pascal, where parameters are pushed left-to-right:

push 10
push 11
call _bar

Now bar returns 121, not 100, like you expected. That is, if the function successfully returns, which it won't, since the callee was supposed to clean up the stack but failed due to the extra parameter, trashing the return address.

1: 4 for MSVC on Windows; 6 on System V ABI

Shrunken answered 22/11, 2018 at 15:33 Comment(4)
Only four arguments are passed in registers in msvc.Jamaaljamaica
@Jamaaljamaica I did mention "the first few" since I wasn't sure of the exact amount. Updated my post with the exact numbers for both PE and SysV ABIShrunken
@GovindParmar you are saying that on x86 in this case second variable is signored.Maid
@Gox If the calling convention is cdecl, then yes.Shrunken
D
1

Normally you'd have this file structure:

foo.c

#include "foo.h"

int bar(int param1)
{
    return param1*param1;
}

foo.h

int bar(int param1);

main.c

#include <stdio.h>
#include "foo.h"

int main (void)
{
    int param = 2, unused = 0;
    printf("%d\n", bar(param, unused));
}

Now you'll get a compilation error as soon as you use bar with non matching parameters.

Diaz answered 22/11, 2018 at 15:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.