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:
As you see, there are TWO LOAD
segments in program headers, but there are FOUR memory mappings, why there are two more mappings?
For the two LOAD
segments, how to figure out which segment maps to which memory region? Is there any standard or any manual?
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)?
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 mprotect
s the region from end of the first PT_LOAD
, until the end of the entire mapping with no-access. And finally it mmap
s 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 */
.