Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do static inline data members not end up in a .bss section on Macos?

Trying out snmalloc on Macos I wondered why all the created binaries are >256MiB.

It turns out that zero-initialized static inline data members are lowered in a weird way on Mac OS X, on both ARM64 and x86_64. Even this simple test produces huge binaries:

container.h

#pragma once
#include <cstdint>

class Container {
    public:
        inline static uint8_t inner[256000000];
};

main.cc

#include "container.h"

int main() {
    return Container::inner[0];
}

Compiled like this:

$ ~/clang+llvm-12.0.0-x86_64-apple-darwin/bin/clang -O3 -std=c++17 main.cc --target=x86_64-apple-darwin -c; ls -l main.o
-rw-r--r--  1 hans  staff  256000744 Jun 21 16:29 main.o

It is the same with open-source clang as with Apple clang. gcc behaves similarly.

On Linux (compiled with either clang or gcc) it is included in the .bss section, thus not taking up any space.

Why is this the case on Macos? And is this a bug or expected behavior?

like image 425
HHK Avatar asked Jun 21 '21 18:06

HHK


People also ask

What is the .bss section used for?

The . bss section is used by the compiler for global and static variables. It is one of the default COFF sections that is used to reserve a specified amount of space in the memory map that can later be used for storing data. It is normally uninitialized.

What is stored in bss section?

In computer programming, the block starting symbol (abbreviated to . bss or bss) is the portion of an object file, executable, or assembly language code that contains statically allocated variables that are declared but have not been assigned a value yet. It is often referred to as the "bss section" or "bss segment".

Why is BSS required?

bss is reserved and has special effects, in particular it occupies no file space, thus the advantage over . data . The downside is of course that all bytes must be set to 0 when the OS puts them on memory, which is more restrictive, but a common use case, and works fine for uninitialized variables.

What is the name of the section where the initialized data is declared?

In computing, a data segment (often denoted . data) is a portion of an object file or the corresponding address space of a program that contains initialized static variables, that is, global variables and static local variables.

Why do we initialize the static data member outside the class?

So the constructor of the class can’t initialize something which isn’t exclusive to the object of that class ( static data members in this case). That’s why we initialize the Static data member outside the class and not in the constructor. Classically, static variables would all be in the data or bss sections of your program.

Why is this feature not provided for non-static members?

"This feature is not provided" because in-class initialization for non-static and static members are semantically very different features. Your suprize is based on the fact that they look superficially similar. But in reality they actually have nothing in common.

Why are static members of a class not defined in C++?

The reason for this is simple, static members are only declared in a class declaration, not defined. They must be explicitly defined outside the class using the scope resolution operator .

What is in-class declaration of static data member?

In-class declaration of a static data member is just that - a declaration. It does not provide definition for that member. The definition has to be provided separately. The placement of that definition in the code of your program will have consequences.


1 Answers

I'll go ahead and take a stab at answering this, though I'll be the first to admit that you can only go so far with an answer before you run into a wall that says "because someone made a decision and you're stuck with it forever."

The primary key to all of this comes in the form of the Mach-O Runtime specification for MacOS, which defines the .bss section as being used for:

uninitialized static variables (for example, static int i;).

You can read about it in this archived version from version 10.3, but you can also find the same information in other Mach-O references.

The important thing to note here is that the use of bss refers to "private" symbols only. In other words, this refers to a C-style use of the static keyword, which is guaranteed to be local to the translation unit.

When you declare a C++17 member variable as static inline, despite the use of the perversely overloaded static keyword, you've created a global object, of which there is guaranteed to only ever be one instance in a program. In other words, every translation unit compiled with this declaration will instantiate it, and the linker will be expected to "coalesce" them into a single instance by picking one of them. This is obviously quite different from the C-style "uninitialized static variable."

MacOS host compilers like clang implement this by declaring the symbol as weak DATA, similar for example to how default constructors would be declared (though those would of course be in TEXT).

To illustrate this point, note that you could get the same effect without C++17 at all. For example compile these sets of examples this and look at the assembly output:

static uint8_t stuff[256000000]; // <- goes into .bss

int main() {
    return (int)reinterpret_cast<uint64_t>(&stuff[0]);
}

Note that I'm having to do the &stuff thing here to make sure the compiler doesn't optimize away stuff entirely in this case.

Now try this:

uint8_t stuff[256000000]; // <-- goes into __DATA,__common

int main() {
    return (int)reinterpret_cast<uint64_t>(&stuff[0]);
}

Getting closer. Note that stuff is not put into .bss like you might see on a linux platform. According again to the Mach-O runtime spec, the common section is used for:

Uninitialized imported symbol definitions (for example, int i;) located in the global scope (outside of a function declaration)."

Now try this:

__attribute__((weak)) uint8_t stuff[256000000]; // <-- in DATA,__data

int main() {
    return (int)reinterpret_cast<uint64_t>(&stuff[0]);
}

This is exactly how a static inline C++17 member variable will be defined. Deep under the hood, clang has assigned this symbol to be "coalesced" data, which on x86 just turns into standard DATA. If you really want to dive into the sausage factory, you can actually see that in the llvm SelectSectionForGlobal function.

   if (GO->isWeakForLinker()) {
     if (Kind.isReadOnly())
       return ConstTextCoalSection;
     if (Kind.isReadOnlyWithRel())
       return ConstDataCoalSection;
     return DataCoalSection;
   }

And DataCoalSection is correspondingly defined here to be identical to the ordinary data section on everything but power PC.

So from my perspective the behavior you're seeing is working as I would expect given the available specifications for the Mach-O runtime.

like image 97
Jon Reeves Avatar answered Oct 31 '22 21:10

Jon Reeves