1

I was learning dynamic linking recently and gave it a try:

dynamic.c

int global_variable = 10; int XOR(int a) { return global_variable; } 

test.c

#include <stdio.h> extern int global_variable; extern int XOR(int); int main() { global_variable = 3; printf("%d\n", XOR(0x10)); } 

The compiling commands are:

clang -shared -fPIC -o dynamic.so dynamic.c clang -o test test.c dynamic.so 

I was expecting that in executable test the main function will access global_variable via GOT. However, on the contrary, the global_variable is placed in test's data section and XOR in dynamic.so access the global_variable indirectly.

Could anyone tell me why the compiler didn't ask the test to access global_variable via GOT, but asked the shared object file to do so?

5
  • I tried to reproduce your results, but in my case the main function seems to be accessing the global_variable through GOT, could you provide actual output of gdb/readelf/any other command you are using to inspect it? Commented Dec 25, 2021 at 13:06
  • Also in my case the location of global_variable is inside a memory area which comes from the dynamic.so library Commented Dec 25, 2021 at 13:42
  • @msaw328 which OS and compiler? Commented Dec 25, 2021 at 14:24
  • Arch linux with 5.15.11 kernel, clang version 13.0.0 Commented Dec 25, 2021 at 14:25
  • I use clang version 10.0.0-4ubuntu1 and Linux f404b4370915 5.10.47-linuxkit. The objdump -d test get this ( Sorry, I have no idea how to keep its formatting ): 0000000000401140 <main>: 401140: 55 push %rbp 401141: 48 89 e5 mov %rsp,%rbp 401144: 48 83 ec 10 sub $0x10,%rsp 401148: c7 04 25 38 40 40 00 movl $0x3,0x404038 40114f: 03 00 00 00 401153: bf 10 00 00 00 mov $0x10,%edi 401158: e8 e3 fe ff ff callq 401040 <XOR@plt> ...... Commented Dec 27, 2021 at 1:44

2 Answers 2

0

Part of the point of a shared library is that one copy gets loaded into memory, and multiple processes can access that one copy. But every program has its own copy of each of the library's variables. If they were accessed relative to the library's GOT then those would instead be shared among the processes using the library, just like the functions are.

There are other possibilities, but it is clean and consistent for each executable to provide for itself all the variables it needs. That requires the library functions to access all of its variables with static storage duration (not just external ones) indirectly, relative to the program. This is ordinary dynamic linking, just going the opposite direction from what you usually think of.

Sign up to request clarification or add additional context in comments.

5 Comments

"But every program has its own copy of each of the library's variables" A program only has a copy of the variables it knows about. "If they were accessed relative to the library's GOT then those would instead be shared among the processes using the library" This is not the case with the variables that the main program doesn't know about. They are accessed (by the library) via the GOT and somehow different processed don't get mixed up. You also can build the main executable with -fpic, and then it will not have its own copy of library variables.
@n.1.8e9-where's-my-sharem. the dynamic object representing an executable may or may not have libraries' variables incorporated into it, but yes, every program does have its own copies of each of the library's variables. This is a question of semantics, not architecture.
If you use the word program this way, then a program may, and indeed in this case does, have more than one physical copy of a variable. The question is then which one is being actually used, and how. Some of the actually used copies physically reside in the executable, some reside in one of the libraries. Some are used with the GOT, some are not.
@John Bollinger Thanks for your reply. I understand the second paragraph and it helps a lot. But what does this sentence mean? "If they were accessed relative to the library's GOT then those would instead be shared among the processes using the library, just like the functions are." In my opinion, an alternative would be: make all shared libraries keep global variables in their data section and access them relative to text section, and make all executables access extern global variables with GOT, as with accessing extern functions.
@pkuGenuine, if the library provided the storage for its static-duration variables then all processes would share them instead of having their own copies. These need to be per-process, and the library cannot rely on them being at the same absolute address because it is not feasible to provide reserved (virtual) addresses for all libraries. Thus, the library must access them indirectly. Certainly it could be that all dynamic objects access them indirectly, but where there is one that is loaded separately per-process then it is an optimization to have that one access them directly.
0

Turns out my clang produced PIC by default so it messed with results.

I will leave updated answer here, and the original can be read below it.


After digging a bit more into the topic i have noticed that compilation of test.c does not generate a .got section by itself. You can check it by compiling the executable into an object file and omitting the linking step for now (-c option):

clang -c -o test.o test.c 

If you inspect the sections of resulting object file with readelf -S you will notice that there is no .got in there:

Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 0000000000000035 0000000000000000 AX 0 0 1 [ 2] .rela.text RELA 0000000000000000 00000210 0000000000000060 0000000000000018 I 11 1 8 [ 3] .data PROGBITS 0000000000000000 00000075 0000000000000000 0000000000000000 WA 0 0 1 [ 4] .bss NOBITS 0000000000000000 00000075 0000000000000000 0000000000000000 WA 0 0 1 [ 5] .rodata PROGBITS 0000000000000000 00000075 0000000000000004 0000000000000000 A 0 0 1 [ 6] .comment PROGBITS 0000000000000000 00000079 0000000000000013 0000000000000001 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 0000000000000000 0000008c 0000000000000000 0000000000000000 0 0 1 [ 8] .note.gnu.pr[...] NOTE 0000000000000000 00000090 0000000000000030 0000000000000000 A 0 0 8 [ 9] .eh_frame PROGBITS 0000000000000000 000000c0 0000000000000038 0000000000000000 A 0 0 8 [10] .rela.eh_frame RELA 0000000000000000 00000270 0000000000000018 0000000000000018 I 11 9 8 [11] .symtab SYMTAB 0000000000000000 000000f8 00000000000000d8 0000000000000018 12 4 8 [12] .strtab STRTAB 0000000000000000 000001d0 000000000000003e 0000000000000000 0 0 1 [13] .shstrtab STRTAB 0000000000000000 00000288 0000000000000074 0000000000000000 0 0 1 

This means that the entirety of .got section present in the test executable actually comes from dynamic.so, as it is PIC and uses GOT.

Would it be possible to compile dynamic.so as non-PIC as well? Turns out it apparently used to be 10 years ago (the article compiles examples to 32-bits, they dont have to work on 64 bits!). Linked article describes how a non-PIC shared library was relocated at load time - basically, every time an address that needed to be relocated after loading was present in machine code, it was instead set to zeroes and a relocation of a certain type was set in the library. During loading of the library the loader filled the zeros with actual runtime address of data/code that was needed. It is important to note that it cannot be applied in your though as 64-bit shared libraries cannot be made out of non-PIC (Source).

If you compile dynamic.so as a shared 32-bit library instead and do not use the -fPIC option (you usually need special repositories enabled to compile 32-bit code and have 32-bit libc installed):

gcc -m32 dynamic.c -shared -o dynamic.so 

You will notice that:

// readelf -s dynamic.so (... lots of output) 27: 00004010 4 OBJECT GLOBAL DEFAULT 19 global_variable // readelf -S dynamic.so (... lots of output) [17] .got PROGBITS 00003ff0 002ff0 000010 04 WA 0 0 4 [18] .got.plt PROGBITS 00004000 003000 00000c 04 WA 0 0 4 [19] .data PROGBITS 0000400c 00300c 000008 00 WA 0 0 4 [20] .bss NOBITS 00004014 003014 000004 00 WA 0 0 1 

global_variable is at offset 0x4010 which is inside .data section. Also, while .got is present (at offset 0x3ff0), it only contains relocations coming from other sources than your code:

// readelf -r Offset Info Type Sym.Value Sym. Name 00003f28 00000008 R_386_RELATIVE 00003f2c 00000008 R_386_RELATIVE 0000400c 00000008 R_386_RELATIVE 00003ff0 00000106 R_386_GLOB_DAT 00000000 _ITM_deregisterTM[...] 00003ff4 00000206 R_386_GLOB_DAT 00000000 __cxa_finalize@GLIBC_2.1.3 00003ff8 00000306 R_386_GLOB_DAT 00000000 __gmon_start__ 00003ffc 00000406 R_386_GLOB_DAT 00000000 _ITM_registerTMCl[...] 

This article introduces GOT as part of introduction on PIC, and i have found that to be the case in plenty of places, which would suggest that indeed GOT is only used by PIC code although i am not 100% sure of it and i recommend researching the topic more.

What does this mean for you? A section in the first article i linked called "Extra credit #2" contains an explanation for a similar scenario. Although it is 10 years old, uses 32-bit code and the shared library is non-PIC it shares some similarities with your situation and might explain the problem you presented in your question.

Also keep in mind that (although similar) -fPIE and -fPIC are two separate options with slightly different effects and that if your executable during inspection is not loaded at 0x400000 then it probably is compiled as PIE without your knowledge which might also have impact on results. In the end it all boils down to what data is to be shared between processes, what data/code can be loaded at arbitrary address, what has to be loaded at fixed address etc. Hope this helps.

Also two other answers on Stack Overflow which seem relevant to me: here and here. Both the answers and comments.


Original answer:

I tried reproducing your problem with exactly the same code and compilation commands as the ones you provided, but it seems like both main and XOR use the GOT to access the global_variable. I will answer by providing example output of commands that i used to inspect the data flow. If your outputs differ from mine, it means there is some other difference between our environments (i mean a big difference, if only addresses/values are different then its ok). Best way to find that difference is for you to provide commands you originally used as well as their output.

First step is to check what address is accessed whenever a write or read to global_variable happens. For that we can use objdump -D -j .text test command to disassemble the code and look at the main function:

0000000000001150 <main>: 1150: 55 push %rbp 1151: 48 89 e5 mov %rsp,%rbp 1154: 48 8b 05 8d 2e 00 00 mov 0x2e8d(%rip),%rax # 3fe8 <global_variable> 115b: c7 00 03 00 00 00 movl $0x3,(%rax) 1161: bf 10 00 00 00 mov $0x10,%edi 1166: e8 d5 fe ff ff call 1040 <XOR@plt> 116b: 89 c6 mov %eax,%esi 116d: 48 8d 3d 90 0e 00 00 lea 0xe90(%rip),%rdi # 2004 <_IO_stdin_used+0x4> 1174: b0 00 mov $0x0,%al 1176: e8 b5 fe ff ff call 1030 <printf@plt> 117b: 31 c0 xor %eax,%eax 117d: 5d pop %rbp 117e: c3 ret 117f: 90 nop 

Numbers in the first column are not absolute addresses - instead they are offsets relative to the base address at which the executable will be loaded. For the sake of explanation i will refer to them as "offsets".

The assembly at offset 0x115b and 0x1161 comes directly from the line global_variable = 3; in your code. To confirm that, you could compile the program with -g for debug symbols and invoke objdump with -S. This will display source code above corresponding assembly.

We will focus on what these two instructions are doing. First instruction is a mov of 8 bytes from a location in memory to the rax register. The location in memory is given as relative to the current rip value, offset by a constant 0x2e8d. Objdump already calculated the value for us, and it is equal to 0x3fe8. So this will take 8 bytes present in memory at the 0x3fe8 offset and store them in the rax register.

Next instruction is again a mov, the suffix l tells us that data size is 4 bytes this time. It stores a 4 byte integer with value equal to 0x3 in the location pointed to by the current value of rax (not in the rax itself! brackets around a register such as those in (%rax) signify that the location in the instruction is not the register itself, but rather where its contents are pointing to!).

To summarize, we read a pointer to a 4 byte variable from a certain location at offset 0x3fe8 and later store an immediate value of 0x3 at the location specified by said pointer. Now the question is: where does that offset of 0x3fe8 come from?

It actually comes from GOT. To show the contents of the .got section we can use the objdump -s -j .got test command. -s means we want to focus on actual raw contents of the section, without any disassembling. The output in my case is:

test: file format elf64-x86-64 Contents of section .got: 3fd0 00000000 00000000 00000000 00000000 ................ 3fe0 00000000 00000000 00000000 00000000 ................ 3ff0 00000000 00000000 00000000 00000000 ................ 

The whole section is obviously set to zero, as GOT is populated with data after loading the program into memory, but what is important is the address range. We can see that .got starts at 0x3fd0 offset and ends at 0x3ff0. This means it also includes the 0x3fe8 offset - which means the location of global_variable is indeed stored in GOT.

Another way of finding this information is to use readelf -S test to show sections of the executable file and scroll down to the .got section:

[Nr] Name Type Address Offset Size EntSize Flags Link Info Align (...lots of sections...) [22] .got PROGBITS 0000000000003fd0 00002fd0 0000000000000030 0000000000000008 WA 0 0 8 

Looking at the Address and Size columns, we can see that the section is loaded at offset 0x3fd0 in memory and its size is 0x30 - which corresponds to what objdump displayed. Note that in readelf ouput "Offset" is actually the offset into the file form which the program is loaded - not the offset in memory that we are interested in.

by issuing the same commands on the dynamic.so library we get similar results:

00000000000010f0 <XOR>: 10f0: 55 push %rbp 10f1: 48 89 e5 mov %rsp,%rbp 10f4: 89 7d fc mov %edi,-0x4(%rbp) 10f7: 48 8b 05 ea 2e 00 00 mov 0x2eea(%rip),%rax # 3fe8 <global_variable@@Base-0x38> 10fe: 8b 00 mov (%rax),%eax 1100: 5d pop %rbp 1101: c3 ret 

So we see that both main and XOR use GOT to find the location of global_variable.

As for the location of global_variable we need to run the program to populate GOT. For that we can use GDB. We can run our program in GDB by invoking it this way:

LD_LIBRARY_PATH="$LD_LIBRARY_PATH:." gdb ./test 

LD_LIBRARY_PATH environment variable tells linker where to look for shared objects, so we extend it to include the current directory "." so that it may find dynamic.so.

After the GDB loads our code, we may invoke break main to set up a breakpoint at main and run to run the program. The program execution should pause at the beginning of the main function, giving us a view into our executable after it was fully loaded into memory, with GOT populated.

Running disassemble main in this state will show us the actual absolute offsets into memory:

Dump of assembler code for function main: 0x0000555555555150 <+0>: push %rbp 0x0000555555555151 <+1>: mov %rsp,%rbp => 0x0000555555555154 <+4>: mov 0x2e8d(%rip),%rax # 0x555555557fe8 0x000055555555515b <+11>: movl $0x3,(%rax) 0x0000555555555161 <+17>: mov $0x10,%edi 0x0000555555555166 <+22>: call 0x555555555040 <XOR@plt> 0x000055555555516b <+27>: mov %eax,%esi 0x000055555555516d <+29>: lea 0xe90(%rip),%rdi # 0x555555556004 0x0000555555555174 <+36>: mov $0x0,%al 0x0000555555555176 <+38>: call 0x555555555030 <printf@plt> 0x000055555555517b <+43>: xor %eax,%eax 0x000055555555517d <+45>: pop %rbp 0x000055555555517e <+46>: ret End of assembler dump. (gdb) 

Our 0x3fe8 offset has turned into an absolute address of equal to 0x555555557fe8. We may again check that this location comes from the .got section by issuing maintenance info sections inside GDB, which will list a long list of sections and their memory mappings. For me .got is placed in this address range:

[21] 0x555555557fd0->0x555555558000 at 0x00002fd0: .got ALLOC LOAD DATA HAS_CONTENTS 

Which contains 0x555555557fe8.

To finally inspect the address of global_variable itself we may examine the contents of that memory by issuing x/xag 0x555555557fe8. Arguments xag of the x command deal with the size, format and type of data being inspected - for explanation invoke help x in GDB. On my machine the command returns:

0x555555557fe8: 0x7ffff7fc4020 <global_variable> 

On your machine it may only display the address and the data, without the "<global_variable>" helper, which probably comes from an extension i have installed called pwndbg. It is ok, because the value at that address is all we need. We now know that the global_variable is located in memory under the address 0x7ffff7fc4020. Now we may issue info proc mappings in GDB to find out what address range does this address belong to. My output is pretty long, but among all the ranges listed there is one of interest to us:

0x7ffff7fc4000 0x7ffff7fc5000 0x1000 0x3000 /home/user/test_got/dynamic.so 

The address is inside of that memory area, and GDB tells us that it comes from the dynamic.so library.

In case any of the outputs of said commands are different for you (change in a value is ok - i mean a fundamental difference like addresses not belonging to certain address ranges etc.) please provide more information about what exactly did you do to come to the conclusion that global_variable is stored in the .data section - what commands did you invoke and what outputs they produced.

5 Comments

On my system (gentoo linux) global _variable is firmly in the .bss section and not in .got, and the assembly does not use indirection --- unless I supply -fpic to compile the executable, then the indirection appears just like you show and the variable moves to .got. It looks like on your system gcc is configured to produce position-independent code in all cases. Not all Linux systems are configured this way (mine certainly isn't). See.
@n. 1.8e9-where's-my-share m. Original question used clang so reproducing it with gcc does not seem relevant, however on godbolt.org using clang does yield similar results to gcc. I also reproduced it with gcc on my system and found out that my clang version does have some PIC-related options enabled by default that gcc does not, so it might be a difference in environments in the end. I will wait for a response from the person that asked the question and will edit my answer accordingly.
@msaw328 Thanks for your detailed reply! On my system, as with @n. 1.8e9-where's-my-share m. 's comment, I obtain the same result as yours if adding the -fpie option. It seems that a binary is "Position Independent" if it access shared global variables via GOT. No matter where the shared variable is placed at link time, it can always work. By the way, I use clang because gcc on my Ubuntu docker image is weird —— it uses an outdated Intel feature.
@msaw328 clang and gcc produce essentially identical results both on my system and on godbolt.
@pkuGenuine I have researched the topic a bit more, while the other answer is relevant i think there is more to the topic than what he explained. I updated my answer with some more references and information i gathered. I left the original answer in tact below it as it might be of help to someone who finds the question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.