1. 程式人生 > >Linux動態鏈接(3)so文件映射地址

Linux動態鏈接(3)so文件映射地址

tar so文件 進程 test 版本 0x13 vdso hdr 退出

一、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將會優先使用低端內存。
其中作者給出了一個效果說明
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)
從文章的說明來看,它主要是為了防止緩沖區溢出攻擊,避免輸入特殊地址字符串,該地址為一個高地址的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時使用專門的接口,從低端地址查找內存區域
5、linux下用戶態程序地址空間變遷
在早期的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地址。

Linux動態鏈接(3)so文件映射地址