Linux动态链接(3)so文件映射地址
一、so文件的加载地址
so文件一般在程序刚启动的时候由动态连接器映射入可执行程序的地址空间,也可以通过dl库中的dlopen来映射入可执行程序的地址空间中,它的底层实现都是通过mmap来实现,这个没有什么好说的。通常来说,我们自己使用的so文件是很少主动确定so文件加载入内存的地址,所以so文件运行时映射在不同程序中的地址是不确定的。但是有些so文件是在生成的时候指明了自己的优选地址,例如我们常见的ld.so,libc.so文件:
动态链接库的加载地址
[tsecer@Harry loadaddr]$ readelf /lib/ld-linux.so.2 -l
Elf file type is DYN (Shared object file)
Entry point 0x1e8850
There are 7 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x001e8000 0x001e8000 0x1d58c 0x1d58c R E 0x1000
LOAD 0x01dc60 0x00206c60 0x00206c60 0x00bc0 0x00c80 RW 0x1000
DYNAMIC 0x01defc 0x00206efc 0x00206efc 0x000c8 0x000c8 RW 0x4
C库的加载地址
[tsecer@Harry loadaddr]$ readelf /lib/libc.so.6 -l
Elf file type is DYN (Shared object file)
Entry point 0x220d10
There are 10 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x0020a034 0x0020a034 0x00140 0x00140 R E 0x4
INTERP 0x13fc90 0x00349c90 0x00349c90 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x0020a000 0x0020a000 0x171bcc 0x171bcc R E 0x1000
LOAD 0x1721c0 0x0037d1c0 0x0037d1c0 0x027bc 0x057a8 RW 0x1000
它们的加载地址都不是从零地址开始,而是指明了自己的起始地址。
二、如何指定so加载地址
这个并没有找到标准应用例子,我使用的Fedora Core系统中的这两个库设置了起始地址,但是我自己使用官方的glibc对应版本没有编译出相同的设置有起始地址的共享库,所以猜测这些发行版本中对标准的C库进行了定制。简单看一下动态链接库使用的内置链接脚本,其中设置的主要选项是通过text-segment变量设置。估计我们现在使用的发行版是在这个基础上打的补丁。
可以测试一下:
[tsecer@Harry loadaddress]$ gcc load.c -fPIC -c -o load.o
[tsecer@Harry loadaddress]$ ld -fPIC -shared -o load.so load.o -Ttext-segment=0x12345678通过链接器选项--Ttext-segment选项设置加载地址为0x12345678。
[tsecer@Harry loadaddress]$ readelf -l load.so
Elf file type is DYN (Shared object file)
Entry point 0x123457c8
There are 4 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x12345000 0x12345000 0x007cd 0x007cd R E 0x1000加载地址为0x12345000,按照页面为单位对设置地址对齐。
LOAD 0x0007d0 0x123467d0 0x123467d0 0x0006c 0x0006c RW 0x1000
DYNAMIC 0x0007d0 0x123467d0 0x123467d0 0x00060 0x00060 RW 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Section to Segment mapping:
Segment Sections...
00 .hash .dynsym .dynstr .text
01 .dynamic .got.plt
02 .dynamic
03
三、加载地址为零的so文件地址如何确定
1、测试程序
[tsecer@Harry soloaddaddr]$ cat looper.c 创建一个无限循环的so文件,从而阻止该so退出,并且不需要依赖其它文件。
int looper(void)
{
while(1);
}
[tsecer@Harry soloaddaddr]$ gcc looper.c -fPIC -c -o looper.o
[tsecer@Harry soloaddaddr]$ ld looper.o -shared -o looper.so
[tsecer@Harry soloaddaddr]$ ./looper.so &
[1] 6238
[tsecer@Harry soloaddaddr]$ cat /proc/6238/maps
008c8000-008c9000 r-xp 00000000 fd:00 535095 /home/tsecer/CodeTest/soloaddaddr/looper.so
008c9000-008ca000 rw-p 00000000 fd:00 535095 /home/tsecer/CodeTest/soloaddaddr/looper.so
00af1000-00af2000 r-xp 00000000 00:00 0 [vdso]
bfc06000-bfc1b000 rw-p 00000000 00:00 0 [stack]
[tsecer@Harry soloaddaddr]$ ./looper.so &
[2] 6242
[tsecer@Harry soloaddaddr]$ cat /proc/6242/maps
00dbd000-00dbe000 r-xp 00000000 fd:00 535095 /home/tsecer/CodeTest/soloaddaddr/looper.so
00dbe000-00dbf000 rw-p 00000000 fd:00 535095 /home/tsecer/CodeTest/soloaddaddr/looper.so
00ef5000-00ef6000 r-xp 00000000 00:00 0 [vdso]
bf994000-bf9a9000 rw-p 00000000 00:00 0 [stack]
[tsecer@Harry soloaddaddr]$
两次程序加载的地址并不相同,有一定的随机性。
2、mmap(0)返回地址通常是高地址
[tsecer@Harry mmapzero]$ cat mmapzero.c
#include <sys/mman.h>
#include <stdio.h>
int main()
{
return printf("address is %x\n",mmap(0,0x1000*4,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANON,0,0));
}
[tsecer@Harry mmapzero]$ gcc mmapzero.c -o mmapzero.c.exe
[tsecer@Harry mmapzero]$ ./mmapzero.c.exe
address is b78c3000
[tsecer@Harry mmapzero]$
可以看到,当mmap的第一个参数为0的时候,它的返回地址并不是非常小的一个数值,而是接近堆栈最低位置附近的高端内存,也就是在用户态最高地址3G(0xC0000000)附近。而我们看到一个so文件的加载位置比较小,一般是在16M一下的地址。
3、为什么so优先使用低地址
我对使用的2.6左右的主线版本的内核进行调试,发现同样的looper.so文件在我自己编译的内核中加载地址和测试二、2中显示相同,也就是位于高端地址,这所以这个现象令人有些费解,所以猜测是我使用的fedora core版本对内核做了订制,所以下载对应内核版本,看到里面有一个linux-2.6-execshield.patch补丁文件,该文件对so文件的加载位置做了限制,但是这个补丁一直没有被合入内核中,即使新的2.6.37内核中也没有。后来在网上也看到了一个说明http://lwn.net/Articles/454949/,其中说了对于可执行文件的mmap将会优先使用低端内存。
其中作者给出了一个效果说明
在早期的linux中,通常的地址划分是基于堆栈两个部分开始。我们知道堆是向高地址增长、栈是向低地址增长,那么mmap的地址该如何确定呢?假设说用户态要执行一个mmap,那么它应该是选择到哪个地址空间?
在早期的内核中,这个起始地址是通过内核的mmap_base接口实现,通常是从0x80000000开始,也就是2G之上、3G以下为mmap地址,而可执行程序bss段之后到2G之间为brk系统调用预留地址空间。
之后的变迁mmap不再从地地址向高地址变迁,而是从高地址向地地址扩展,也就是越早执行mmap,它的地址越高(当然是在没有出现回绕之前)。
那么此时就有问题,那就是堆栈不再是可以无限增加,而只能是在运行时确定。
linux-2.6.37.1\arch\x86\mm\mmap.c
#define MIN_GAP (128*1024*1024UL + stack_maxrandom_size())
#define MAX_GAP (TASK_SIZE/6*5)
static unsigned long mmap_rnd(void)
{
unsigned long rnd = 0;
/*
* 8 bits of randomness in 32bit mmaps, 20 address space bits
* 28 bits of randomness in 64bit mmaps, 40 address space bits
*/
if (current->flags & PF_RANDOMIZE) {
if (mmap_is_ia32())
rnd = (long)get_random_int() % (1<<8);
else
rnd = (long)(get_random_int() % (1<<28));
}
return rnd << PAGE_SHIFT;
}
static unsigned long mmap_base(void)
{
unsigned long gap = rlimit(RLIMIT_STACK);该值默认为8M,其中MIN_GAP地址包含了stack_maxrandom_size,该值在32位系统下同样为8M。
if (gap < MIN_GAP)
gap = MIN_GAP;
else if (gap > MAX_GAP)
gap = MAX_GAP;
return PAGE_ALIGN(TASK_SIZE - gap - mmap_rnd());
}
/*
* Limit the stack by to some sane default: root can always
* increase this limit if needed.. 8MB seems reasonable.
*/
#define _STK_LIM (8*1024*1024)
/*
也就是在用户态最高地址向下减去堆栈可能占用空间,然后再减去一个随机值,该随机值地址空间为20bits,也即1M以内。但是由于堆栈栈顶起始位置本身都有随机值,所以不同进程的mmap地址也并不是在相差1M地址。
so文件一般在程序刚启动的时候由动态连接器映射入可执行程序的地址空间,也可以通过dl库中的dlopen来映射入可执行程序的地址空间中,它的底层实现都是通过mmap来实现,这个没有什么好说的。通常来说,我们自己使用的so文件是很少主动确定so文件加载入内存的地址,所以so文件运行时映射在不同程序中的地址是不确定的。但是有些so文件是在生成的时候指明了自己的优选地址,例如我们常见的ld.so,libc.so文件:
动态链接库的加载地址
[tsecer@Harry loadaddr]$ readelf /lib/ld-linux.so.2 -l
Elf file type is DYN (Shared object file)
Entry point 0x1e8850
There are 7 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x001e8000 0x001e8000 0x1d58c 0x1d58c R E 0x1000
LOAD 0x01dc60 0x00206c60 0x00206c60 0x00bc0 0x00c80 RW 0x1000
DYNAMIC 0x01defc 0x00206efc 0x00206efc 0x000c8 0x000c8 RW 0x4
C库的加载地址
[tsecer@Harry loadaddr]$ readelf /lib/libc.so.6 -l
Elf file type is DYN (Shared object file)
Entry point 0x220d10
There are 10 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x0020a034 0x0020a034 0x00140 0x00140 R E 0x4
INTERP 0x13fc90 0x00349c90 0x00349c90 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x0020a000 0x0020a000 0x171bcc 0x171bcc R E 0x1000
LOAD 0x1721c0 0x0037d1c0 0x0037d1c0 0x027bc 0x057a8 RW 0x1000
它们的加载地址都不是从零地址开始,而是指明了自己的起始地址。
二、如何指定so加载地址
这个并没有找到标准应用例子,我使用的Fedora Core系统中的这两个库设置了起始地址,但是我自己使用官方的glibc对应版本没有编译出相同的设置有起始地址的共享库,所以猜测这些发行版本中对标准的C库进行了定制。简单看一下动态链接库使用的内置链接脚本,其中设置的主要选项是通过text-segment变量设置。估计我们现在使用的发行版是在这个基础上打的补丁。
可以测试一下:
[tsecer@Harry loadaddress]$ gcc load.c -fPIC -c -o load.o
[tsecer@Harry loadaddress]$ ld -fPIC -shared -o load.so load.o -Ttext-segment=0x12345678通过链接器选项--Ttext-segment选项设置加载地址为0x12345678。
[tsecer@Harry loadaddress]$ readelf -l load.so
Elf file type is DYN (Shared object file)
Entry point 0x123457c8
There are 4 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x12345000 0x12345000 0x007cd 0x007cd R E 0x1000加载地址为0x12345000,按照页面为单位对设置地址对齐。
LOAD 0x0007d0 0x123467d0 0x123467d0 0x0006c 0x0006c RW 0x1000
DYNAMIC 0x0007d0 0x123467d0 0x123467d0 0x00060 0x00060 RW 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Section to Segment mapping:
Segment Sections...
00 .hash .dynsym .dynstr .text
01 .dynamic .got.plt
02 .dynamic
03
三、加载地址为零的so文件地址如何确定
1、测试程序
[tsecer@Harry soloaddaddr]$ cat looper.c 创建一个无限循环的so文件,从而阻止该so退出,并且不需要依赖其它文件。
int looper(void)
{
while(1);
}
[tsecer@Harry soloaddaddr]$ gcc looper.c -fPIC -c -o looper.o
[tsecer@Harry soloaddaddr]$ ld looper.o -shared -o looper.so
[tsecer@Harry soloaddaddr]$ ./looper.so &
[1] 6238
[tsecer@Harry soloaddaddr]$ cat /proc/6238/maps
008c8000-008c9000 r-xp 00000000 fd:00 535095 /home/tsecer/CodeTest/soloaddaddr/looper.so
008c9000-008ca000 rw-p 00000000 fd:00 535095 /home/tsecer/CodeTest/soloaddaddr/looper.so
00af1000-00af2000 r-xp 00000000 00:00 0 [vdso]
bfc06000-bfc1b000 rw-p 00000000 00:00 0 [stack]
[tsecer@Harry soloaddaddr]$ ./looper.so &
[2] 6242
[tsecer@Harry soloaddaddr]$ cat /proc/6242/maps
00dbd000-00dbe000 r-xp 00000000 fd:00 535095 /home/tsecer/CodeTest/soloaddaddr/looper.so
00dbe000-00dbf000 rw-p 00000000 fd:00 535095 /home/tsecer/CodeTest/soloaddaddr/looper.so
00ef5000-00ef6000 r-xp 00000000 00:00 0 [vdso]
bf994000-bf9a9000 rw-p 00000000 00:00 0 [stack]
[tsecer@Harry soloaddaddr]$
两次程序加载的地址并不相同,有一定的随机性。
2、mmap(0)返回地址通常是高地址
[tsecer@Harry mmapzero]$ cat mmapzero.c
#include <sys/mman.h>
#include <stdio.h>
int main()
{
return printf("address is %x\n",mmap(0,0x1000*4,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANON,0,0));
}
[tsecer@Harry mmapzero]$ gcc mmapzero.c -o mmapzero.c.exe
[tsecer@Harry mmapzero]$ ./mmapzero.c.exe
address is b78c3000
[tsecer@Harry mmapzero]$
可以看到,当mmap的第一个参数为0的时候,它的返回地址并不是非常小的一个数值,而是接近堆栈最低位置附近的高端内存,也就是在用户态最高地址3G(0xC0000000)附近。而我们看到一个so文件的加载位置比较小,一般是在16M一下的地址。
3、为什么so优先使用低地址
我对使用的2.6左右的主线版本的内核进行调试,发现同样的looper.so文件在我自己编译的内核中加载地址和测试二、2中显示相同,也就是位于高端地址,这所以这个现象令人有些费解,所以猜测是我使用的fedora core版本对内核做了订制,所以下载对应内核版本,看到里面有一个linux-2.6-execshield.patch补丁文件,该文件对so文件的加载位置做了限制,但是这个补丁一直没有被合入内核中,即使新的2.6.37内核中也没有。后来在网上也看到了一个说明http://lwn.net/Articles/454949/,其中说了对于可执行文件的mmap将会优先使用低端内存。
其中作者给出了一个效果说明
If 16 Mbs are over, we fallback to the old allocation algorithm. Without the patch: $ ldd /bin/ls linux-gate.so.1 => (0xf779c000) librt.so.1 => /lib/librt.so.1 (0xb7fcf000) libtermcap.so.2 => /lib/libtermcap.so.2 (0xb7fca000) libc.so.6 => /lib/libc.so.6 (0xb7eae000) libpthread.so.0 => /lib/libpthread.so.0 (0xb7e5b000) /lib/ld-linux.so.2 (0xb7fe6000) With the patch: $ ldd /bin/ls linux-gate.so.1 => (0xf772a000) librt.so.1 => /lib/librt.so.1 (0x0014a000) libtermcap.so.2 => /lib/libtermcap.so.2 (0x0015e000) libc.so.6 => /lib/libc.so.6 (0x00162000) libpthread.so.0 => /lib/libpthread.so.0 (0x00283000) /lib/ld-linux.so.2 (0x00131000)5、linux下用户态程序地址空间变迁
从文章的说明来看,它主要是为了防止缓冲区溢出攻击,避免输入特殊地址字符串,该地址为一个高地址的so文件地址,从而执行一些字符串形式
的指令。
4、具体如何实现
unsigned long
-get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
- unsigned long pgoff, unsigned long flags)
+get_unmapped_area_prot(struct file *file, unsigned long addr, unsigned long len,
+ unsigned long pgoff, unsigned long flags, int exec)
{
unsigned long (*get_area)(struct file *, unsigned long,
unsigned long, unsigned long, unsigned long);
unsigned long error = arch_mmap_check(addr, len, flags);
if (error)
return error;
/* Careful about overflows.. */
if (len > TASK_SIZE)
return -ENOMEM;
- get_area = current->mm->get_unmapped_area;
+ if (exec && current->mm->get_unmapped_exec_area)
+ get_area = current->mm->get_unmapped_exec_area;
+ else
+ get_area = current->mm->get_unmapped_area;
+
if (file && file->f_op && file->f_op->get_unmapped_area)
get_area = file->f_op->get_unmapped_area;
addr = get_area(file, addr, len, pgoff, flags);
@@ -1473,8 +1497,76 @@ get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
return arch_rebalance_pgtables(addr, len);
}
这里实现的思路就是在mm_struct结构中再增加一个get_unmapped_exec_area接口(相对于之前的get_unmapped_area等接口),从而当mmap的PORT
中包含有PROT_EXEC时使用专门的接口,从低端地址查找内存区域。
在早期的linux中,通常的地址划分是基于堆栈两个部分开始。我们知道堆是向高地址增长、栈是向低地址增长,那么mmap的地址该如何确定呢?假设说用户态要执行一个mmap,那么它应该是选择到哪个地址空间?
在早期的内核中,这个起始地址是通过内核的mmap_base接口实现,通常是从0x80000000开始,也就是2G之上、3G以下为mmap地址,而可执行程序bss段之后到2G之间为brk系统调用预留地址空间。
之后的变迁mmap不再从地地址向高地址变迁,而是从高地址向地地址扩展,也就是越早执行mmap,它的地址越高(当然是在没有出现回绕之前)。
那么此时就有问题,那就是堆栈不再是可以无限增加,而只能是在运行时确定。
linux-2.6.37.1\arch\x86\mm\mmap.c
#define MIN_GAP (128*1024*1024UL + stack_maxrandom_size())
#define MAX_GAP (TASK_SIZE/6*5)
static unsigned long mmap_rnd(void)
{
unsigned long rnd = 0;
/*
* 8 bits of randomness in 32bit mmaps, 20 address space bits
* 28 bits of randomness in 64bit mmaps, 40 address space bits
*/
if (current->flags & PF_RANDOMIZE) {
if (mmap_is_ia32())
rnd = (long)get_random_int() % (1<<8);
else
rnd = (long)(get_random_int() % (1<<28));
}
return rnd << PAGE_SHIFT;
}
static unsigned long mmap_base(void)
{
unsigned long gap = rlimit(RLIMIT_STACK);该值默认为8M,其中MIN_GAP地址包含了stack_maxrandom_size,该值在32位系统下同样为8M。
if (gap < MIN_GAP)
gap = MIN_GAP;
else if (gap > MAX_GAP)
gap = MAX_GAP;
return PAGE_ALIGN(TASK_SIZE - gap - mmap_rnd());
}
/*
* Limit the stack by to some sane default: root can always
* increase this limit if needed.. 8MB seems reasonable.
*/
#define _STK_LIM (8*1024*1024)
/*
也就是在用户态最高地址向下减去堆栈可能占用空间,然后再减去一个随机值,该随机值地址空间为20bits,也即1M以内。但是由于堆栈栈顶起始位置本身都有随机值,所以不同进程的mmap地址也并不是在相差1M地址。