linuxgccshared-librarieselfposition-independent-code

Memory mapping and variable location of shared library compiled with -fPIC


I am using a Linux box and I want to figure out the addresses of symbols inside a Position-Independent-Code shared library during runtime, now I can achieve that according to some observation, however, I still have some questions about the program/library loading (yeah, I know how but I don't know why). Assume we have the following two C source files:

// file: main.c
#include <stdio.h>

extern int global_field;
void main() {
    printf("global field(%p) = %d\n", &global_field, global_field);
}

// file: lib.c
int global_field = 1;

And we compile the above code with follow command:

gcc -fPIC -g -c lib.c -o lib.o      # note the -fPIC flag here
gcc -fPIC -g -c main.c -o main.o    # note the -fPIC flag here
gcc -shared -o lib.so lib.o
gcc -o main main.o ./lib.so

And readelf -sW lib.so shows the global_field symbol:

Num:    Value          Size Type    Bind   Vis      Ndx Name
  ...
  8: 0000000000201028     4 OBJECT  GLOBAL DEFAULT   21 global_field
  ...

And readelf -lW lib.so outputs the following program headers:

...
Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x00065c 0x00065c R E 0x200000
  LOAD           0x000df8 0x0000000000200df8 0x0000000000200df8 0x000234 0x000238 RW  0x200000
  DYNAMIC        0x000e18 0x0000000000200e18 0x0000000000200e18 0x0001c0 0x0001c0 RW  0x8
  NOTE           0x000190 0x0000000000000190 0x0000000000000190 0x000024 0x000024 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x000df8 0x0000000000200df8 0x0000000000200df8 0x000208 0x000208 R   0x1

And now we run the program, it outputs the following:

global field(0x7ffff7dda028) = 1

And cat /proc/<pid>/maps outputs the following:

...
7ffff7bd9000-7ffff7bda000 r-xp 00000000 fd:02 18650951    /.../lib.so
7ffff7bda000-7ffff7dd9000 ---p 00001000 fd:02 18650951    /.../lib.so
7ffff7dd9000-7ffff7dda000 r--p 00000000 fd:02 18650951    /.../lib.so
7ffff7dda000-7ffff7ddb000 rw-p 00001000 fd:02 18650951    /.../lib.so
...

Sorry it's a little too much code here... Now my questions are:

  1. As you see, there are TWO LOAD segments in program headers, but there are FOUR memory mappings, why there are two more mappings?

  2. For the two LOAD segments, how to figure out which segment maps to which memory region? Is there any standard or any manual?

  3. the symbol global_field's value is 0000000000201028 (see the output of readelf -sW lib.so), however, according to the ELF standard:

In executable and shared object files, st_value holds a virtual address. To make these files' symbols more useful for the runtime linker, the section offset (file interpretation) gives way to a virtual address (memory interpretation) for which the section number is irrelevant.

I know this is Position-Independent-Code, it CANNOT be virtual address and MUST be some kind of offset. Subtract global_field's address with the symbol's value: 0x7ffff7dda028 - 0x201028 = 0x7ffff7bd9000, it seems that the offset is based on the beginning address of the lowest memory mapping (see the output of cat /proc/<pid>/maps). However, is there any standard telling us that, how to detect the symbols' value type (virtual address or offset) programmatically? And if it's an offset, why should the offset base on that and why doesn't it base on its own memory region (I guess its own region is the last one, as it has the write permission)?


Solution

  • As you see, there are TWO LOAD segments in program headers, but there are FOUR memory mappings, why there are two more mappings?

    Because GNU_RELRO tells the dynamic loader to make the first 0x208 bytes of the second PT_LOAD segment read-only.

    If you link the library with gcc -shared -o lib.so lib.o -Wl,-z,norelro, you will only get 3 mappings ... Which still leaves the question of why are there 3 instead of two?

    You'll note that this mapping:

    7ffff7bda000-7ffff7dd9000 ---p 00001000 fd:02 18650951    /.../lib.so
    

    is actually a "hole" in the process space (no access is allowed). You'll also note that the alignment for second PT_LOAD (for both, actually) is very large: 0x200000.

    This is done to accomodate the possibility of running with 1MB pages.

    If you re-link again, with gcc -shared -o lib.so lib.o -Wl,-z,norelro,-z,max-page-size=4096, you will now only have the two mappings you are expecting.

    What actually happens for the default case is that the loader must preserve the offset between the first and the second PT_LOAD (or else the binary will not work correctly). So it creates a large mapping (covering both PT_LOAD segments) at kernel-selected address (via mmap(0, ...)). Then mprotects the region from end of the first PT_LOAD, until the end of the entire mapping with no-access. And finally it mmaps the second PT_LOAD segment at desired address using MAP_FIXED flag, leaving a hole between the two mappings.

    For the two LOAD segments, how to figure out which segment maps to which memory region? Is there any standard or any manual?

    You can tell pretty easily from offset. The mappings which have offset 0 correspond to the first PT_LOAD, the hole doesn't correspond to anything, and the mapping with offset 00001000 corresponds to the second PT_LOAD.

    it seems that the offset is based on the beginning address of the lowest memory mapping

    Correct: it's the relocation for the entire lib.so ELF image (determined by the very first mmap(0, ...). That relocation is applied to every symbol in the image.

    However, is there any standard telling us that, how to detect the symbols' value type (virtual address or offset) programmatically?

    There is no standard. But you can use dladdr to find out the "base address" (relocation). In particular, dli_fbase; /* Base address at which shared object is loaded */.