Position-independent code
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. Pokud strojový kód obsahuje absolutní adresy v adresách skoků i v odkazech na data a 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.
Překladače generují instrukce skoků a volání s relativními adresami — pokud takové instrukce obsahuje instrukční sada zvoleného procesoru a pokud se ta adresa nezískává až za běhu (například protože sídlí v jiné knihovně).
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ěn 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 rozhodnutí o umístění kódu v paměti, 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 pokračování kódu bylo nahráno na adresu 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
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 (v tzv. relokační tabulce), které je nutné přizpůsobit umístění kódu v paměti ještě před tím, než je relokovaný kód možné spustit. 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. 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 spustily 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 globální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
Knihovny DLL v Microsoft Windows nejsou sdílenými knihovnami v unixovém smyslu a nepoužívají pozičně nezávislý kód. To znamená, že obsažené funkce nemohou být překryty funkce z dříve nahraných DLL knihoven a vyžadují použití menších triků, aby byla sdílena vybraná globální data. Kód musí být po nahrání z disku do paměti relokován, což potenciálně způsobuje nesdílitelnost mezi různými procesy.
Pro zmírnění tohoto omezení jsou téměř všechny DLL knihovny předmapovány na různé fixní adresy tak, aby nevznikaly konflikty. Pak není nutné knihovny před použitím relokovat, jejich kód zůstane stejný a paměť s knihovnou může být mezi různými procesy sdílena. V předmapovaných DLL knihovnách ponechány informace umožňující eventuální relokaci na jinou adresu, pokud je to nutné.
Technika sdílení kódu knihoven se v Microsoft Windows nazývá „mapování paměti“ (neplést s mapováním paměťového prostoru I/O zařízení) je někdy schopná dovolit více procesům sdílet DLL knihovnu, která je nahrána do paměti. Avšak ve skutečnosti nejsou Microsoft Windows vždy schopny sdílet DLL knihovnu mezi více procesy.[2] Systém Microsoft Windows vyžaduje, aby každý program věděl, kde budou v jeho adresním prostoru DLL knihovny zpřístupněny – není zde žádná podpora pro pozičně nezávislý kód.
Uvnitř DLL knihovny je specifikována požadovaná bázová adresa, na kterou by knihovna měla být umístěna, přičemž tato adresa je určena při vytváření DLL knihovny (Visual C++ nastavuje výchozí bázovou adresu na 0x10000000). Když však má více najednou použitých DLL knihoven stejné požadované bázové adresy, dojde ke kolizi a některé DLL knihovny je nutné při startu programu a jeho linkování s kolidujícími knihovnami relokovat. Když je v Microsoft Windows spouštěn program, zavaděč nahraje do paměti spustitelný soubor a zkontroluje, jestli jsou všechny DLL knihovny nahrány do paměti na bázové adresy, které byly zaznamenány do programu při jeho překladu. Pokud není některá DLL knihovna nahrána na předpokládanou bázovou adresu, je knihovna automaticky relokována na adresu požadovanou spustitelným souborem. Všimněte si, že tento způsob umožňuje sdílení DLL knihovny mezi více procesy vzniklými ze stejného spustitelného souboru (například pokud jsou přihlášeni dva různí uživatelé pomocí rychlého přepínání uživatelů), avšak neumožňuje nezbytně sdílení DLL knihovny mezi různými programy, které používají stejnou DLL knihovnu.
Jiné platformy, jako například Mac OS X nebo Linux, také v současné době podporují metodu předrelokace knihovny na předem danou bázovou adresu. V Mac OS X se tento systém nazývá prebinding. V Linuxu je systém implementován programem prelink. V obou případech se však jedná o velmi odlišný princip, než „mapování paměti“ použité v Microsoft Windows.
Pozičně nezávislý spustitelný soubor
Pozičně nezávislý spustitelný soubor (PIE, Position-independent executable) je soubor, který je zcela tvořen pozičně nezávislým kódem. Pozičně nezávislé spustitelné soubory jsou používány pro zvýšení bezpečnosti. Spustitelný soubor je při spuštění nahrán do paměti od náhodně zvolené adresy, což stěžuje nebo dokonce znemožňuje použití některých exploitů v případě, že se spoléhají na předem zjištěnou pozici spustitelného kódu v paměti (např. return-to-libc attack).[3][4] Účinnost bezpečnostní ochrany počítače pomocí pozičně nezávislých spustitelných souborů je na 32bitových systémech omezená. Na 64bitových systémech je však účinnější díky obrovskému rozsahu použitelných náhodně volených umístění v paměti.
Windows NT používají náhodné umisťování od Windows Vista. Většina současných distribucí Linuxu PIE podporuje pomocí PaX nebo Exec Shield (například Fedora, RHEL a jeho klony).[5][6][7]
Literatura
- [s.l.]: [s.n.] ISBN 1-55860-496-0.
Reference
- [cit. 2009-12-03]. Dostupné online.
- Archivovaná kopie [online]. [cit. 2007-04-26]. Dostupné v archivu pořízeném dne 2007-04-19.
- http://seclists.org/bugtraq/1997/Aug/63 – Getting around non-executable stack (and fix)
- http://x82.inetcop.org/h0me/papers/FC_exploit/FC_exploit.txt Archivováno 25. 1. 2010 na Wayback Machine – Advanced exploitation in exec-shield (Fedora Core case study)
- Fedora Core 1 Release Notes [online]. 2003-11 [cit. 2007-10-18]. (Red Hat, Inc.). Dostupné v archivu pořízeném z originálu dne 2003-12-02.
- VAN DE VEN, Arjan. New Security Enhancements in Red Hat Enterprise Linux v.3, update 3 [PDF]. 2004-08 [cit. 2007-10-18]. (Red Hat, Inc.). Dostupné v archivu pořízeném z originálu dne 2005-05-12.
- https://archive.is/20120529183334/kerneltrap.org/node/644 – Linux: Exec Shield Overflow Protection
Externí odkazy
- http://www.gentoo.org/proj/en/hardened/pic-guide.xml – Introduction to Position Independent Code (anglicky)
- http://www.gentoo.org/proj/en/hardened/pic-internals.xml – Position Independent Code internals (anglicky)
- https://web.archive.org/web/20150526050031/http://linux4u.jinr.ru/usoft/WWW/www_debian.org/Documentation/elf/node21.html – Programming in Assembly Language with PIC
V tomto článku byl použit překlad textu z článku Position-independent code na anglické Wikipedii.