Global Offset Tables

You might have noticed a critical problem with relocations when thinking about the goals of a shared library. We mentioned previously that the big advantage of a shared library with virtual memory is that multiple programs can use the code in memory by sharing of pages.

The problem stems from the fact that libraries have no guarantee about where they will be put into memory. The dynamic linker will find the most convenient place in virtual memory for each library required and place it there. Think about the alternative if this were not to happen; every library in the system would require its own chunk of virtual memory so that no two overlapped. Every time a new library were added to the system it would require allocation. Someone could potentially be a hog and write a huge library, not leaving enough space for other libraries! And chances are, your program doesn't ever want to use that library anyway.

Thus, if you modify the code of a shared library with a relocation, that code no longer becomes sharable. We've lost the advantage of our shared library.

Below we explain the mechanism for doing this.

The Global Offset Table

So imagine the situation where we take the value of a symbol. With only relocations, we would have the dynamic linker look up the memory address of that symbol and re-write the code to load that address.

A fairly straight forward enhancement would be to set aside space in our binary to hold the address of that symbol, and have the dynamic linker put the address there rather than in the code directly. This way we never need to touch the code part of the binary.

The area that is set aside for these addresses is called the Global Offset Table, or GOT. The GOT lives in a section of the ELF file called .got.

Figure 9-1. Memory access via the GOT

The GOT is private to each process, and the process must have write permissions to it. Conversely the library code is shared and the process should have only read and execute permissions on the code; it would be a serious security breach if the process could modify code.

The GOT in action

Example 9-6. Using the GOT

    $ cat got.c
    extern int i;
    
    void test(void)
  5 {
            i = 100;
    }
    
    $ gcc -nostdlib  -shared -o got.so ./got.c
 10 
    $ objdump --disassemble ./got.so
    
    ./got.so:     file format elf64-ia64-little
    
 15 Disassembly of section .text:
    
    0000000000000410 <test>:
     410:   0d 10 00 18 00 21       [MFI]       mov r2=r12
     416:   00 00 00 02 00 c0                   nop.f 0x0
 20  41c:   81 09 00 90                         addl r14=24,r1;;
     420:   0d 78 00 1c 18 10       [MFI]       ld8 r15=[r14]
     426:   00 00 00 02 00 c0                   nop.f 0x0
     42c:   41 06 00 90                         mov r14=100;;
     430:   11 00 38 1e 90 11       [MIB]       st4 [r15]=r14
 25  436:   c0 00 08 00 42 80                   mov r12=r2
     43c:   08 00 84 00                         br.ret.sptk.many b0;;
    
    $ readelf --sections ./got.so
    There are 17 section headers, starting at offset 0x640:
 30 
    Section Headers:
      [Nr] Name              Type             Address           Offset
           Size              EntSize          Flags  Link  Info  Align
      [ 0]                   NULL             0000000000000000  00000000
 35        0000000000000000  0000000000000000           0     0     0
      [ 1] .hash             HASH             0000000000000120  00000120
           00000000000000a0  0000000000000004   A       2     0     8
      [ 2] .dynsym           DYNSYM           00000000000001c0  000001c0
           00000000000001f8  0000000000000018   A       3     e     8
 40   [ 3] .dynstr           STRTAB           00000000000003b8  000003b8
           000000000000003f  0000000000000000   A       0     0     1
      [ 4] .rela.dyn         RELA             00000000000003f8  000003f8
           0000000000000018  0000000000000018   A       2     0     8
      [ 5] .text             PROGBITS         0000000000000410  00000410
 45        0000000000000030  0000000000000000  AX       0     0     16
      [ 6] .IA_64.unwind_inf PROGBITS         0000000000000440  00000440
           0000000000000018  0000000000000000   A       0     0     8
      [ 7] .IA_64.unwind     IA_64_UNWIND     0000000000000458  00000458
           0000000000000018  0000000000000000  AL       5     5     8
 50   [ 8] .data             PROGBITS         0000000000010470  00000470
           0000000000000000  0000000000000000  WA       0     0     1
      [ 9] .dynamic          DYNAMIC          0000000000010470  00000470
           0000000000000100  0000000000000010  WA       3     0     8
      [10] .got              PROGBITS         0000000000010570  00000570
 55        0000000000000020  0000000000000000 WAp       0     0     8
      [11] .sbss             NOBITS           0000000000010590  00000590
           0000000000000000  0000000000000000   W       0     0     1
      [12] .bss              NOBITS           0000000000010590  00000590
           0000000000000000  0000000000000000  WA       0     0     1
 60   [13] .comment          PROGBITS         0000000000000000  00000590
           0000000000000026  0000000000000000           0     0     1
      [14] .shstrtab         STRTAB           0000000000000000  000005b6
           000000000000008a  0000000000000000           0     0     1
      [15] .symtab           SYMTAB           0000000000000000  00000a80
 65        0000000000000258  0000000000000018          16    12     8
      [16] .strtab           STRTAB           0000000000000000  00000cd8
           0000000000000045  0000000000000000           0     0     1
    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings)
 70   I (info), L (link order), G (group), x (unknown)
      O (extra OS processing required) o (OS specific), p (processor specific);

Above we create a simple shared library which refers to an external symbol. We do not know the address of this symbol at compile time, so we leave it for the dynamic linker to fix up at runtime.

But we want our code to remain sharable, in case other processes want to use our code as well.

The disassembly reveals just how we do this with the .got. On IA64 (the architecture which the library was compiled for) the register r1 is known as the global pointer and always points to where the .got section is loaded into memory.

If we have a look at the readelf output we can see that the .got section starts 0x10570 bytes past where library was loaded into memory. Thus if the library were to be loaded into memory at address 0x6000000000000000 the .got would be at 0x6000000000010570, and register r1 would always point to this address.

Working backwards through the disassembly, we can see that we store the value 100 into the memory address held in register r15. If we look back we can see that register 15 holds the value of the memory address stored in register 14. Going back one more step, we see we load this address is found by adding a small number to register 1. The GOT is simply a big long list of entries, one for each external variable. This means that the GOT entry for the external variable i is stored 24 bytes (that is 3 64 bit addresses).

Example 9-7. Relocations against the GOT

    $ readelf --relocs ./got.so
    
    Relocation section '.rela.dyn' at offset 0x3f8 contains 1 entries:
      Offset          Info           Type           Sym. Value    Sym. Name + Addend
  5 000000010588  000f00000027 R_IA64_DIR64LSB   0000000000000000 i + 0;

We can also check out the relocation for this entry too. The relocation says "replace the value at offset 10588 with the memory location that symbol i is stored at".

We know that the .got starts at offset 0x10570 from the previous output. We have also seen how the code loads an address 0x18 (24 in decimal) past this, giving us an address of 0x10570 + 0x18 = 0x10588 ... the address which the relocation is for!

So before the program begins, the dynamic linker will have fixed up the relocation to ensure that the value of the memory at offset 0x10588 is the address of the global variable i!