What techniques do compilers use to initialize statically allocated pointers to addresses of other statically allocated objects?
Broadly speaking it is not much different from a function call or access to a global variable. The compiler will insert a symbolic reference and the exact memory location will be filled in later.
Logically a program goes through a number of steps between source code and execution.
- Compilation.
- Assembly (optional, the compilation process may generate object files directly)
- Linking
- Loading/Dynamic linking (optional, the code may be burned into a rom).
At some stage in the process, the symbolic references in the code must be resolved to become actual addresses. There are broadly two stages where this can happen, if the code is built targetting a fixed address, then the linker can complete the resolution process. If the code is built to be relocatable at load time then the resolution process must be completed by the loader/dynamic linker.
I'm going to use the following code for my tests, it is similar to yours but I have removed the "static", this means that the compiler will not be able to optimise the unused variables away. I'm going to use arm32 as it's the platform whose assembler I find easy to read, and linux as the target OS. That said, the assembler I'm going to present isn't going to contain any actual code.
int x;
int *const p = &x;
const char *const s = "The address of this string will not necessarily be the same across executions";
On most linux ports* linux we can control whether the code is built to be relocatable or not with the -fPIC switch.
When looking at godbolt be aware that the output is heavily filtered by default. We need to turn off some of the filtering here or we will miss important stuff. I have manually skipped some stuff that is not relavent to the question.
First lets build it with no compiler flags. In this configuration the compiler will produce code that can be relocated at link time but cannot be relocated during the loading process.
.global x
.bss
.align 2
.type x, %object
.size x, 4
x:
.space 4
x is not initialised, so it is placed in the bss section and no initial valid is provides.
.global p
.section .rodata
.align 2
.type p, %object
.size p, 4
p:
.word x
x declared const, so it's placed in the rodata section. However the compiler does not provide a numeric value but merely a symbolic reference. The linker will fill in that symbolic reference later.
.global s
.align 2
.LC0:
.ascii "The address of this string will not necessarily be "
.ascii "the same across executions\000"
.align 2
.type s, %object
.size s, 4
s:
.word .LC0
The compiler has put both the string literal and the constant s in the rodata section and like it has used a symbolic reference to allow s to refer to the string constant. The section is not explicitly selected in this block of code because it was already set to .rodata from the previous block.
So now lets try adding -fPIC how does the code change.
.bss
.align 2
.type x, %object
.size x, 4
x:
.space 4
No change here, x is still an uninitialised variable in the bss section.
.global p
.section .data.rel.ro,"aw"
.align 2
.type p, %object
.size p, 4
p:
.word x
However, this has changed. Rather than being placed in the normal rodata section this has now been placed in a "data.rel.ro" section. This is a special area of memory that is writeable during the relocation phase of dynamic loading, but will become read-only once loading is complete.
.global s
.section .rodata
.align 2
.LC0:
.ascii "The address of this string will not necessarily be "
.ascii "the same across executions\000"
.section .data.rel.ro.local,"aw"
.align 2
.type s, %object
.size s, 4
s:
.word .LC0
The string literal is still placed in the regular rodata section, since it doesn't refer to any memory locations, but the pointer to it is once-again placed in a special section.
* for historic reasons the situation is a bit different on x86-32.
static void *const p = (void *)0xdeadbeef;
is valid code on 32-bit targets. $\endgroup$dlopen
or byld.so
. $\endgroup$