Přeskočit na obsah

Position-independent code

Z Wikipedie, otevřené encyklopedie

Position independent code (PIC – pozičně nezávislý kód, též Position Independent Executable, PIE – pozičně nezávislý spustitelný soubor) je v informatice strojový kód, který je možné vykonat nezávisle na tom, na jaké adrese je v operační paměti umístěn. Běžný strojový kód obsahuje absolutní adresy v adresách skoků i v odkazech na data. Pokud je takový kód umístěn na jiné adresy, než byl původně určen, míří cíle skoků nebo odkazy na jeho vlastní data na nesprávné adresy. Pozičně nezávislý kód používá místo absolutních adres relativní odkazy (např. skok o 10 adres dále, data jsou na adrese o 100 méně, než je aktuální adresa), takže je funkční i při umístění na jinou adresu.

Absolutní a relativní adresy

Zdrojový kód programu, který je zapsán v nějakém programovacím jazyku, je zpracován překladačem. Překladač vytvoří posloupnost strojových instrukcí, které jsou uloženy do spustitelného souboru (např. [[EXE], ELF). Ve spustitelném souboru je umístě strojový kód, který je vytvořen tak, aby byl funkční při umístění od jisté počáteční adresy v operační paměti počítače. Za konečné umístění v paměti a vyčíslení skoků a odkazů na data v paměti zodpovídá linker (je typicky chápán jako součást překladače).

Klasické procesory (tj. i ty nejstarší) obsahují strojové instrukce, které se odkazují na místa v paměti pomocí absolutních adres (tj. konkrétní číslo paměťové buňky, které jsou číslovány od nuly dále, přičemž nula odpovídá první paměťové buňce). Jsou-li ve výsledném strojovém kódu data umístěna na jeho konci, pak jejich absolutní adresa závisí na tom, kolik strojových instrukcí je před nimi. V případě, že strojový kód bude umístěn od adresy nula a bude zabírat 100 adres, budou data začínat na adrese 101. Strojová instrukce, která bude načítat data, bude přistupovat k adresám od 101 dále. Nahrajeme-li tento strojový kód do paměti místo od adresy nula například od adresy 1000, budou data umístěna od adresy 1101, ale strojová instrukce bude stále používat absolutní adresu 101. Proto nebude program fungovat správně.

Stejně tak nebudou při posunu strojového kódu fungovat skoky, které používají absolutní adresy. Při použití výše zmíněného příkladu může strojový kód obsahovat na první pozici skok na adresu 10. Bude-li do paměti nahrán od adresy nula, bude vše fungovat správně, protože na adrese 10 budou umístěny následující strojové instrukce výpočtu. Nahrajeme-li ovšem tento strojový kód od adresy 1000, bude proveden skok na absolutní adresu 10, na které se v tuto chvíli nebude nacházet žádný kód, protože předpokládané pokračování je na adrese 1010.

Ve výše zmíněných příkladech byly použity absolutní adresy. Některé procesory však podporují relativní adresy, které jsou buď vztaženy k právě zpracovávanému místu v paměti nebo k tzv. bázovému registru. V případě vztahu k právě zpracovávané strojové instrukci se používá registr čítač instrukcí, který používá sám procesor. Relativní adresa je pak odchylka od čítače instrukcí (například +2 nebo -100). Procesor tedy při přístupu k datům musí nejprve vypočítat absolutní adresu, která se skládá z čítače instrukcí sečteného s odchylkou (tzv. offset) a pak teprve může na sběrnici sběrnici nastavit adresu buňky v paměti.

V případě použití bázového registru je v procesoru speciální registr, který například slouží jako ukazatel počátku datové oblasti. Při přístupu k datům uvádíme ve strojové instrukci označení bázového registru a odchylku, která vyjadřuje vzdálenost umístění příslušných dat od tohoto bázového registru. Stejně jako v předchozím případě musí procesor nejprve sečíst bázový registr a odchylku (offset) a pak teprve může na sběrnici nastavit výslednou absolutní adresu, na které jsou cílová data umístěna. Bázový registr musí být naplněn vhodnou hodnotou před započetím používání relativních adres, což je možné zajistit například odvozením jeho obsahu od umístění začátku kódu, který má být pozičně nezávislý (například odvozením od čítače instrukcí).

Používání relativních adres může způsobit, že je kód méně efektivní, protože je nutné vždy nejprve vypočítat výslednou absolutní adresu. V moderních procesorech je však tento rozdíl téměř zanedbatelný.[1]

Pozičně nezávislý kód

Pozičně nezávislý kód je možné použít pouze v případě, že procesor poskytuje možnost použití relativních adres (viz výše), takže musí obsahovat strojové instrukce, které mají relativní adresy jako argumenty. Bez hardwarové podpory v procesoru tedy není možné jednoduše vytvořit pozičně nezávislý kód.

V případě, že procesor relativní instrukce podporuje, může být překladač nastaven tak, aby je výhradně používal a vyhnul se použití strojových instrukcí s absolutními adresami. Tak je možné pozičně nezávislý kód vytvořit.

Využití pozičně nezávislého kódu

Pozičně nezávislý kód se běžně používá pro sdílené knihovny, protože může být namapován do libovolné části adresního prostoru procesu tak, aby nekolidoval s jiným kódem (například s jinými sdílenými knihovnami).

Pozičně nezávislý kód byl používán u starších procesorů, které neobsahovaly MMU (jednotku správy paměti), avšak podporovaly relativní adresování, aby nebylo nutné kód relokovat.

Relokace

Hlavní článek: Relokace

V případě, že procesor nepodporuje relativní adresy, může překladač s linkerem vytvořit kód, který bude tzv. relokovatelný. V takovém případě je na začátku kódu uveden seznam absolutních adres, které je nutné přizpůsobit umístění kódu v paměti. Přizpůsobení provádí zavaděč (loader) po nakopírování strojového kódu do paměti. Kód je vytvořen například tak, aby byl funkční při umístění od adresy nula. Je-li nahrán do paměti od jiné adresy (např. 100), je tato adresa chápána jako odchylka (offset), kterou je nutno přičíst ke všem použitým absolutním adresám (jejichž seznam je v tzv. relokační tabulce). Relokováním je však kód změněn, takže nemůže být sdílen, což je problém zejména u sdílených knihoven.

Historie

V dřívějších počítačích, byl kód závislý na pozici: každý program byl postaven tak, aby byl nahrán a spuštěn z určité adresy. Aby mohlo být spuštěno více procesů najednou, operátor musel důsledně naplánovat procesy tak, aby dva souběžné procesy nespustili programy, které by vyžadovaly stejnou nahrávací adresu. Například pokud byly dva programy postaveny tak, aby běžely na adrese 32 KiB, operátor nemohl spustit oba zároveň. Někdy si operátor ponechával víc verzí programu, každou pro jinou nahrávací adresu, aby měl více možností.

Aby se těmto komplikacím předešlo, byl vymyšlen PIC. Ten mohl být spuštěn z jakékoliv adresy, ze které si operátor vybral ho nahrát.

Vynalezení překladu dynamických adres (funkce prováděná MMU) způsobilo, že se PIC stal poněkud zastaralým, protože každá úloha mohla mít svoji vlastní adresu 32 KiB a programátor mohl psát všechny programy, tak aby běžely na adrese 32 KiB a ony mohly běžet všechny najednou (každý ve svém vlastním adresním prostoru).

Další problém, který bylo potřeba vyřešit bylo plýtvání pamětí, které nastává, když je stejný kód nahráván několikrát, aby byl použit ve více současných procesech. Když dva procesy spustili dva stejné programy, překlad dynamických adres poskytoval řešení, tím že nechal systém namapovat dva různé procesy s adresami 32 KiB do stejných bajtů fyzické paměti, obsahující jen jednu kopii programu.

Mnohem častěji jsou ale programy odlišné a zřídka kdy sdílí hodně stejného kódu. Obvykle ale obsahují dva podobné programy stejné funkce. Proto programátoři vymysleli sdílené moduly (sdílená knihovna je formou sdíleného modulu). Zatímco programy jsou nahrány do oddělené paměti, sdílený modul se nahraje jen jednou a je jednoduše namapován do dvou adresních prostorů.

To ale přináší problém s alokováním paměti, podobný tomu, který PIC vyřešil předtím: Pokud program může mít jeden sdílený modul, může jich mít i více. Co když jeden program v jednom adresním prostoru chce použít dva sdílené moduly, oba postavené tak, aby běžely na stejné adrese? Systém nemůže nahrát oba současně, takže není možné program nahrát. Aby se tomu programátoři vyhnuli, snažili se vždy, aby nepostavili dva sdílené moduly tak, aby byly spouštěny na stejné adrese, pokud oba mohou být použity stejným programem. Občas vytvořili několik verzí sdílených modulů, každý spustitelný na jiné adrese.

Toto samozřejmě opět není přívětivé řešení. Vyžaduje hodně manuální práce a plýtvá adresním prostorem. PIC řeší tento problém, protože pokud sdílený modul může být spuštěn z jakékoliv adresy, pak ho loader jednoduše může spustit na jakékoliv volné adrese. Funkce může běžet na adrese 32 KiB v jednom procesu, ale i na adrese 48K v souběžném procesu. Obě adresy odpovídají stejné fyzické paměti, v paměti je pouze jedna kopie funkce.

PIC se používá nejen ke koordinaci práce uživatelských aplikací, ale také v rámci operačního systému. Dřívější stránkovací systémy nevyužívaly adresní prostory virtuální paměti, místo toho operační systém explicitně nahrál jednotlivé své moduly, které byly potřeba a přepsal ty méně potřebné (paměť dostupná pro operační systém byla mnohem menší než operační systém). Modul musel být schopný běžet v jakékoliv části paměti, která byla volná, když jej bylo třeba, takže jednotlivé moduly operačního systému byly tvořeny PICem.

Vynález virtuální paměti odsunul tuto metodu do pozadí, protože operační systém mohl mít virtuální adresní prostor tak velký, že každý modul operačního systému mohl mít svoji stálou virtuální adresu.

Technické detaily

Volání procedur uvnitř sdílené knihovny je typicky aplikováno pomocí volání malé procedurální spojovací tabulky, která potom volá danou funkci. To dovoluje sdílené knihovně zdědit určité volání funkcí od dříve nahraných knihoven spíše, než používat svoje vlastní verze.

Ukazatele na data v PIC jsou většinou nepřímé, přes globální offsetovou tabulku, která obsahuje adresy všech použitých globálních proměnných. Každá kompilační jednotka, nebo modul má svou offsetovou tabulku a ta je umístěna v daném offsetu od kódu (ačkoliv tento offset není znám dokud není knihovna nahrána Linkerem). Když Linker spojí moduly, aby vznikla sdílená knihovna, sloučí také offsetové tabulky a nastaví výsledný offset v kódu. Offsety už není nutné přizpůsobovat, když nahráváme sdílenou knihovnu později.

Funkce nezávislá na pozici, která přistupuje ke globalním datům, začíná určením absolutní adresy v offsetové tabulce a dostane svoji současnou hodnotu. Ta má často podobu falešného volání funkce, aby dostala návratovou hodnotu v zásobníku (x86) nebo ve speciálním registru (PowerPC, SPARC, ESA/390), který může být potom uchován v předdefinovaném standardním registru. Některé architektury procesorů, jako Motorola 68000, Motorola 6809, ARM a nový AMD64 dovolují přistupovat k datům pomocí offsetu z tabulky instrukcí. Cílem je, aby byl PIC menší, vyžadoval méně registrů, a tím i více efektivní.

DLL ve Windows

DLL v Microsoft Windows nejsou sdílené knihovny podobné Unixovým a nepoužívají PIC. To znamená, že nemohou dědit funkce od dříve nahraných DLL a vyžadují použití menších triků, aby sdílely vybraná globální data. Kód musí být relokován, poté co je nahrán z disku, což způsobuje potenciální nesdílitelnost procesů.

Pro zmírnění tohoto omezení jsou téměř všechny DLL předmapovány na různých fixních adresách tak, aby nevznikaly žádné konflikty. Není nezbytné relokovat knihovny před jejich použitím, aby mohla být paměť sdílena. Dokonce i předmapovaná DLL pořád obsahují informace, které jim dovolují, aby byly nahrány na libovolných adresách, pokud je to potřeba.

Technika sdílení nazývaná ve Windowsu "mapování paměti" je někdy schopná dovolit více procesům sdílet instance DLL nahrané do paměti. Nicméně, ve skutečnosti Windows není vždy schopný sdílet jednu instanci DLL nahranou více procesy.[2] Windows vyžaduje, aby každý kompilovaný program věděl, kde bude v jeho adresním prostoru každé DLL zpřístupněno — Není tu žádná podpora pro nezávislost pozice v kódu.

DLL specifikuje svoji požadovanou bázovou adresu, když je DLL vytvořeno (Visual C++ nastavuje výchozí offset na 0x10000000), ale když má více DLL stejné požadované bázové adresy, program je nemůže všechny relokovat na tento offset a musí specifikovat nové offsety při linkování. Když loader ve Windows nahraje spustitelný soubor do paměti pro spuštění, zkontroluje, jestli všehchny DLL byly nahrány s offsetem použitým když byl spustitelný soubor vytvořen. Pokud není DLL s tímto offsetem nahráno, je relokováno do báze požadované spustitelným souborem. Všimněte si, že tohle umožní sdílení mezi více procesy stejného spustitelného souboru, ale ne nezbytně mezi různými programy, které jsou spojeny se stejným DLL.

Jiné platformy jako Mac OS X a Linux nyní také podporují formu předspojování. V Mac OS X se tento systém nazývá prebinding. V Linuxu je systém implementován přes program nazvaný prelink, což je velmi odlišné od mapování paměti.

Position-independent executables

Position-independent executables (pozičně nezávislý spustitelný soubor (PIE) je spustitelný soubory (binárka), vytvořené zcela z PIC. PIE jsou používány v některých na bezpečnost zaměřených Linuxových distribucích a dovolují PaX, či Exec Shield použít náhodný výběr adresního prostoru, což stěžuje některé útoky, které spoléhají na to, že znají offset spustitelného kódu v souboru, viz return-to-libc attacks.

Literatura

Reference

  1. Alexander Gabert. Position Independent Code internals [online]. 2004 [cit. 2009-12-03]. Dostupné online. 
  2. Rick Anderson. The End of DLL Hell [online]. 2000 [cit. 2007-04-26]. Dostupné online. 

Související články

Externí odkazy

V tomto článku byl použit překlad textu z článku Position-independent code na anglické Wikipedii.