2

I meet an issue with global/static variables' initialization with __attribute__((constructor)) in shared library, that certain variables seem to be initialized twice.

Below are code snippets:

shared.cpp

struct MyStruct
{
  MyStruct(int s = 1)
  : s(s) {
    printf("%s, this: %p, s=%d\n", __func__, this, s);
  }
  ~MyStruct() {
    printf("%s, this: %p, s=%d\n", __func__, this, s);
  }
  int s;
};

MyStruct* s1 = nullptr;
std::unique_ptr<MyStruct> s2 = nullptr;
std::unique_ptr<MyStruct> s3;
MyStruct s4;

void onLoad() __attribute__((constructor));
void onLoad()
{
  s1 = new MyStruct;
  s2 = std::make_unique<MyStruct>();
  s3 = std::make_unique<MyStruct>();
  s4 = MyStruct(2);

  printf("&s1: %p, &s2: %p, &s3: %p\n", &s1, &s2, &s3);
  printf("s1: %p, s2: %p, s3: %p\n", s1, s2.get(), s3.get());
  printf("s4: %p, s4.s: %d\n", &s4, s4.s);
}

extern "C" void foo()
{
  printf("&s1: %p, &s2: %p, &s3: %p\n", &s1, &s2, &s3);
  printf("s1: %p, s2: %p, s3: %p\n", s1, s2.get(), s3.get());
  printf("s4: %p, s4.s: %d\n", &s4, s4.s);
}

main.cpp

#include <cstdio>
#include <dlfcn.h>

using Foo = void(*)(void);

int main()
{
  printf("Calling dlopen...\n");
  void* h = dlopen("./libshared.so", RTLD_NOW | RTLD_GLOBAL);
  Foo f = reinterpret_cast<Foo>(dlsym(h, "foo"));
  printf("\nCalling foo()...\n");
  f();
  return 0;
}

Compiled with

$ g++ -fPIC -shared -std=c++14 shared.cpp -o libshared.so
$ g++ -std=c++14 -o main main.cpp -ldl

The output:

Calling dlopen...
MyStruct, this: 0x121b200, s=1
MyStruct, this: 0x121b220, s=1
MyStruct, this: 0x121b240, s=1
MyStruct, this: 0x7ffc19736910, s=2
~MyStruct, this: 0x7ffc19736910, s=2
&s1: 0x7fb1fe487190, &s2: 0x7fb1fe487198, &s3: 0x7fb1fe4871a0
s1: 0x121b200, s2: 0x121b220, s3: 0x121b240
s4: 0x7fb1fe4871a8, s4.s: 2
MyStruct, this: 0x7fb1fe4871a8, s=1

Calling foo()...
&s1: 0x7fb1fe487190, &s2: 0x7fb1fe487198, &s3: 0x7fb1fe4871a0
s1: 0x121b200, s2: (nil), s3: 0x121b240
s4: 0x7fb1fe4871a8, s4.s: 1
~MyStruct, this: 0x7fb1fe4871a8, s=1
~MyStruct, this: 0x121b240, s=1

The value of s1 and s3 are expected.

But s2 and s4 behave weird.

  • s2.get() should be 0x121b220, but in foo() it becomes nullptr;
  • s4's value is printed as s4.s: 2 in onLoad(), but after that its constructor is called with default value s=1, then in foo() its value is s=1.

Putting the variables in anonymous namespace has the same result.

What's wrong with s2 and s4?

My OS: Ubuntu 16.04.2, GCC: 5.4.0

Mine
  • 4,123
  • 1
  • 25
  • 46
  • I don't see any evidence that anything's "initialized twice" – Lightness Races in Orbit May 12 '17 at 15:21
  • 1
    The sequence of ctor/dtor calls make it appear that `s4` was never constructed before `onLoad()` got called, but then gets constructed after. The first three ctor calls are from your heap allocations. The fourth is the temporary `MyStruct(2)` and the following dtor call is the temporary being destroyed. There's no default ctor call for `s4` -- until after the final `printf()`. That's strange indeed. That's probably why `s4.s` becomes 1, though. – cdhowie May 12 '17 at 15:21
  • @BoundaryImposition Yeah, that's why I say it *seems*, I'm just not sure. For example `s4` seems to be constructed twice, or it is used before constructed, then it's constructed... – Mine May 12 '17 at 15:25
  • No it doesn't. The output shows precisely the right number of calls to the `MyStruct` constructor. – Lightness Races in Orbit May 12 '17 at 15:26
  • @BoundaryImposition It does, but not in the right order. We are compiling without `-O` so we don't get copy elision. See my comment. `s4` does appear to be copy-assigned from the temporary prior to its default construction, which happens after `onLoad()` returns, clobbering the value 2 with 1. – cdhowie May 12 '17 at 15:27
  • @cdhowie: Yes, I read your comment, which is correct – Lightness Races in Orbit May 12 '17 at 15:27
  • [This](https://pastebin.com/1avcsnX6) is how I interpret the sequence of events, anyway, if this makes it clearer. This does seem like a GCC bug, as the documentation for `__attribute__((constructor))` implies that it follows the standard static initialization order rules; that is, things are initialized/called in the order they are declared. But `onLoad()` is declared after `s4`, so we would expect `s4` to be initialized first. – cdhowie May 12 '17 at 15:31
  • I'm not sure but maybe you are looking for `__atribute__((init_priority(N)))` for your `s` variables [reference](https://gcc.gnu.org/onlinedocs/gcc-3.2/gcc/C---Attributes.html) – W.F. May 12 '17 at 15:34

1 Answers1

3

As per the discussion on this GCC bug report and this follow-up doc patch it seems that what you're seeing is unspecified behavior in GCC (not a bug).

However, the order in which constructors for C++ objects with static storage duration and functions decorated with attribute constructor are invoked is unspecified. In mixed declarations, attribute init_priority can be used to impose a specific ordering.

It seems in this case that a segfault was narrowly avoided, as assigning to an uninitialized std::unique_ptr could cause delete to be invoked for an uninitialized pointer member. GCC's unspecified behavior translates into undefined behavior (in this particular case) according to the C++ specification, because it's undefined behavior to read from an uninitialized variable (except for an uninitialized unsigned char).

Anyway, to correct this problem you do indeed need to use __attribute((init_priority)) to order initialization of your statically-declared objects before the constructor function.

Community
  • 1
  • 1
cdhowie
  • 158,093
  • 24
  • 286
  • 300
  • I still consider this as GCC bug, since the bug is still not closed. Using `__attribute__((init_priority(101)))` does resolve the issue. – Mine May 13 '17 at 01:48