Menu ▾ ▴

#891 Bad timing on static object construction/destruction when compiling for Linux 64-bit

closed
nobody
FBC64 (1)
compiler
2018-09-26
2018-09-20
No

The fault when compiling for Linux 64-bit is the following:

  • static (or global) objects are constructed after the module constructor execution instead of before,
    and symmetrically:
  • static (or global) objects are destructed before the module destructor execution instead of after.

The order is right when compiling for Linux 32-bit and Windows 32/64-bit.

This following example code should result in:

constructor fired
inside constructor  134     134534956
outside Namespace  134      134534956
Type udt
   As Integer a
   Declare Constructor
End Type
Constructor udt()
   ? "constructor fired"
End Constructor

Static Shared As udt t
Sub init() Constructor
  t.a = 134
  ? "inside constructor ";t.a, @t.a
End Sub

? "outside Namespace ";t.a, @t.a
Sleep

But on 64bit Linux/Ubuntu using FBC64 compiler it results in this:

inside constructor  134     6311024
constructor fired
outside Namespace  0        6311024

Discussed on the FreeBASIC forum here.

Discussion

  • Jeff Marshall

    Jeff Marshall - 2018-09-20

    Can you please check the generated C code for the example given? For example, compile with 'fbc a.bas -R' and post the generated 'a.c' file. I am getting identical C code generated for both fbc-linux64 and fbc-win64 (except for line endings) on both fbc-1.05 and current 1.06 master. I have attached 'lin64-jm.c' for comparison.

     
  • Leslie Ferenci

    Leslie Ferenci - 2018-09-20
    typedef   signed char       int8;
    typedef unsigned char      uint8;
    typedef   signed short      int16;
    typedef unsigned short     uint16;
    typedef   signed int        int32;
    typedef unsigned int       uint32;
    typedef   signed long long  int64;
    typedef unsigned long long uint64;
    typedef struct { char *data; int64 len; int64 size; } FBSTRING;
    typedef int8 boolean;
    struct $3UDT {
        int64 A;
    };
    #define __FB_STATIC_ASSERT( expr ) extern int __$fb_structsizecheck[(expr) ? 1 : -1]
    __FB_STATIC_ASSERT( sizeof( struct $3UDT ) == 8 );
    void fb_PrintLongint( int32, int64, int32 );
    void fb_PrintULongint( int32, uint64, int32 );
    void fb_PrintString( int32, FBSTRING*, int32 );
    FBSTRING* fb_StrAllocTempDescZEx( uint8*, int64 );
    void fb_Init( int32, uint8**, int32 );
    void fb_End( int32 );
    void fb_Sleep( int32 );
    void _ZN3UDTC1Ev( struct $3UDT* );
    void INIT( void ) __attribute__(( constructor ));
    static void _GLOBAL__I( void ) __attribute__(( constructor ));
    static struct $3UDT T$;
    
    void _ZN3UDTC1Ev( struct $3UDT* THIS$1 )
    {
        label$2:;
        __builtin_memset( (int64*)THIS$1, 0, 8ll );
        FBSTRING* vr$2 = fb_StrAllocTempDescZEx( (uint8*)"constructor fired", 17ll );
        fb_PrintString( 0, vr$2, 1 );
        label$3:;
    }
    
    __attribute__(( constructor )) void INIT( void )
    {
        label$4:;
        *(int64*)&T$ = 134ll;
        FBSTRING* vr$0 = fb_StrAllocTempDescZEx( (uint8*)"inside constructor ", 19ll );
        fb_PrintString( 0, vr$0, 0 );
        fb_PrintLongint( 0, *(int64*)&T$, 2 );
        fb_PrintULongint( 0, (uint64)(int64*)&T$, 1 );
        label$5:;
    }
    
    int32 main( int32 __FB_ARGC__$0, char** __FB_ARGV__$0 )
    {
        int32 fb$result$0;
        __builtin_memset( &fb$result$0, 0, 4ll );
        fb_Init( __FB_ARGC__$0, (uint8**)__FB_ARGV__$0, 0 );
        label$0:;
        FBSTRING* vr$1 = fb_StrAllocTempDescZEx( (uint8*)"outside Namespace ", 18ll );
        fb_PrintString( 0, vr$1, 0 );
        fb_PrintLongint( 0, *(int64*)&T$, 2 );
        fb_PrintULongint( 0, (uint64)(int64*)&T$, 1 );
        fb_Sleep( -1 );
        label$1:;
        fb_End( 0 );
        return fb$result$0;
    }
    
    __attribute__(( constructor )) static void _GLOBAL__I( void )
    {
        label$7:;
        _ZN3UDTC1Ev( &T$ );
        label$8:;
    }
    
     
  • Leslie Ferenci

    Leslie Ferenci - 2018-09-20

    The only difference I can see is that the two sleep commands are missing in the windows version.
    What looks like a function declaration on Line 22 :

    void fb_Sleep( int32 );
    

    and on line 58:

        fb_Sleep( -1 );
    

    I doubt that this forms a part of the issue but it is the only difference I can see.

     
  • Jeff Marshall

    Jeff Marshall - 2018-09-20

    Yeah, I deleted the sleep command and forgot. I compile, run, and log output from example programs many times and I don't need sleep commands to pause the output. I think it fair to say we have identical compiler output.

    So, the next step is to determine if it is gcc behaviour, C run-time behaviour, or linker script that is the issue for you.

    As far as I can tell within gcc, there's no guarantee that module constructors must be executed after static variable initializers. Really the only guarantee is that the module constructor is executed before your first executable statement (i.e. main()), though you can control to some degree with the priority attribute. Independantly from module constructors, the static globals should be initialized in the order that they are declared.

    I tried using a custom linker script to change the .ctors sort order, but I couldn't reproduce the behaviour you are seeing on Ubuntu. (you can see what the default linker script is with 'ld --verbose', there should be line in there that looks like )

    The only time I seen this issue is when I was writing the fbcunit test-suite which relies heavily on module constructors and a few globals. The test-suite was failing due to out of order constructors when running on Travis-CI, which I think is Ubuntu Trusty. Because I don't know what system the test suite might run on., and obviously behaviour is different from one system to the next, the only sane thing to do was to change the design of the API so it does not rely on execution order (it still relies heavily on module constructors though).

     
  • fxm (freebasic.net)

    Check if that can be the same problem for initialization of static simple numeric variables (value initialized at the declaration level).

    If that unstable behavior with objects is normal, it should be desirable for such global variables with construction/destruction have their accesses from a module constructor/destructor forbidden by the compiler.

     

    Last edit: fxm (freebasic.net) 2018-09-20
  • Leslie Ferenci

    Leslie Ferenci - 2018-09-20

    The variable must have a constructor in order for the problem to appear.
    Simple (static shared) module level variables are unaffected because there is no constructor order misfire.

    As in this code which prints 44.

    Static Shared As Integer n = 12 
    
    Sub test() Constructor
        '
        n = 44
    End Sub
    
    ? n
    sleep
    
     
  • Jeff Marshall

    Jeff Marshall - 2018-09-20

    Initialization of static simple numeric type variables, that will have a value that can be determined at compile time (for example, default zero, constants, pointers to static objects, pointers to functions, etc), are initialized before any code is executed. These values are part of the EXE image and have an initial value when the EXE is loaded in to memory. In otherwords, trivial static globals where no code is needed to initialize, are guaranteed to be initialized and can be reliably used in all code, including global static object constructors and module constructors.

    I think the fix for this bug is a documented recommendation to avoid non-trivial static globals that depend on other non-trivial static globals for their initialization.

    Even if global access were forbidden, a module constructor could just call some other procedure that accesses the globals. Even if we replaced the current mechanism with one that ensures that the module constructors are called after (or optionally before), the user can still access globals out of order in object constructors.

    I attached an example, showing more cases to avoid.

    EDIT:
    I think we should expect that:
    1) module constructors execute in a deterministic order when used with priority attribute.
    2) static globals execute in the order that they are declared

     

    Last edit: Jeff Marshall 2018-09-20
  • Jeff Marshall

    Jeff Marshall - 2018-09-21

    Did some research and set-up ubuntu-64bit 14.04 with gcc 4.8.4 to test this problem and I was able to replicate the behaviour shown in the opening report.

    The behaviour appears correct for our current implementation.

    Explanation:

    Current implementation:

    • fbc uses a special .ctors/.dtors section of the executable to store a list of procedures that are to be called before and after our main program code.
    • module constructors/destructors expose this capability directly so it is possible for a user to place procedures on the .ctors/.dtors list to be executed before and after the main program code. The order of execution can be controlled only with priority attributes.
    • object constuctor/destructor code is placed in its own (implicit) module constructor, so for objects only, the ctor/dtor code is expected to execute in order of declaration within the module
    • MODULE constructors/destructors are independant of OBJECT constructors/destructors

    The reason for difference in behaviour:

    • behaviour of the special .ctors/.dtors section is dependant on gcc, the linker (and linker script), and the c run time.
    • A significant change to this behaviour was made around 2010 that affected gnu binutils and glibc runtime. Discussion about this is available: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=46770
    • The change adds new .init_array & .fini_array sections that have different behaviour from .ctors/.dtors. In fact .ctors/.dtors behaviour changes as well including reverse order of execution (if no priority attribute given).
    • The change is available from gcc 4.8 and glibc 2.4 onward, but not every distribution has adopted the changed. It is user selectable with the --enable-initfini-array configure option when building gcc from sources.
    • on some distros (for example mingw-w64, slackware-64bit) we have the old behaviour
    • on some distros (for example ubuntu-64bit) we have the new behaviour
    • behaviour is dependant on gcc configure options
     
  • Jeff Marshall

    Jeff Marshall - 2018-09-26
    • status: open --> closed
     
  • Jeff Marshall

    Jeff Marshall - 2018-09-26

    Updated wiki pages to add information on execution order:
    CONSTRUCTOR (Module)
    DESTRUCTOR (Module)

    The module constructors/destructors expose a low-level feature of the linker and run time environment and execution order can vary across platforms.

     

Log in to post a comment.