Does LTO intentionally emit .init_array regardless of -target value?

I have questions about LTO respecting -target values. Here’s my reproducer code (tested using LLVM 13):

// a.h
#pragma once
struct A {
    A();
    int a;
};

// a.cc
#include "a.h"
int foo();
A::A() {
    a = foo();
}
A a;

// b.cc
#include "a.h"

extern A a;

int bar() {
    return a.a;
}

And build script:

# build.sh
rm -f a.o b.o c.ro
clang++ ${CFLAGS} a.cc -c
clang++ ${CFLAGS} b.cc -c
ld.lld -r -o c.ro a.o b.o
llvm-readelf -S c.ro | egrep "ctors|init_array"

If we specify a target that uses .ctors, we get that section as expected:

$ env CFLAGS="-target x86_64-unknown-freebsd9" ./build.sh
  [ 4] .ctors            PROGBITS        0000000000000000 0000b0 000008 00  WA  0   0  8
  [ 5] .rela.ctors       RELA            0000000000000000 0001c8 000018 18   I 11   4  8

If we specify a target that uses .init_array, we get that section as expected:

$ env CFLAGS="-target x86_64-unknown-freebsd12" ./build.sh
  [ 4] .init_array       INIT_ARRAY      0000000000000000 0000b0 000008 00  WA  0   0  8
  [ 5] .rela.init_array  RELA            0000000000000000 0001c8 000018 18   I 11   4  8

But if we specify a target that uses .ctors and enable LTO, we get .init_array instead:

$ env CFLAGS="-target x86_64-unknown-freebsd9 -flto=thin" ./build.sh
  [ 9] .init_array       INIT_ARRAY      0000000000000000 000080 000008 00  WA  0   0  8
  [10] .rela.init_array  RELA            0000000000000000 0001b0 000018 18   I 18   9  8
  1. This is unexpected to me, but is it actually an expected outcome?
  2. Is there a way to control LTO’s above section behavior via a flag or linker script?
  3. Alternatively, is there a way to force a .ctors-style ordering of .init_array? I believe this would allow migration from .ctors to .init_array while negating the observable difference between them (yes, I’m somewhat nervous about exposing a latent static init fiasco).

Thank you for any help.

Not honouring the target triple of the input bitcode sounds like a bug. Probably the version is being discarded and set to the default rather than the max of all inputs.

You used a discouraged way to perform the link (invoking the linker directly). If you invoke Clang Driver for the link action, --target=x86_64-unknown-freebsd9 will tell the backend to use .ctors.

.init_array is supported by modern systems .init, .ctors, and .init_array | MaskRay

FreeBSD added support in 2012-03.
OpenBSD added support in 2016-08.
NetBSD made DT_INIT_ARRAY available for all ports in 2018-12.

It’s true that the proper driver should be preferred, but in this case I don’t see why that should be required to get the correct behaviour.

Thank you both for the replies! I was unaware that my example was a discouraged method of linking, so I’m glad to learn that.

But unfortunately I am not seeing .ctors emitted even when using clang to drive the link:

# build2.sh
rm -f a.o b.o c.ro
clang++ ${CFLAGS} a.cc -c
clang++ ${CFLAGS} b.cc -c
clang++ ${CFLAGS} -nostdlib -fuse-ld=lld -r a.o b.o -o c.ro
llvm-readelf -S c.ro | egrep "ctors|init_array"

Without LTO, we still see .ctors:

$ env CFLAGS="--target=x86_64-unknown-freebsd9" ./build2.sh
  [ 4] .ctors            PROGBITS        0000000000000000 0000b0 000008 00  WA  0   0  8
  [ 5] .rela.ctors       RELA            0000000000000000 0001c8 000018 18   I 11   4  8

And with LTO we still see .init_array:

$ env CFLAGS="--target=x86_64-unknown-freebsd9 -flto=thin" ./build2.sh
  [ 9] .init_array       INIT_ARRAY      0000000000000000 000080 000008 00  WA  0   0  8
  [10] .rela.init_array  RELA            0000000000000000 0001b0 000018 18   I 18   9  8

Does my updated build script represent what you intended?

@MaskRay When you get the chance, could you take a look at my previous post (build2.sh)? I’m still not sure if there is a bug here or I misunderstood your suggestion.

(And thank you for the blog post you wrote. I found it when looking into this issue and it was incredibly helpful getting me this far.)

Clang 13 is too old and I don’t know whether it works as intended. I checked latest Clang built from source few days ago and it works as intended. The .ctors/.init_array decision is for the whole module and we rely on Clang Driver to set the value based on the target triple. Nowadays Clang supported ELF OSes that don’t use .ctors are all legacy versions (all end-of-life?), it it not necessary to add more support for them.

Thank you for the response. I understand the rationale for avoiding further support. And for my issue, I plan to migrate to using .init_array.

But…I intended to bisect the change that made LTO honor the --target value, and even at head of line it isn’t honored as expected:

# build3.sh
rm -f a.o b.o c.ro
${CXX} --version | head -1
${CXX} ${CFLAGS} a.cc -c
${CXX} ${CFLAGS} b.cc -c
${CXX} ${CFLAGS} -nostdlib -fuse-ld=lld -r a.o b.o -o c.ro
llvm-readelf -S c.ro | egrep "ctors|init_array"

We get .ctors without LTO using clang 17:

$ env CXX="bin/clang++" CFLAGS="--target=x86_64-unknown-freebsd9" ./build3.sh
clang version 17.0.0 ([...]/llvm-project.git f098fb69f16420cda0624a3ae5ad625a09ca7fe6)
  [ 4] .ctors            PROGBITS        0000000000000000 0000b0 000008 00  WA  0   0  8
  [ 5] .rela.ctors       RELA            0000000000000000 0001c8 000018 18   I 11   4  8

And we still get .init_array with LTO, even using clang 17:

$ env CXX="bin/clang++" CFLAGS="--target=x86_64-unknown-freebsd9 -flto=thin" ./build3.sh
clang version 17.0.0 ([...]/llvm-project.git f098fb69f16420cda0624a3ae5ad625a09ca7fe6)
  [ 9] .init_array       INIT_ARRAY      0000000000000000 000080 000008 00  WA  0   0  8
  [10] .rela.init_array  RELA            0000000000000000 0001b0 000018 18   I 18   9  8

Again, I understand this might not be worth fixing. But as far as I can detect, the issue remains.