Skip to content

Instantly share code, notes, and snippets.

@MisterDA
Last active December 5, 2023 16:39
Show Gist options
  • Save MisterDA/418811298f90303ee1791b89b1e47324 to your computer and use it in GitHub Desktop.
Save MisterDA/418811298f90303ee1791b89b1e47324 to your computer and use it in GitHub Desktop.
Test TLS callbacks on Windows
*.obj
*.lib
*.exe
*.dll
*.o
*.a

Registering a TLS callback in C

  1. The Thread Local Storage CRT implementation is located at C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.39.33218/crt/src/vcruntime/tlssup.cpp.

  2. The TLS callback symbol must be registered in the CRT$XL? section, where ? is an arbitrary letter excluding A and Z. We chose CRT$XLF for our symbol.

  3. const_seg and data_seg are specific to MSVC and used to register symbols in specific sections of the object file.

  1. On x64, using data_seg results in these warnings, so const_seg must be used.

    LIBCMT.lib(exe_main.obj) : warning LNK4078: multiple '.CRT' sections found with different attributes (40400040)
    LIBCMT.lib(initializers.obj) : warning LNK4254: section '.CRT' (C0000040) merged into '.rdata' (40000040) with different attributes
    
  2. If const_seg must be used, then the variable must be const.

  3. I couldn't find a good reason not to use const_seg instead of data_seg on x86. All the examples use data_seg on x86. Historical artifact? C++ quirks?

  4. The attribute __declspec(allocate("section-name")) seems unnecessary when using data_seg or const_seg, but only needed when declaring a new section with the section pragma.

    A warning is raised on x64 and the callback doesn't fire if the shared attribute of section is used.

  5. In C++, const variables don't have external linkage. However, one cannot define an extern const variable, which is why the snippet is common:

    typedef void (*proc)(void *);
    void f(void *arg) { ... }
    extern const proc _f;
    const proc _f = &f;

    At the same time, in C++, the exported symbol should use C mangling rules, so this becomes:

    extern "C" const proc _f;

    Does the symbol retains external linkage with extern "C"?

    IIUC external linkage can be forced with (see /INCLUDE (Force Symbol References)):

    #pragma comment (linker, "/INCLUDE:_f")

    but for the case of C, comparing the output of dumpbin /ALL with and without this directive doesn't seem to show a relevant difference. It's possible that the directive is still needed in case of whole program optimization.

  6. The loader and the linker need to know the symbol describing the callback array. It is named _tls_used, but symbols are prefixed with an additional underscore on x86, and it becomes __tls_used. We could always re-declare it with extern, but that could be bad if its type changes. We can use a linker directive instead:

    #if defined _M_IX86
    #pragma comment (linker, "/INCLUDE:__tls_used")
    #elif defined _M_X64
    #pragma comment (linker, "/INCLUDE:_tls_used")
    #endif
  7. It seems that GCC only needs the section on the TLS callback:

    #if defined __GNUC__
    __attribute__((__section__(".CRT$XLF")))
    #endif

    See section variable attribute.

  8. Needs testing on arm and arm64 (don't forget to make clean between tests):

    # Do that in x86, x86_64. arm, arm64 envs
    make CC=cl && ./main.exe
    make CC=clang-cl && ./main.exe
    
    make main-gcc.exe CC=x86_64-w64-mingw32-gcc AR=x86_64-w64-mingw32-ar && ./main-gcc.exe
    make main-gcc.exe CC=i686-w64-mingw32-gcc AR=i686-w64-mingw32-ar && ./main-gcc.exe
  9. References

#include <windows.h>
#include <stdio.h>
static DWORD callback_reason = MAXDWORD;
static void NTAPI __stdcall
tls_callback(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
UNREFERENCED_PARAMETER(DllHandle);
UNREFERENCED_PARAMETER(Reserved);
callback_reason = Reason;
}
#if defined(_MSC_VER)
/* Force a reference to _tls_used to make the linker create the TLS
* directory if it's not already there. (e.g. if __declspec(thread)
* is not used).
* Force a reference to __xl_f to prevent whole program optimization
* from discarding the variable. */
/* On x86, symbols are prefixed with an underscore. */
#if defined(_M_IX86)
# pragma comment(linker, "/INCLUDE:__tls_used")
# pragma comment(linker, "/INCLUDE:___xl_f")
#elif defined(_M_ARM) || defined(_M_ARM64) || defined(_M_X64)
# pragma comment(linker, "/INCLUDE:_tls_used")
# pragma comment(linker, "/INCLUDE:__xl_f")
#endif
/* .CRT$XLA to .CRT$XLZ is an array of PIMAGE_TLS_CALLBACK
* pointers. Pick an arbitrary location for our callback.
*
* See VC\...\crt\src\vcruntime\tlssup.cpp for reference. */
/* .CRT section is merged with .rdata on x64 so it must be constant
* data. Also works on all other architectures. */
#define USE_CONST_SEG 0
#if USE_CONST_SEG
# pragma const_seg(push, old_seg)
# pragma const_seg(".CRT$XLF")
#else
# pragma section(".CRT$XLF", long, read)
#endif
#endif
extern const PIMAGE_TLS_CALLBACK
#if defined __GNUC__
__attribute__((__section__(".CRT$XLF")))
#elif !USE_CONST_SEG && defined _MSC_VER
__declspec(allocate(".CRT$XLF"))
#endif
__xl_f;
const PIMAGE_TLS_CALLBACK
#if defined __GNUC__
__attribute__((__section__(".CRT$XLF")))
#elif !USE_CONST_SEG && defined _MSC_VER
__declspec(allocate(".CRT$XLF"))
#endif
__xl_f = tls_callback;
#if USE_CONST_SEG && defined _MSC_VER
# pragma const_seg(pop, old_seg)
#endif
int foo_init(void) {
switch(callback_reason) {
case MAXDWORD: printf("TLS callback didn't fire.\n"); return 1;
case DLL_PROCESS_ATTACH: printf("DLL_PROCESS_ATTACH\n"); return 0;
case DLL_PROCESS_DETACH: printf("DLL_PROCESS_DETACH\n"); return 0;
case DLL_THREAD_ATTACH: printf("DLL_THREAD_ATTACH\n"); return 0;
case DLL_THREAD_DETACH: printf("DLL_THREAD_DETACH\n"); return 0;
default: return 2;
}
}
#include <windows.h>
#include <stdio.h>
extern int foo_init(void);
int main(void)
{
int rc = foo_init();
printf("Main function!\n");
return rc;
}
#include <windows.h>
#include <stdio.h>
static DWORD callback_reason = MAXDWORD;
static void NTAPI __stdcall
tls_callback(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
UNREFERENCED_PARAMETER(DllHandle);
UNREFERENCED_PARAMETER(Reserved);
callback_reason = Reason;
}
#if defined(_MSC_VER)
/* Force a reference to _tls_used to make the linker create the TLS
* directory if it's not already there. (e.g. if __declspec(thread)
* is not used).
* Force a reference to __xl_f to prevent whole program optimization
* from discarding the variable. */
/* On x86, symbols are prefixed with an underscore. */
#if defined(_M_IX86)
# pragma comment(linker, "/INCLUDE:__tls_used")
# pragma comment(linker, "/INCLUDE:___xl_f")
#elif defined(_M_ARM) || defined(_M_ARM64) || defined(_M_X64)
# pragma comment(linker, "/INCLUDE:_tls_used")
# pragma comment(linker, "/INCLUDE:__xl_f")
#endif
/* .CRT$XLA to .CRT$XLZ is an array of PIMAGE_TLS_CALLBACK
* pointers. Pick an arbitrary location for our callback.
*
* See VC\...\crt\src\vcruntime\tlssup.cpp for reference. */
/* .CRT section is merged with .rdata on x64 so it must be constant
* data. Also works on all other architectures. */
#define USE_CONST_SEG 1
#if USE_CONST_SEG
# pragma const_seg(push, old_seg)
# pragma const_seg(".CRT$XLF")
#else
# pragma section(".CRT$XLF", long, read)
#endif
#endif
const PIMAGE_TLS_CALLBACK
#if defined __GNUC__
__attribute__((__section__(".CRT$XLF")))
#elif !USE_CONST_SEG && defined _MSC_VER
__declspec(allocate(".CRT$XLF"))
#endif
__xl_f = tls_callback;
#if USE_CONST_SEG && defined _MSC_VER
# pragma const_seg(pop, old_seg)
#endif
int main(void) {
switch(callback_reason) {
case MAXDWORD: printf("TLS callback didn't fire.\n"); return 1;
case DLL_PROCESS_ATTACH: printf("DLL_PROCESS_ATTACH\n"); return 0;
case DLL_PROCESS_DETACH: printf("DLL_PROCESS_DETACH\n"); return 0;
case DLL_THREAD_ATTACH: printf("DLL_THREAD_ATTACH\n"); return 0;
case DLL_THREAD_DETACH: printf("DLL_THREAD_DETACH\n"); return 0;
}
}
all: main.exe main2.exe
main-clang-cl.exe: foo.c main.c
clang-cl -O2 -clang:-fuse-ld=lld -clang:-flto=thin -W4 -c foo.c
clang-cl -O2 -clang:-fuse-ld=llvm-lib -W4 -o foo.lib foo.obj
clang-cl -O2 -clang:-fuse-ld=lld -clang:-flto=thin -W4 -c main.c
clang-cl -O2 -clang:-fuse-ld=lld -clang:-flto=thin -W4 -o main.exe foo.lib main.obj
main-gcc.exe: libfoo.a main.o
$(CC) $(LDFLAGS) main.o $(LOADLIBES) $(LDLIBS) libfoo.a -o $@
libfoo.a: foo.o
$(AR) rcs libfoo.a foo.o
main.exe: foo.lib main.obj
link.exe -nologo $(LDFLAGS) /OUT:$@ $(LOADLIBES) $(LDLIBS) $^
%.obj: %.c
$(CC) -nologo $(CFLAGS) -c $^
foo.lib: foo.obj
lib.exe -nologo /OUT:$@ $^
main2.exe: main2.c
$(CC) -nologo $(CFLAGS) $^
clean:
$(RM) *.o *.a *.obj *.lib *.dll *.exe
.PHONY: clean all
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment