使用-fPIC 编译的共享库的内存映射和变量位置
Memory mapping and variable location of shared library compiled with -fPIC
我正在使用一个 Linux 框,我想在 运行 时间内找出 Position-Independent-Code 共享库中符号的地址,现在我可以根据一些实现观察,然而,我仍然对 program/library 加载有一些疑问(是的,我知道如何但我不知道为什么)。假设我们有以下两个 C 源文件:
// 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;
然后我们用下面的命令编译上面的代码:
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
和readelf -sW lib.so
显示global_field
符号:
Num: Value Size Type Bind Vis Ndx Name
...
8: 0000000000201028 4 OBJECT GLOBAL DEFAULT 21 global_field
...
并且readelf -lW lib.so
输出以下程序头:
...
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
现在我们运行程序,它输出如下:
global field(0x7ffff7dda028) = 1
并且cat /proc/<pid>/maps
输出如下:
...
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
...
抱歉,这里的代码有点多...现在我的问题是:
如你所见,程序头中有两个 LOAD
个段,但是有四个 内存映射,为什么多了两个映射?
对于两个LOAD
段,如何找出哪个段映射到哪个内存区域?有没有标准或手册?
符号global_field
的值为0000000000201028
(参见readelf -sW lib.so
的输出),然而,根据ELF标准:
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.
我知道这是位置无关代码,它不能是虚拟地址,必须是某种偏移量。用符号的值减去global_field
的地址:0x7ffff7dda028 - 0x201028 = 0x7ffff7bd9000
,偏移量似乎是基于最低内存映射的起始地址(见cat /proc/<pid>/maps
的输出)。但是,是否有任何标准告诉我们,如何以编程方式检测符号的值类型(虚拟地址或偏移量)?如果它是一个偏移量,为什么偏移量基于那个而不是它自己的内存区域(我猜它自己的区域是最后一个,因为它有写权限)?
As you see, there are TWO LOAD segments in program headers, but there are FOUR memory mappings, why there are two more mappings?
因为 GNU_RELRO
告诉动态加载器将第二个 PT_LOAD
段的前 0x208
字节 read-only.
如果你 link 带有 gcc -shared -o lib.so lib.o -Wl,-z,norelro
的库,你将只会得到 3 个映射......这仍然留下了为什么有 3 个而不是两个的问题?
您会注意到此映射:
7ffff7bda000-7ffff7dd9000 ---p 00001000 fd:02 18650951 /.../lib.so
实际上是进程space中的一个"hole"(不允许访问)。
您还会注意到第二个 PT_LOAD
(实际上对于两者)的对齐非常大:0x200000
.
这样做是为了适应 运行 1MB 页面的可能性。
如果您再次 re-link,使用 gcc -shared -o lib.so lib.o -Wl,-z,norelro,-z,max-page-size=4096
,您现在将只有您期望的两个映射。
默认情况下实际发生的是加载程序必须保留第一个和第二个PT_LOAD
之间的偏移量(否则二进制文件将无法正常工作).因此它在 kernel-selected 地址(通过 mmap(0, ...)
)创建了一个大映射(覆盖两个 PT_LOAD
段)。然后 mprotect
s 从第一个 PT_LOAD
结束的区域,直到 no-access 整个映射的结束。最后它 mmap
使用 MAP_FIXED
标志在所需地址处的第二个 PT_LOAD
段,在两个映射之间留下一个洞。
For the two LOAD segments, how to figure out which segment maps to which memory region? Is there any standard or any manual?
您可以很容易地从偏移量中分辨出来。偏移0
的映射对应第一个PT_LOAD
,空洞不对应任何东西,偏移00001000
的映射对应第二个PT_LOAD
.
it seems that the offset is based on the beginning address of the lowest memory mapping
正确:这是整个 lib.so
ELF 图像的重定位(由第一个 mmap(0, ...)
确定。该重定位应用于图像中的每个符号。
However, is there any standard telling us that, how to detect the symbols' value type (virtual address or offset) programmatically?
没有标准。但是你可以用dladdr找出"base address"(重定位)。特别是 dli_fbase; /* Base address at which shared object is loaded */
.
我正在使用一个 Linux 框,我想在 运行 时间内找出 Position-Independent-Code 共享库中符号的地址,现在我可以根据一些实现观察,然而,我仍然对 program/library 加载有一些疑问(是的,我知道如何但我不知道为什么)。假设我们有以下两个 C 源文件:
// 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;
然后我们用下面的命令编译上面的代码:
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
和readelf -sW lib.so
显示global_field
符号:
Num: Value Size Type Bind Vis Ndx Name
...
8: 0000000000201028 4 OBJECT GLOBAL DEFAULT 21 global_field
...
并且readelf -lW lib.so
输出以下程序头:
...
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
现在我们运行程序,它输出如下:
global field(0x7ffff7dda028) = 1
并且cat /proc/<pid>/maps
输出如下:
...
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
...
抱歉,这里的代码有点多...现在我的问题是:
如你所见,程序头中有两个
LOAD
个段,但是有四个 内存映射,为什么多了两个映射?对于两个
LOAD
段,如何找出哪个段映射到哪个内存区域?有没有标准或手册?符号
global_field
的值为0000000000201028
(参见readelf -sW lib.so
的输出),然而,根据ELF标准:
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.
我知道这是位置无关代码,它不能是虚拟地址,必须是某种偏移量。用符号的值减去global_field
的地址:0x7ffff7dda028 - 0x201028 = 0x7ffff7bd9000
,偏移量似乎是基于最低内存映射的起始地址(见cat /proc/<pid>/maps
的输出)。但是,是否有任何标准告诉我们,如何以编程方式检测符号的值类型(虚拟地址或偏移量)?如果它是一个偏移量,为什么偏移量基于那个而不是它自己的内存区域(我猜它自己的区域是最后一个,因为它有写权限)?
As you see, there are TWO LOAD segments in program headers, but there are FOUR memory mappings, why there are two more mappings?
因为 GNU_RELRO
告诉动态加载器将第二个 PT_LOAD
段的前 0x208
字节 read-only.
如果你 link 带有 gcc -shared -o lib.so lib.o -Wl,-z,norelro
的库,你将只会得到 3 个映射......这仍然留下了为什么有 3 个而不是两个的问题?
您会注意到此映射:
7ffff7bda000-7ffff7dd9000 ---p 00001000 fd:02 18650951 /.../lib.so
实际上是进程space中的一个"hole"(不允许访问)。
您还会注意到第二个 PT_LOAD
(实际上对于两者)的对齐非常大:0x200000
.
这样做是为了适应 运行 1MB 页面的可能性。
如果您再次 re-link,使用 gcc -shared -o lib.so lib.o -Wl,-z,norelro,-z,max-page-size=4096
,您现在将只有您期望的两个映射。
默认情况下实际发生的是加载程序必须保留第一个和第二个PT_LOAD
之间的偏移量(否则二进制文件将无法正常工作).因此它在 kernel-selected 地址(通过 mmap(0, ...)
)创建了一个大映射(覆盖两个 PT_LOAD
段)。然后 mprotect
s 从第一个 PT_LOAD
结束的区域,直到 no-access 整个映射的结束。最后它 mmap
使用 MAP_FIXED
标志在所需地址处的第二个 PT_LOAD
段,在两个映射之间留下一个洞。
For the two LOAD segments, how to figure out which segment maps to which memory region? Is there any standard or any manual?
您可以很容易地从偏移量中分辨出来。偏移0
的映射对应第一个PT_LOAD
,空洞不对应任何东西,偏移00001000
的映射对应第二个PT_LOAD
.
it seems that the offset is based on the beginning address of the lowest memory mapping
正确:这是整个 lib.so
ELF 图像的重定位(由第一个 mmap(0, ...)
确定。该重定位应用于图像中的每个符号。
However, is there any standard telling us that, how to detect the symbols' value type (virtual address or offset) programmatically?
没有标准。但是你可以用dladdr找出"base address"(重定位)。特别是 dli_fbase; /* Base address at which shared object is loaded */
.