Search code examples
ccobolgnucobol

Generic function from dlsym with dereferenced float


The GnuCOBOL compiler supports dynamic CALL by using dynamic symbol lookup, but the MCVE here is strictly C, and is a little less than minimal to demonstrate (what I think) is both 4 and 8 byte sizes working.

This is AMD-64, so sizeof *float is not equal to sizeof float.

The problem only manifests when dereferencing the float when called by generic (unsignatured in this case) function pointer from dlsym lookup.

// gcc -Wl,--export-dynamic -g -o byval byval.c -ldl

#include <stdio.h>
#include <dlfcn.h>

// hack in a 1 / 3 float 0.303030, 1050355402 as 32bit int
unsigned char field[4] = {0xca, 0x26, 0x9b, 0x3e};

// and a 1 / 6 double, 0.151515
unsigned char dtype[8] = {0x64, 0x93, 0x4d, 0x36, 0xd9, 0x64, 0xc3, 0x3f};

int aroutine(float);

int
main(int argc, char** argv)
{

    float* fp = (float*)field;
    double g;

    void* this;
    int (*calling)();

    int result;

    /* call the routines using generic data treated as float */
    float f = *fp;
    printf("Initial: %f \n", f);

    printf("\nBy signature\n");
    result = aroutine(*(float*)(field));

    this = dlopen("", RTLD_LAZY);

    printf("\nGeneric: (busted, stack gets 0x40000000)\n");
    calling = dlsym(this, "aroutine");
    result = calling(*(float*)(field));

    printf("\nBy reference: (works when callee dereferences)\n");
    calling = dlsym(this, "proutine");
    result = calling((float*)(field));

    printf("\nGeneric double (works):\n");
    calling = dlsym(this, "droutine");
    result = calling(*(double*)(dtype));

    printf("\nGeneric int and double (works):\n");
    calling = dlsym(this, "idroutine");
    result = calling(*(int*)(field),*(double*)(dtype));

    printf("\nGeneric int and float (busted) and int:\n");
    calling = dlsym(this, "ifiroutine");
    result = calling(*(int*)(field), *(float*)(field), *(int*)(field));

    return 0;
}

int aroutine(float f) {
    printf("aroutine: %f\n", f);
    return 0;
}

int proutine(float *fp) {
    printf("proutine: %f\n", *fp);
    return 0;
}

int droutine(double g) {
    printf("droutine: %g\n", g);
    return 0;
}

int idroutine(int i, double g) {
    printf("idroutine: %d %g\n", i, g);
    return 0;
}

int ifiroutine(int i, float f, int j) {
    printf("ifiroutine: %d %f %d\n", i, f, j);
    return 0;
}

with a run of

prompt$ gcc -Wl,--export-dynamic -g -o mcve stackoverflow.c -ldl
prompt$ ./mcve
Initial: 0.303030

By signature
aroutine: 0.303030

Generic: (busted, stack gets 0x40000000)
aroutine: 2.000000

By reference: (works when callee dereferences)
proutine: 0.303030

Generic double (works):
droutine: 0.151515

Generic int and double (works):
idroutine: 1050355402 0.151515

Generic int and float (busted) and int:
ifiroutine: 1050355402 2.000000 1050355402

I think I need a little edumacating on how the 64bit ABI handles unsignatured calls when dereferencing float data.

The COBOL tag is included as this is breaking GnuCOBOL (which generates C intermediates) when using FLOAT-SHORT (C float) with CALL BY VALUE, whereas FLOAT-LONG (C double) CALL BY VALUE works, as do 32bit integers.

By the way, I'm pretty sure this is not a bug in gcc, as tcc tcc -rdynamic -g -o tccmcve stackoverflow.c -ldl manifests the same output, the float dereference seems borked, so I'm leaning to (and hoping) this is a fixable thing, given proper syntax hints to the compiler, or compile time options.


Solution

  • C99 and C11 6.5.2.2p6 states

    If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions.

    and 6.5.2.2p7 continues with

    If the expression that denotes the called function has a type that does include a prototype, the arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter. The default argument promotions are performed on trailing arguments.

    So, when you call a function that has a float parameter, the prototype ("signature" as you call it) tells the compiler to not convert the float to double. (Similarly for integer types smaller than int.)

    The fix is, obviously, to use the prototypes ("signatures"). You have to, if you want to pass a float, char, or short, because without the prototype, they're promoted to double, int, and int, respectively.

    That should really not be a burden, however. If you have some prototypeless function pointer, say

    int (*generic)() = dlsym(self, "aroutine");
    

    and you want to call a function whose prototype is, say, void foo(float, int, double), you can always cast the function pointer:

    ((void (*)(float, int, double))generic)(f_arg, i_arg, d_arg);
    

    although using a temporary function pointer with the correct prototype is certainly easier to read and maintain:

    {
        void (*call_foo)(float, int, double) = (void *)generic;
        call_foo(f_arg, i_arg, d_arg);
    }
    

    See the POSIX dlsym() documentation for reference. (The *(void **)(&funcptr) idiom recommended in older versions is no longer recommended; it was silly anyway.)