clinkermingwmsvcrttcc

Why does TinyCC fail to link standard C runtime functions in 32-bit mode but works in 64-bit mode?


If you use TinyCC in its 32-bit mode (-m32) to compile a sample program that uses any CRT function/symbol from msvcrt.dll (such as the snippet provided later in this question), you'll be met with compilation failures:

tcc.exe -std=c11 -Wall -Werror -Wl,-subsystem=console -m32 .\main.c
"tcc: error: undefined symbol '_iob', missing __declspec(dllimport)?"

(This only happens under -m32, whereas -m64 works perfectly fine.)

_iob is not the only "unresolved" symbol, either; printf, freopen, freopen_s, and basically everything from the CRT will fail to link.

Regardless of whether or not you use -lmsvcrt, #pragma comment(lib, "msvcrt"), _declspec(dllimport), attribute ((dllimport)), -static or -shared, or even -impdef on C:\Windows\SysWow64\msvcrt.dll (or earlier versions thereof: msvcrt40.dll), TCC *still *complains.

I've verified with DUMPBIN.exe that both 32- and 64-bit msvcrt.dlls do, in fact, define _iob and other symbols.

By some arcane logic, the following works perfectly fine: tcc.exe -std=c11 -Wall -Werror -Wl,-subsystem=console -m64 .\main.c

main.c

//#pragma comment(lib, "msvcrt")
//__attribute__((dllimport)) extern __declspec(dllimport) FILE _iob[];

#include <windows.h>

// _MSVCRT_ being defined will cause MinGW's stdio.h to use _iob as
// opposed to _imp___iob; only the former is defined in msvcrt.dll.
// However, even though _iob is exported by both the 32- and 64-bit
// versions of said dll, TinyCC still fails to find _iob in the former.
#define _MSVCRT_
#include <stdio.h>

void main() {
    // AllocConsole() and basically everything from kernel32.dll or
    // user32.dll work perfectly fine, both in -m32 and -m64; it's
    // only msvcrt.dll that causes issues with TinyCC.
    AllocConsole();

    // Any CRT function (e.g., freopen, freopen_s, printf, etc.)
    // fail to get linked properly ONLY in -m32; -m64 is fine.
    // Even if I change the -I and -L paths to C:/Windows/SysWow64
    // and/or use tcc.exe -impdef to create .def files from them,
    // TCC still fails in finding _iob and other symbols.
    // Also, using #pragma comment(lib, "msvcrt") or -lmsvcrt
    // doesn't help at all.  Even if you do get TCC to somehow
    // stop complaining about missing symbols, it'd just include
    // a blank IAT.printf or IAT.freopen, causing segfaults.
    freopen("CONOUT$", "w", stdout);
    printf("This only compiles (and prints) under TCC in 64-bit mode.");
}

As mentioned earlier, this error in -m32 happens regardless of other switches like -std, -shared, -static, -lmsvcrt, -subsyetem, etc. So, at this point, I'm starting to think this might really be a bug with TinyCC 0.9.27 (Win32 & Win64 builds) itself.


Solution

  • After nearly 16 hours of debugging, I've found the culprit: _iob and _imp___iob should have been declared with either __attribute__((dllimport)) or __declspec(dllimport).

    To be more specific, lines 93 to 106 of <stdio.h> in TCC's ./include directory are currently written as follows:

    <stdio.h>:93-106

    #ifndef _STDIO_DEFINED
    #  ifdef _WIN64
    _CRTIMP FILE *__cdecl __iob_func(void);
    #  else
    #    ifdef _MSVCRT_
    extern FILE _iob[];     /* A pointer to an array of FILE */
    #      define __iob_func()    (_iob)
    #    else
    extern FILE (*_imp___iob)[];    /* A pointer to an array of FILE */
    #      define __iob_func()    (*_imp___iob)
    #      define _iob __iob_func()
    #    endif
    #  endif
    #endif
    

    To fix them so that it compiles under -m32 just as fine as it does under -m64, you need to change them to the following:

    <stdio.h>:93-106

    #ifndef _STDIO_DEFINED
    #  ifdef _WIN64
    _CRTIMP FILE *__cdecl __iob_func(void);
    #  else
    #    ifdef _MSVCRT_
    __attribute__((dllimport)) extern FILE _iob[];
    #      define __iob_func()    (_iob)
    #    else
    __attribute__((dllimport)) extern FILE (*_imp___iob)[];
    #      define __iob_func()    (*_imp___iob)
    #      define _iob __iob_func()
    #    endif
    #  endif
    #endif
    

    (NOTE: As I said earlier, both __attribute__((dllimport)) and __declspec(dllimport) work here. Either of them fix it for both _MSVCRT_ and non-_MSVCRT_ headers.)

    A few additional details on why this fix is even useful in the first place:

    I still find it strange that such a "fix" was required, because TCC works just fine under -m64 anyway. I'm not yet sure if this is a TCC-specific patch or if it actually affects any compiler that uses <stdio.h> from MinGW; I'll follow up the rest in TinyCC's mailing lists.

    (Also, I did compile the latest mob branch from 10/24/2024 using build tcc.bat -x -c cl but this <stdio.h> bug affected it the same way it affected 0.9.27.)