1. 程式人生 > >計算機科學基礎知識(四)動態庫和位置無關代碼

計算機科學基礎知識(四)動態庫和位置無關代碼

iba type 限制 body print 我們 attr 動態庫文件 約束

一、前言

本文主要描述了動態庫以及和動態庫有緊密聯系的位置無關代碼的相關資訊。首先介紹了動態庫和位置無關代碼的源由,了解這些背景知識有助於理解和學習動態庫。隨後,我們通過加-fPIC和不加這個編譯選項分別編譯出兩個relocatable object file,看看編譯器是如何生成位置無關代碼的。最後,我們自己動手編寫一個簡單的動態庫,並解析了一些symbol Visibility、動態符號表等一些相關基本概念。

本文中的描述是基於ARM MCU,GNU/linux平臺而言的,本文是個人對動態庫的理解,如果有錯誤,請及時指出。

二、背景介紹

位置無關代碼實際上是和動態庫概念緊密聯系在一起的,本章首先描述為何會提出動態庫的概念,然後解釋動態庫為何需要編譯成PIC的代碼。

1、為何會提出動態庫的概念?

引入靜態庫後,解決了一些問題,但是仍然存在下面的弊端:

(1)任何對靜態庫的升級都需要rebuild(或者叫做relink)的過程

(2)通用的函數(例如標準IO函數scanf和printf)存在於各個靜態鏈接的程序中,導致編譯後的靜態可執行程序的size比較大,在各個可執行程序中,這些通用的函數代碼是重復的,占用了磁盤和內存資源

正因為如此,動態庫和動態鏈接的概念被提出來來解決這些問題。動態庫也是一種ELF格式的對象文件,在運行的時候,它可以被加載到任何的地址執行。

2、動態庫為何需要編譯成PIC的代碼?

無論是動態庫還是靜態庫,其本質都是代碼共享。對於靜態庫,其代碼以及數據都是在各個靜態鏈接的可執行文件中有一份copy,所有符號的地址已經確定,因此在loading的時候,OS會比較輕松。不過這種代碼共享無法在run time的時候共享代碼,從而導致了資源的浪費。當然,它的好處就是簡單、速度快(無需dynamic linker來重定位符號)。對於靜態編譯,static linker將多個編譯單元(.o文件和庫文件)整合成一個模塊,因此,進入run time,實際上只有一個執行模塊。對於動態鏈接,在run time的時候,除了可執行文件這個模塊,該可執行文件所依賴的各個動態庫也是一個個的運行模塊,這時候,可執行文件調用動態庫的符號實際上是就是需要引用其他運行模塊的符號了。對於可執行文件而言,loader將其加載到哪個地址並不關鍵,反正每個進程都有自己獨一無二的地址空間,可執行文件可以mapping到各自virtual memory space的相同地址也無妨,不過對於動態庫模塊而言,就有些麻煩了。如果我們不將動態庫編譯成PIC的也就是意味著loader一定要把動態庫加載到某個特定的地址(該地址編譯的時候就確定了)上它才可以正確的執行。假設我們有A B C D四個動態庫,假設程序P1依賴A B兩個動態庫,P2依賴C D兩個動態庫,那麽A B和C D的動態庫的加載地址有重疊也沒有關系,P1和P2可以同時運行。但是如果有一個新的程序P3依賴A B C D四個動態庫,那麽前面為動態庫分配的加載地址就不能正常工作了。當然,重新為這四個動態庫分配load address(讓地址不重疊)也是ok的,但是這樣一來,P1雖然沒有使用C D這兩個動態庫,但是P1的地址空間還是要保留C D動態庫的那段地址,對於地址這樣寶貴資源,這麽浪費簡直是暴殄天物。更重要的是:這樣的機制實際上對進程虛擬地址的管理就變得非常復雜了,假設A B C D是分配了一段連續的地址,如果C動態庫更新了,size變大了,原本分配的地址空間不夠了,怎麽辦?我們必須再尋找一個新的地址段來加載C動態庫。如果系統只有四個動態庫起始還是OK的,如果動態庫非常非常多……怎麽辦?更糟的是:不同的系統使用不同的動態庫,管理起來更令人頭痛

最好的方法就是將動態庫編譯成PIC(Position Independent Code),也就是說動態庫可以被加載到任何地址並正確運行。

三、動手實踐:觀察PIC的.o文件的反匯編結果

1、源代碼foo.c

#include <stdio.h>
int xxx = 0x1234;
int yyy;
int foo(void)
{
yyy = 0x5678;
printf("xxx=%x yyy=%x\n", xxx, yyy);
return xxx;
}

2、觀察foo.o文件中的符號定位信息

使用arm-linux-gcc –c foo.c將source code編譯成relocatable file。我們來看看正文段中的relocation信息:

00000030 00000e1c R_ARM_CALL 00000000 printf
00000044 00000f02 R_ARM_ABS32 00000004 yyy
0000004c 00000c02 R_ARM_ABS32 00000000 xxx

R_ARM_ABS32是一種ARM平臺上的absolute 32-bit relocation,在32 bit的ARM平臺上,這種重定位的方式是沒有任何約束的,可以將地址重定位到4G地址空間的任何位置。具體實現方式需要參考反編譯的匯編代碼,我們來看看匯編代碼是如何訪問yyy這個數據的:

……
8: e59f2034 ldr r2, [pc, #52] ; 44 <.text+0x44>
c: e59f3034 ldr r3, [pc, #52] ; 48 <.text+0x48>
10: e5823000 str r3, [r2]
……
44: 00000000 .word 0x00000000
48: 00005678 .word 0x00005678

具體做法非常的簡單,在這段代碼的後面(也是.text section的一部分)給出一個32-bit的跳板memory(上面黑色加粗的那一行),位於<.text+0x44>,這個memory用於保存yyy符號的運行地址。由於同在一個正文段,因此它們之間的offset是確定的,使用“ldr r2, [pc, #52] ”這樣的PC-relative的訪問指令可以訪問到yyy變量的地址,通過“str r3, [r2]”可以將yyy變量的內容保存到r3中。

下面我們我們再看看函數符號的訪問。R_ARM_CALL這種類型的重定位信息主要用於函數調用的(對應的ARM指令就是BL和BLX),實現也很簡單,如下:

……
30: ebfffffe bl 0

……

BL指令是一個PC-relative指令,會將控制權交給相對於當前PC值的一個地址上去(同時設定lr寄存器),bl這條指令的0~23個bit(用imm24表示))用來表示相對與PC的偏移地址,最終跳轉到的地址是PC+(imm24在低位添加00b,然後做符號擴展),也就是正負32M的區域(註意:BL不能任意跳轉4G範圍的地址空間)。之所以添加兩個0是因為offset地址總是4字節對齊的。

對於靜態鏈接,很簡單,雖然那些重定位信息在正文段,但是沒有關系,在程序loading之前,static linker可以修改正文段的內容。

3、編譯PIC的.o文件並觀察

編譯成位置無關代碼也就意味著這段代碼多半是動態庫的一部分,需要動態加載到一個編譯時候未知的地址上。也就是說上文中使用的方法已經不行了,編譯時候符號的地址還是不確定的,因此static linker無法將地址填入到.text section中。在loading的時候,雖然知道了符號runtime address,但是正文段是read only的,也無法修改。怎麽辦呢?我們來一起看看程序如何實現。

使用arm-linux-gcc -fPIC–c foo.c將source code編譯成relocatable file。我們來看看正文段中的relocation信息:

Relocation section ‘.rel.text‘ at offset 0x4e0 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
00000048 00000f1b R_ARM_PLT32 00000000 printf
00000064 00001019 R_ARM_BASE_PREL 00000000 _GLOBAL_OFFSET_TABLE_
00000068 0000111a R_ARM_GOT_BREL 00000004 yyy
00000070 00000d1a R_ARM_GOT_BREL 00000000 xxx

我們首先看看_GLOBAL_OFFSET_TABLE_這個符號,看起來和傳說中的GOT(Global Offset Table)有關。那麽什麽是GOT呢?它有什麽作用呢?我們先回到c代碼,思考一下對xxx符號的訪問。這時候,我們能確定xxx的runtime address嗎?當然不能,離loading還遠著呢,這時候我們能確定訪問xxx的代碼(.text section中)和xxx符號(.data section)之間offset嗎?也不能,因為還有多個.o文件最後被link成一個動態庫。怎麽辦?我們必須借助一個橋梁來讓數據訪問變得Position Independent,這個橋梁就是GOT(Global Offset Table)。當然GOT必須是可讀可寫的,因為後續在run time的時候還要修改其內容。_GLOBAL_OFFSET_TABLE_就是定義了GOT在memory中的位置。因此64那個位置的重定位信息和GOT相關,R_ARM_BASE_PREL這個relocation type則說明這個重定位信息說明該位置保存了GOT offset。由於目前還是.o文件,還沒有確定最後GOT信息,因此需要這個relocation的信息,一旦完成動態庫的編譯,這個relocation entry就不需要了。

R_ARM_GOT_BREL這個type說明這個重定位信息是一個描述GOT entry和GOT起始位置的offset。例如:yyy這個符號還需要relocation,那麽它的relocation位於正文段offset是0x68的位置,其內容保存了yyy符號在GOT entry中的地址和GOT起始位置的偏移。OK,有了這些鋪墊,可以看看程序對yyy這個數據是如何訪問的:

……
c: e59f4050 ldr r4, [pc, #80] ; 64 <.text+0x64>
10: e08f4004 add r4, pc, r4 ---------------獲得GOT的起始位置的地址
14: e59f304c ldr r3, [pc, #76] ; 68 <.text+0x68> -----獲得yyy符號在GOT中的offset
18: e7942003 ldr r2, [r4, r3] --------------獲得yyy符號的runtime address
1c: e59f3048 ldr r3, [pc, #72] ; 6c <.text+0x6c>
20: e5823000 str r3, [r2] ---------------設定yyy符號的內容
……
64: 0000004c .word 0x0000004c-----GOT offset
68: 00000000 .word 0x00000000-----yyy的地址在GOT中的偏移
6c: 00005678 .word 0x00005678

由此可見,PIC的代碼對全局數據的訪問都是通過GOT來完成的,從而做到了位置無關。

四、動手實踐:觀察動態庫的反匯編結果

1、如何生成動態庫?

我們準備動手做一個動態庫了,先看source code,一如既往的簡單(註意:我們不建議導出動態庫中的數據符號,這裏主要是為了描述動態庫的概念而這麽做的):

int xxx = 0x1234;

int yyy;
int foo(void)
{
yyy = 0x5678;
return xxx;
}

通過下面的命令可以編譯出一個libfoo的動態庫:

arm-linux-gcc -shared -fPIC -o libfoo.so foo.c

-shared告知gcc生成share object文件,而-fPIC則告訴gcc請生成位置無關代碼。

2、觀察符號表的變化

我們在relocatable object中已經對符號表進行了描述:對靜態編譯的程序而言,.o文件中的符號表一是要對外宣稱自己定義了哪些符號,另外一個是向外宣布自己引用了哪些符號,需要其他模塊來支持。有了這些信息,static linker才能整合各個relocatable object file中的資源,互通有無,最後融合成一個靜態的可執行程序。因此,實際上,對於靜態的可執行程序,在加載執行的時候,其符號表已經沒有任何意義了(不過可以方便debug),對於CPU而言,其執行就是要知道地址就OK了(靜態編譯程序所有的符號都已經定位了),符號什麽的它不關心,因此,實際上符號表可以刪除。如果你願意,你可以通過strip命令來進行實驗,看看tripped和not stripped的elf文件有什麽不同。

然而,計算機科學的發展是不斷前進的,當有了動態庫之後,符號表會怎樣呢?我們自己可以動手生成一個動態鏈接的可執行程序或者動態庫並觀察其中的符號表信息(恰好上一節已經生成一個libfoo.so,就它吧)。通過readelf工具,我們可以看到,動態鏈接的程序中有兩個符號表,一個是大家之前就熟悉的.symtab section(我們稱之符號表),另外一個就是.dynsym section(動態符號表)。這兩個符號表都有自己對應的string table,分別是.strtab和.dynstr section。

.symtab section我們前面的文章都有描述,為何又增加了一個.dynsym section呢?我們先假設我們編譯出來的動態庫只有一個符號表,那麽當使用strip命令刪除符號表以及對應的字符串表之後會怎樣?當其他程序調用該動態庫提供的接口API函數的時候,dynamic linker還能找到對應的API函數符號嗎?當然不行,符號表都刪除了還想怎樣。靜態鏈接的程序之所以可以strip掉符號表以及對應的字符串表那是因為程序中所有符號都已經塵埃落定(所有符號已經重定位),因此strip後也毫無壓力,但是動態鏈接的情況下,程序中的沒有定位的符號以及動態庫中宣稱的符號都需要有一個特別的符號表(是正常符號表的子集)來保存動態鏈接符號的信息,這個表就是動態連接符號表(.dynsym section)。

OK,最後總結一下:符號表(.symtab section)是指導static linker工作的,運行的時候可以不需要。動態符號表(.dynsym section)是給dynamic linker用的,程序(或者動態庫)運行的時候,dynamic linker用動態符號表的信息來定位符號。

3、Binding Property和Symbol Visibility

我們在講述relocatable object file的時候已經給出了binding屬性(binding property)的解釋。一個符號可能有global、local和weak三種binding property。這個binding property主要是被static linker用來進行.o之間的符號解析(symbol resolution)的。Bind屬性之外還有一個屬性我們一直沒有描述(通過readelf觀察符號表的時候,該屬性對應列的名字是Vis的那個),我們稱之Symbol Visibility或者符號的可見性。之所以前面的文章中沒有描述主要是因為Symbol visibility是和動態庫以及動態鏈接相關的。

當引入動態連接和動態庫的概念之後,代碼和數據的共享會變得復雜一些。和binding property不一樣,Symbol Visibility是針對運行模塊(動態鏈接的可執行程序或者動態庫)之間的相互引用。例如我們有A.o B.o C.o三個編譯模塊,static linker將這三個.o文件link成一個libABC.so文件。A.o模塊要調用B.o中的一個函數bb,那麽bb函數就一定需要是一個GLOBAL類型的,但是bb函數並不是動態庫libABC.so的接口API(或者稱之export symbol),也就是說,為了更好的封裝性,我們希望bb這個函數對外不可見,dynamic linker看不到這個符號,bb不參與動態符號解析。如果動態庫導出所有的符號,那麽,在動態鏈接的時候,符號沖突的可能性就非常的大,特別是對於那些大型項目,可能該項目涉及的每個動態庫都是由不同team負責的。除了模塊的封裝性之外,Symbol Visibility也是和程序的性能有關。如果導出太多的符號,除了占用更多的內存,還意味著增加loading time和dynamic linking time。

看,不控制Symbol Visibility的危害還是很大D,這時候閱讀本文的你估計一定會問:那麽控制Symbol Visibility哪家強呢?我推薦使用大殺器static關鍵字,簡單,實用,人人會。給function或者全局變量加上static關鍵字,別說是對dynamic linker(運行模塊之間的引用)進行了限制,就是static linker(.o 文件之間的引用)也是拿他毫無辦法。當然,缺點也很明顯:不能在動態庫的多個.o之間共享。在這種場景下,我們需要求助其他方法了,對於gcc,我們可以用下面的方法:

符號類型 符號名字 __attribute__ ((visibility ("xxx")));

其中xxx指明了該符號的Symbol Visibility屬性,Symbol Visibility屬性可以設定為:

(1)DEFAULT(雖然命名是default,但是有些public的味道)。該屬性的符號被導出,該符號可以被其他運行模塊訪問

(2)PROTECTED。同DEFAULT,不過該符號不能被overridden。也就是說,如果一個動態庫中的符號是PROTECTED,那麽動態庫中的代碼訪問該符號是享有優先權的,即便其他的運行模塊定義了同名的符號。

(3)HIDDEN。HIDDEN的符號不會被導出,不參與動態鏈接。

(4)INTERNAL。其他運行模塊不能訪問該類型的符號。

回到上一節描述的這個source code,其中有三個符號:xxx、yyy和foo,都是被導出的,可以被其他的模塊調用。如果你有興趣,可以自己試著控制符號的visibility,看看效果如何。

4、動態庫文件的加載

libfoo這個shared object elf文件的加載是根據Program header進行的。在ELF file header中可以看到該動態庫共計4個program header,如下:

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x005c0 0x005c0 R E 0x8000
LOAD 0x0005c0 0x000085c0 0x000085c0 0x00118 0x00120 RW 0x8000

DYNAMIC 0x0005cc 0x000085cc 0x000085cc 0x000e0 0x000e0 RW 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4

帶有LOAD標記的那些program header entry會被mapping到進程地址空間上去。第一項是code segment,由於動態庫的代碼是PIC的,因此其VirtAddr和PhysAddr都是0,表示可以運行在任意地址上。第二項是data segment,在實際中,動態庫的code和data segment都是連續加載的,因此,如果code segment的run time地址是0的話,那麽data segment的地址應該是0x5c0,不過由於code segment是0x8000對齊的,因此data segment的地址被設定為0x85c0。當然,如果實際該動態庫被加載到了進程的X虛擬地址上的話,data segment的runtime地址應該是X + 0x85c0。對於動態庫而言,其code segment可以被多個進程共享,也就是說,雖然code segment被加載到不同的進程的不同的虛擬地址空間,但是其物理地址是一樣的,只不過各個進程設定自己的page table就OK了。對於code segment,各個進程都有自己的副本,不可能共享的。

沒有LOAD標記,這說明第三項和第四項(DYNAMIC這個entry下一節描述)都是和進程加載無關的(不占用進程虛擬地址空間)。GNU_STACK是用來告訴操作系統,當加載ELF文件的時候,如果控制stack的屬性。這是和系統安全相關(通過stack來攻擊系統),我們在relocatable object file的時候已經描述,這裏略過(https://wiki.gentoo.org/wiki/Hardened/GNU_stack_quickstart中有更詳細的信息)。

5、如何找到動態鏈接的信息

和靜態鏈接的可執行序程序相比,DYNAMIC那個program header entry是動態庫文件特有的。既然是動態庫,當然要參與動態鏈接的過程,因此動態庫的ELF文件需要提供一些dynamic linking信息給OS以及dynamic linker,DYNAMIC那個program header entry就是起這個作用的。dynamic segment只包含了一個section,名字是.dynamic。需要註意的是.dynamic section也是data segment的一部分被加載到了進程的地址空間中。下面我們仔細看看libfoo.so的Dynamic section的內容:

Dynamic section at offset 0x5cc contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x460
0x0000000d (FINI) 0x5ac
0x00000019 (INIT_ARRAY) 0x85c0
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x85c4
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
……

我們先不著急看具體的各個項次的含義,我們先看看section table中對.dynamic的描述:

[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
……
[16] .dynamic DYNAMIC 000085cc 0005cc 0000e0 08 WA 3 0 4

由此可知,.dynamic section是有Entry size的,也就是說,這個section中的內容是按照8個byte形成一個個的entry,下面的這個Elf32_Dyn(對於64bit的CPU,對應是Elf64_Dyn)數據結構可以解析這8個bytes:

typedef struct {
Elf32_Sword d_tag; /* Dynamic entry type */
union {
Elf32_Word d_val; /* Integer value */
Elf32_Addr d_ptr; /* Address value */
} d_un;
} Elf32_Dyn;

d_tag定義dynamic entry的類型,而根據tag的不同,附加數據d_un可能是一個整數類型d_val,其含義和具體的tag相關,或者附加數據是一個虛擬地址d_ptr。了解了這些信息後,我們可以來解析.dynamic section的具體內容了。

dynamic tag是NEEDED這個entry標識libfoo這個動態庫依賴的object文件。ldd工具可以打印出給定程序或者動態庫的share library的依賴關系,本質上ldd就是應用了NEEDED這個tag信息。對於libfoo.so這個動態庫,它會依賴libc.so.6這個動態庫,也就是c庫了。不過,你可能會奇怪,我們c代碼沒有引用任何的c庫函數啊,怎麽會依賴c庫呢?其實這和靜態鏈接的hello world程序類似,我們在講靜態鏈接的時候已經描述了,你可以在build libfoo.so的時候加上-v的選項,這時候你可以從不斷滾動的屏幕信息中找到答案:你的c代碼不是一個人在戰鬥。你可以可以從.text中看到一些端倪,例如.text中有一個call_gmon_start的函數,這個函數本來就不是我們的c代碼定義的符號,我們的c代碼只定義了foo函數以及xxx、yyy這兩個變量符號。本來以為在.text中只有foo的定義,call_gmon_start是從那裏冒出來的呢?實際上這個符號定義在crti.o中(在最後生成libfoo.so的動態庫的時候,有若幹個crt*.o參與其中)。libfoo.so定義了call_gmon_start這個函數,那麽什麽時候調用呢?這又回到了linux下動態庫的結構這個問題上:雖然動態庫定義了一些符號(函數或者全局變量),但是,我們希望在調用這些函數或者訪問這些變量之前,先執行一些初始化的代碼(這發生在動態庫加載的時候,dlopen的時候,由dynamic linker負責)。這些初始化代碼被放到一些特殊的section(例如.init),libfoo.so的.init section的反匯編結果如下:

00000460 <_init>:
460: e52de004 str lr, [sp, #-4]!
464: e24dd004 sub sp, sp, #4 ; 0x4
468: eb000009 bl 494 -----以上來自crti.o

這裏可以存放動態庫自己定義的初始化函數,當然我們這麽簡單的動態庫當然沒有。
46c: e28dd004 add sp, sp, #4 ; 0x4------以下來自crtn.o
470: e8bd8000 ldmia sp!, {pc}

INIT(對應.init section)到FINI_ARRAYSZ這些entry都是和該動態庫的初始化和退出函數相關的。當dynamic linker open這個動態庫的時候(dlopen)會執行初始化函數,當dynamic linker close這個動態庫的時候(dlclose)會執行退出函數。還有很多dynamic tag,這裏主要關註結構,暫且略過,一言以蔽之,dynamic linker可以通過.dynamic section找到所有它需要的動態鏈接信息。

6、動態庫中訪問全局變量

我們來看看foo中如何訪問yyy這個符號的。yyy的重定位信息如下(.rel.dyn section中):

000086bc 00000815 R_ARM_GLOB_DAT 000086dc yyy

符號表中可以查到GOT的位置:

56: 000086ac 0 OBJECT LOCAL HIDDEN ABS _GLOBAL_OFFSET_TABLE_

當然0x86ac是一個offset,並不是run time address,畢竟只有loading後才知道其具體的地址信息。如果該動態庫被loading到address_libfoo,那麽GOT實際應該位於address_libfoo+0x86ac。而yyy符號的地址在address_libfoo+0x86bc,dynamic linker會在適當的時間把真實的yyy符號的地址寫入到這個位置的。由此可見,在offset是0x000086bc(GOT中的某個entry)的位置上保存了yyy符號的重定位信息。

……
568: e59f202c ldr r2, [pc, #44] ; 59c <.text+0x108> ---獲取GOT到當前指令的偏移
56c: e08f2002 add r2, pc, r2 --------------獲取GOT的絕對地址
570: e59f3028 ldr r3, [pc, #40] ; 5a0 <.text+0x10c> ---獲取yyy在GOT中的偏移
574: e7921003 ldr r1, [r2, r3] --------------從GOT entry找到yyy的絕對地址
578: e59f3024 ldr r3, [pc, #36] ; 5a4 <.text+0x110> ---r3被賦值0x5678
57c: e5813000 str r3, [r1] ---------------給yyy賦值
……
59c: 00008138 .word 0x00008138 -----------指令到GOT的偏移
5a0: 00000010 .word 0x00000010 -----------yyy符號在GOT中的offset
5a4: 00005678 .word 0x00005678
5a8: 00000018 .word 0x00000018

雖然不知道GOT的絕對地址,但是在靜態鏈接的時候,代碼段的代碼和GOT的偏移是已經確定的(loading的時候是按照program header中的信息進行loading,code segment和data segment是連續的),因此,在指令中可以通過59c這個橋梁獲取GOT的首地址,加上entry偏移就可以獲取指定符號的GOT入口地址,從該GOT入口地址中可以取出runtime的符號的絕對地址。

計算機科學基礎知識(四)動態庫和位置無關代碼