Quantcast
Viewing all articles
Browse latest Browse all 17

Implicit Function Declarations Will Bite

Is the following code correct in C?

#include <stdio.h>
int main () {
    int i = DoSomething();    /* does it work? */
    printf("%d\n", i);
    return 0;
}
int DoSomething (int num) {
    return 5;
}

I haven’t done any survey on this, but I dare to guess that most modern day coders would answer the above code would not compile. Reasons are obvious. DoSomething is used without being declared first and the parameter defined in the function signature is missing from the caller. At the very least, when I first saw it, I was convinced that it would not work.

However, it turns out that it is indeed legal1 C code (albeit legacy). Running it against gcc without any option produced no warning message, and executing the resulting binary did indeed produce an output of 5. This is known as the implicit function declaration rule.

So, how does this rule work? How does the compiler handle an unresolved function call? What are the pitfalls?

Before attempting to answer these questions, it should be noted that this feature is only in the C89 standard because of historic reasons. As some may recall, prior to C89 the C language as defined in K&R’s The C Programming Language2 did not support function prototypes. An ordinary C function declaration in K&R C might look like the following.

foobar (num, input)    /* assumed to return int; variable types left out */
    int num;           /* types are specified here */
    char *input;
{
    return 5;
}

During standardization, C obtained the feature of employing function prototypes, as it had been shown quite beneficial from various uses in C++. However, the vast amount of C code written prior to standardization did not have access to using function prototypes, and thus some of the most common used techniques in C today, such as declaring functions in a header file, were not available to programmers at the time. To cope with this and other limitations, K&R C was very lax on various things. For one, declaring a variable without a type preceding it was perfectly legal, the variable would then be simply assumed as an integer type (known as the implicit int rule). Allowing a function to be used without first being declared was among the more prominent compromises, which many still exist in the standard today.

When the compiler sees a name preceding a pair of parenthesis within a function body, it looks up the name within the current scope up and tries to match it with one of the function declarations, if any, defined prior to invoking this function call. If not matched, the compiler then, employing the implicit function declaration rule, goes on assuming that this name belongs to a function that takes in whatever arguments the caller supplies and returns a variable of type int. The compiler performs no type checking on implicitly declared functions. The signature of DoSomething mentioned first in this post is to be inferred by the compiler as the following.

int DoSomething (void);    /* takes in no argument and returns int */

Is this the correct signature? No, but it doesn’t matter. The compiler would implicitly declare the function at the place where DoSomething is first used and, after the translation unit is produced, pass the control to the linker. The linker would then search for the name of the function in the translation unit and in this case match it with the actual DoSomething which is defined right after main. Then, the linker would properly generate the call stack according to the function signature inferred by the compiler, not the actual signature of DoSomething, and link the caller code to the function. Is this the correct behavior? Not quite, but close enough, as in this case it does not actually affect the outcome of the resulting binary.

So what really happened? The compiler finds an unresolved function call, assumes it is of a particular form, and passes it along to the linker. Then, the linker does all in its power to prepare the caller to call that particular form of the function. In our case, it happened to work.

But what if our code looked something like this instead?

#include <stdio.h>
int main () {
    int i = DoSomething();    /* does it work? */
    printf("%d\n", i);
    return 0;
}
int DoSomething (int num) {
    return num;
}

The code would still compile, without any complaints from the compiler. However, unfortunately now the produced binary would no longer give the correct output. The binary would, depending on your platform and the local time, produce 0, 1 or 15033, crash, trash your entire movie collection, or even kill your cat!

So what’s wrong? Well, things starts to go weary as soon as the compiler assumes DoSomething is of a particular form. DoSomething takes in an argument of type int and uses that argument in its body, but the compiler, at the point of the caller, does not yet know that. So, the compiler assumes that DoSomething would take in no arguments and return an int, and then generates code based on that assumption. Note that the compiler does indeed generate correct behaving code with regards to DoSomething itself. The only thing that is wrong is the statement that calls DoSomething. So, DoSomething, under the assumption that the argument can be found from one of the processor registers or somewhere on the stack frame, would simply assign num with the value read from its supposed location, and return it to the caller. However, in this case, that action would be undefined and could trigger anything.

This is serious trouble. Implicitly declaring a function of the wrong signature has little to no symptoms (no warning unless you turn on -Wimplicit-function-declaration in gcc), which means in a large, complex codebase such code would likely go unnoticed for a long, long time and would bite hard when and where it were least expected.

For example, given the following code, what is the expected output?

int foo (UserInput *input, SystemTime time) {
    double result = BeggingForTrouble(input, time);
    if (result == 2.0)
        BurnDownYourHouse();
    else
        KillYourCat();
    return result;
}
double BeggingForTrouble (int input) {
    double result;
    /* do stuff with input... */
    return result;
}

So the caller is attempting to pass in two arguments, one of type pointer to UserInput and the other of type SystemTime, to BeggingForTrouble, which actually takes an int as its sole argument. Would this code compile? Yes, the compiler won’t complain about a thing. Will it burn down your house or kill your cat? Sure, anything can happen; it is undefined behavior.

The analysis can go deeper. Three things are wrong at the place of the caller. First, BeggingForTrouble is assumed to take in whatever arguments the caller supplies. So, storage space for input and time would be allocated when preparing the calling stack for the function. Second, BeggingForTrouble does indeed take an argument, so it’s going to try to grab it from the location in where the argument would reside if the function was invoked properly. But now, since the storage allocated for arguments are completely off from the one BeggingForTrouble would expect, the function could attempt to read from any place; invalid memory space, read-and-clear processor register, or legal location on the stack frame but without the correct data. At last, BeggingForTrouble returns a double, but is assumed to return an int. So, the compiler would generate code that converts a region of memory that is the size of an int into a double and assign to result. So, the likely-is-already-garbage data would thus be altered again. Though, likely it would not matter much at all, as the returned data at this time would be beyond saving anyways.

A quick summary can be stated here as such: at where the implicit function declaration rule is used, the resulting executable would only be correctly behaving if the assumed signature matches exactly both the signature the caller uses and the actual signature of the function.

Any code that leverages the implicit function declaration rule is at high risk of breaking, as the compiler will not conduct any type checking on it. No warning will be issued if there is a mismatch between the assumed signature, the signature used in caller, and the signature of the actual function itself.

So, after coming to understand the implicit function declaration rule, two additional questions must be raised. Is this feature still necessary today? Is it still relevant?

My answer to the first question is No. The C programming language has come a long way since its advent in the early 70s. Language features such as this are obsolete and their presence in the standard are mostly for compatibility reasons with legacy code. The implicit function declaration rule was rendered useless by the introduction of function prototypes into the standard since C89. As a matter of fact, support for implicit function declarations were dropped from the C99 standard.

But is it still relevant? Especially to the newer generation C programmers (such as myself), among whom many were born after C had been standardized, are legacy features such as this still relevant? My answer is Yes. An enormous amount of legacy C code is still in production today, and with the C99 standard not nearly as widely supported as the standard committee had hoped, C89 is still in wide use today. This means, not just maintaining legacy code, even writing new code could accidentally spur the uses of implicit function declarations. And hence the reason to why I am convinced that new C programmers should get themselves familiar with once-widely-used language features such as this. Only can a programmer immediately spot bugs introduced by the implicit function declaration rule, if he/she understands it well enough. Same can be said to other legacy features in C.

[1] Implicit function declaration is no longer legal as of C99.
[2] First edition only. Second edition was updated to describe ANSI C (C89).

Image may be NSFW.
Clik here to view.
Image may be NSFW.
Clik here to view.

Viewing all articles
Browse latest Browse all 17

Trending Articles