Gotchas when using linker sections to arrange data, part 1


We saw last time that you can use linker sections to arrange the order in which data appears in the module. We ended with a diagram like this:

mydata$a firstInitializer main.obj
mydata$g DoThisSooner3 file3.obj unspecified
order
DoThisSooner4 file4.obj
mydata$m Function2 file2.obj unspecified
order
Function1
file1.obj
Function3 file3.obj
mydata$t DoThisLater2 file2.obj unspecified
order
DoThisLater4 file4.obj
mydata$z lastInitializer main.obj

Based on this table, we would be tempted to write code like this:

// Code in italics is wrong.
void NaiveInitializeAllTheThings()
{
    const INITIALIZER* initializer = &firstInitializer + 1;
    while (initializer < &lastInitializer) {
      (*initializer++)();
    }
}

From a language lawyer standpoint, this code is not valid because it dereferences a pointer beyond the end of an object, and because it compares two pointers which are not part of the same aggregate. We can fix this by switching to uintptr_t as our currency.

// Code in italics is still wrong.
void LessNaiveInitializeAllTheThings()
{
    auto begin = (uintptr_t)&firstInitializer
                 + sizeof(firstInitializer);
    auto end = (uintptr_t)&lastInitializer;
    for (auto current = begin; current < end;
         current += sizeof(INITIALIZER)) {
      auto initializer = *(const INITIALIZER*)current;
      initializer();
    }
}

The conversion between pointers and uintptr_t is implementation-defined (rather than undefined), so this avoids the undefined behavior problems of using pointers to walk between two global variables.

But the code is still not right, because it fails to take into account another detail of linker sections: intra-section padding.

The linker will add padding after a fragment in order to satisfy any alignment requirements of the subsequent fragment. That's expected.

What most people aren't aware of is that the linker is permitted but not required to add padding after each fragment, up to the section's alignment. In practice, you are likely to see this "unnecessary" padding when using incremental linking.

In all cases, the padding bytes (if any) will be zero.

mydata$a firstInitializer main.obj
Optional padding
mydata$g DoThisSooner3 file3.obj unspecified
order
Optional padding
DoThisSooner4 file4.obj
Optional padding
mydata$m Function2 file2.obj unspecified
order
Optional padding
Function1
file1.obj
Optional padding
Function3 file3.obj
Optional padding
mydata$t DoThisLater2 file2.obj unspecified
order
Optional padding
DoThisLater4 file4.obj
Optional padding
mydata$z lastInitializer main.obj
Optional padding

To accommodate padding, we need to skip over any possible null pointers.

void InitializeAllTheThings()
{
    auto begin = (uintptr_t)&firstInitializer
                 + sizeof(firstInitializer);
    auto end = (uintptr_t)&lastInitializer;
    for (auto current = begin; current < end;
         current += sizeof(INITIALIZER)) {
      auto initializer = *(const INITIALIZER*)current;
      if (initializer) initializer();
    }
}

We'll look at another consequence of padding next time.

Comments (11)

  1. Anders Munch says:

    The uintptr_t version has undefined behaviour as well, from the point of view of the C standard. But that’s OK: Nothing stops a particular implementation from defining a behaviour for something that the C standard does not define a behaviour for.

    A particular implementation could even have chosen to define past-the-end pointer arithmetic in a way that would make the first all-wrong attempt correct, except for the padding. (And the infinite loop. Why is the pointer incremented outside of the while loop?)

    Language lawyering aside, I’m sure Raymond has a good handle on what is defined with the compiler(s) he uses, so it’s still good advice to use uintptr_t.

    1. 6.3.2.3 Pointers: “An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined.”

  2. Henke37 says:

    Pretty sure that it’s undefined behavior to convert an uintptr_t value back to a pointer that is not equal to the value you got when converting from a pointer to uintptr_t.

    1. [expr.reinterpret.cast] says “A value of integral type or enumeration type can be explicitly converted to a pointer. A pointer converted to an integer of sufficient size (if any such exists on the implementation) and back to the same pointer type will have its original value; mappings between pointers and integers are otherwise implementation-defined.”

  3. wilsone88 says:

    I know that the NaiveInitializeAllTheThings function is wrong anyways, but it is also an infinite loop; you never increment the initializer variable.

    [Oops. “Fixed.” -Raymond]

  4. uffa8 says:

    first at all when if declare `const INITIALIZER initializer##fn = fn` – use `const` the latest msvc compiler (i assume optimization and /GL – link time code generation) not even create variable (no such name (?initializer…) in obj file ) if no reference for it address. say if we will be use `&firstInitializer` and `&lastInitializer` – will be such symbols obj but if no reference – no variable. but even if we remove `const` modificator – with /GL and /OPT:REF optimization – symbol will be dropped by linker. i test this.
    one way use direct `/include` linker option: __pragma(comment(linker, “/include:?__x_m@@3P6AXXZEA”)) (i use `__x_m` symbol)
    also i note that if section name the same as used in #pragma init_seg(*) (or `.CRT$XCU` as default place) – where msvc put addresses of initializers – linker in this case not drop our custom ?__x_m@@.. too.

    so really question how correct declare self data, without reference for it in code and to be not dropped. how i say – i found only explicit `/include` linker option for every declared symbol. but standard intializers generated by compiler (when we declare global objects with constructor or global variable with function call initialization) not droped

    also instead use #pragma section we can better use `#pragma const_seg(“mydata$*”)` for firstInitializer, lastInitializer if we declare it with `const` (this possible because we take it addresses) even not need `__declspec(allocate(“*”))` because it will be const data, but for other initializers this need, because we must declare it without const.

    so my demo code for test: (https://pastebin.com/V0bgQrst)

    typedef void (__cdecl *INITIALIZER)();

    #pragma const_seg(“mydata$a”)
    const INITIALIZER firstInitializer = 0;

    #pragma const_seg(“mydata$z”)
    const INITIALIZER lastInitializer = 0;

    void InitializeAllTheThings(const INITIALIZER * pfbegin, const INITIALIZER * pfend) {
    // if (pfbegin < pfend)
    do {
    if (const INITIALIZER initializer = *pfbegin++) initializer();
    } while(pfbegin < pfend);
    }

    void InitializeAllTheThings()
    {
    InitializeAllTheThings(&firstInitializer + 1, &lastInitializer);
    }

    void __cdecl someFunc();

    #pragma const_seg("mydata$u")
    __declspec(allocate("mydata$u")) /*const*/ INITIALIZER __x_m = someFunc;

    #pragma warning(disable : 4075) // initializers put in unrecognized initialization area
    #pragma init_seg("mydata$u")

    struct XXX {
    XXX() { someFunc(); }
    };

    //static XXX g_x;
    __pragma(comment(linker, "/include:?__x_m@@3P6AXXZEA"))

    (look in obj, asm (set /FAs) and map file). and mandatory use /GL /OPT:REF /O1 or more )

    comment __pragma(comment(linker, "/include:?__x_m@@3P6AXXZEA")) and ?__x_m@@3P6AXXZEA (even all "mydata$u") go away from map (and final code). un comment static XXX g_x; just for demo – `?__x_m@@3P6AXXZEA` again exist

    at second i not agree with *From a language lawyer standpoint, this code is not valid because it dereferences a pointer beyond the end of an object, and because it compares two pointers which are not part of the same aggregate.*

    why ? code perfect compiled, are something wrong in my implementation `void InitializeAllTheThings(const INITIALIZER * pfbegin, const INITIALIZER * pfend)` ? or in call `InitializeAllTheThings(&firstInitializer + 1, &lastInitializer);` ? and if you look for `_initterm` in msvc crt files – your view the same implementation. we have knowledge that here we have array of objects (pointers) and can work with it. not view nothing wrong (by syntax or semantic) in use say `&firstInitializer + 1`. and what we gain if try use `uintptr_t` in mix with pointers ? we anyway use the same knowledge that here we have array of objects (pointers in concrete case). if you think that correct (by sense) take some `uintptr_t` value and convert it to pointer to object (`INITIALIZER*`)and insist that this is correct pointer to object, despite we not take it via & of any object, why not correct say that `&firstInitializer + 1` also correct pointer to the some object ? i think use trick with uintptr_t have no any sense here

    1. Darran Rowe says:

      Unless you are using the /Gw compiler command line option, the compiler, and thus the linker, will group all variables together.
      This means that if you have two global variables, but you only access one of them, then both of them will be included. It doesn’t matter how many you use define, as long as one of the global variables are referenced then all of them will come along for the ride.
      The other option if you know that your variable will not be included (you are using /Gw or you are not referencing a global variable), then the linker’s /include option is the only option, but you can tell the linker at compile time to include a symbol. The compiler has the #pragma directive, and you can use #pragma comment(linker. “/include:<decorated symbol name>”) to get the compiler to tell the linker to do this automatically. (https://docs.microsoft.com/en-gb/cpp/preprocessor/comment-c-cpp?view=vs-2017)

      1. uffa8 says:

        /Gw compiler here unrelated. problem here how used `const`. if we write `const INITIALIZER initializer_fn = fn;` and no reference to address of `initializer_fn` – compiler not generate variable for `initializer_fn` (look in obj file). we need add reference for `&initializer_fn` somehow. i found way how add this and no generate additional code. look full comment https://blogs.msdn.microsoft.com/oldnewthing/20181107-00/?p=100155#comment-1370335 . unfortunately dont know how correct format code in comment. external link – https://pastebin.com/32RehATA

        1. Darran Rowe says:

          Even if the variable isn’t const, when you build using optimisation, the compiler doesn’t the initialisation function at compile time if it is able to work out the value. In the PE format, global variables are allocated storage in the object file/executable itself, and if the compiler can figure out the value of a variable, then it can just write that value to the object itself.
          For example:
          const int value()
          {
          return 1;
          }

          const int g_value = value();
          Why should the compiler generate the initialisation function for g_value when it can see that it is being initialised by a constant value? Instead, it can just write the value 1 to the space for g_value in the object right? It completely cuts out the need for running the g_value initialisation function.

          1. uffa8 says:

            but how this is related to what i wrote ? i can only propose for you and Raymond take my concrete code https://pastebin.com/32RehATA and compile build it. and comment / remove MAKE_VAR(random_var) – and compare result. all what i try say – if we have in code `const T* x = y;` and no reference fo `&x` in code – compiler can not create variable at all. will be no `?x@..` even in obj file. because this – proposed code is not correct. the original Raymond macros `ADD_INITIALIZER_TO_SECTION` will be have no effect. i found how force it not drop via `MAKE_VAR` macro. another my point – instead use #pragma section and __declspec(allocate better use __pragma(const_seg(section)). and finally i not agree that code with pointer increment is not correct, but code when we convert pointer to `uintptr_t`, increment it and that convert back to pointer is correct. anyway – are you try really compile and build and look in result ?

  5. uffa8 says:

    unfortunately i sure that author even not compile self code (https://godbolt.org/z/OqAqUV) – the constructions like __declspec(allocate(“mydata$””m”)) not correct – string “mydata$””m” not spited by linker to “mydata$m”

    about your concrete note: if we write `const int g_value = 1;` and even will be use `g_value` in code (say like void __cdecl someFunc() {
    DbgPrint(“%u\n”, g_value);
    }
    ) compile anyway replace g_value to 1 and you not found ?g_value@@ in obj file at all. and it will be no in map file too. simply no such variable. but if we add reference to &g_value address say via static void fakeFn() { if (&g_value)__debugbreak(); }; – all is changed – will be `?g_value@@3HB` symbol in obj and map files. was real variable in binary. despite even in code it will be not used – where we use g_value in code – compiler just insert 1 and never reference variable memory, but allocate memory for it in section

Skip to main content