位置独立コード
位置独立コード(いちどくりつコード、英: position-independent code、PIC)または位置独立実行形式(いちどくりつじっこうけいしき、英: position-independent executable、PIE)とは、主記憶装置内のどこに置かれても絶対アドレスに関わらず正しく実行できる機械語の列である。PICは主に共有ライブラリに使われ、各プログラムが(例えば他の共有ライブラリに)使われていない任意の別々のアドレスに同じ共有ライブラリをロードして使うことができる。PICはMMUによる仮想記憶の仕組みが無い古いコンピュータシステムでも使われていた[1]。PICを使えば、MMUのないシステムであってもオペレーティングシステム (OS) が単一のアドレス空間内で複数のアプリケーションを共存させることができる。
位置独立コードはメモリ上の任意の位置にコピーでき、修正することなく実行できる。リロケータブルコードは、指定されたアドレスで実行可能にするためにリンケージエディタやローダが特別な処理を施すが、位置独立コードではそれが不要である。位置独立コードはソースコードにおける特別な意味論が必要で、コンパイラがそれをサポートしていなければならない。絶対アドレスを指定する分岐命令など、特定のメモリアドレスを参照する命令は、等価なプログラムカウンタ相対命令に置き換えなければならない。そのために命令数が増えることもあるので効率は低下するが、最近のプロセッサはその差が無視できる程度になるよう設計されている[2]。
歴史
[編集]初期のコンピュータではコードは位置依存だった。プログラムは特定アドレスにロードされ、実行されるよう構築されていた。複数のジョブを同時に並行して実行させるには、それらジョブが必要とするアドレスが重ならないようオペレータが注意深くスケジューリングする必要があった。例えば、給与支払いプログラムと受取勘定プログラムが共に同じアドレスで動作するよう構築されていた場合、オペレータはそれらを同時に実行させることができない。時には1つのプログラムのロードアドレスを変化させた複数のバージョンを作っておき、スケジューリングしやすくしていた。
より柔軟にするため、位置独立コードが発明された。位置独立コードをロードするアドレスはオペレータが任意に選択可能である。
動的アドレス変換(MMUが提供する機能)によって各プロセスに別々のアドレス空間が割り当てられるようになり、位置独立コードはほとんど不要となった。位置独立コードは位置依存コードより効率が悪いため、動的アドレス変換の方がよりよい解決策だった。
次に解決すべき課題は、複数の似たようなジョブを同時に実行する際、同じコードを複数回 重複してロードしなければならず、メモリを浪費することになる点である。全く同じプログラムを動作させる2つのジョブがあるとき、動的アドレス変換では物理メモリ上にロードしたプログラムを2つのジョブの仮想アドレス空間にマッピングすることで、プログラムの唯一の実体をメモリ上へ配置する事ができる。
しかし、プログラムは別々でも多くのコードが共通しているということの方が多い。例えば給与支払いプログラムと受取勘定プログラムは、全く同一のサブルーチンを数多く含んでいると考えられる。そこでモジュールの共有という考え方が生まれた(共有ライブラリは共有モジュールの一種である)。給与支払いプログラムと受取勘定プログラムの主プログラムはそれぞれ別のメモリにロードされるが、共有モジュールは物理メモリ上には1回だけロードされ、それを2つの仮想アドレス空間にマッピングする。
位置独立コードはアプリケーションで使われるだけでなく、OS内でも使われている。初期のページング方式は仮想アドレス空間を採用していなかった。その代わりOSが必要に応じて個々のモジュールをロードし、必要性の低いモジュールを上書きしていた(OSが使えるメモリ容量は、OS自体のコード容量より小さかった)。モジュールは必要なときに空いているメモリ上で動作可能でなければならないため、個々のOSモジュールは位置独立コードとして書かれていた。
仮想記憶の発明で、OS自体も広大な仮想アドレス空間を持てるようになり、各OSモジュールに別々の恒久的仮想アドレスを割り当てられるようになったので、この技法も廃れた。
技術的詳細
[編集]共有ライブラリ内部でのプロシージャ呼び出しは一般に小さなプロシージャ・リンク・テーブルのスタブを通して行い、そこから実際の関数を呼び出す。これにより、共有ライブラリが以前にロードされていた別のライブラリ群からの関数呼び出しを引き継ぐことを可能にしている。
位置独立コードからデータを参照する場合はさらに間接的な方式となり、そのコードがアクセスする全グローバル変数のアドレスを格納したGOT (Global Offset Table) を使用する。コンパイル単位またはオブジェクトモジュールごとにGOTがあり、コードから見て固定の相対位置に置かれる(ただし、ライブラリをリンクするまで実際のオフセットは不明である)。リンカがモジュール群をリンクして共有ライブラリを作る場合、各モジュールのGOTをマージし、最終的なコードとのオフセット値を設定する。共有ライブラリをロードした後は、オフセット群を調整する必要はない。
位置独立関数がグローバルなデータにアクセスする際、その時点のプログラムカウンタの値からGOTの絶対アドレスを決定する。そのため、偽の関数呼び出しを行ってリターンアドレスをスタック上に得る技法(x86の場合)や、(特定の汎用レジスタに特別な意味を与える規約を設けるなど)特別なレジスタを使う場合(PowerPC、SPARC、MIPSなどのRISCプロセッサやESA/390など)がある。MC68000、MC6809、WDC 65C816、クヌースのMMIX、ARM、x86-64 といったプロセッサアーキテクチャでは、プログラムカウンタ相対でデータを参照できる。その場合は位置独立コードを小さくでき、レジスタもあまり使わずに済むので、より効率的となる。
Windows のDLL
[編集]32ビット版の Microsoft Windows のDLLは位置独立コードを使っていないので、Unix系の考え方では共有ライブラリではない。したがって、以前にロードしていたDLLを新たなDLLで上書きすることができず、グローバルなデータを共有するにはトリックを必要とする。コードは補助記憶装置から主記憶装置へ読み込んだ後でリロケート(再配置)する必要があり、プロセス間で共有できない可能性が生じる。DLLの共有は基本的にディスク上でのことである。
その制約を緩和するため、WindowsのシステムDLLのほとんどが事前に異なる固定アドレスにマッピングされ、重ならないようになっている。したがって、使用前に再配置する必要はなく、主記憶上でも共有可能である。ただし事前マッピングされたDLLであっても、必要に応じて任意のアドレスにロードできるよう再配置のための情報を含んでいる。
Windowsではこの共有技法を "memory mapping" と呼び、主記憶上にロードされたDLLのインスタンスを複数プロセス間で共有できることもある。しかし実際には、主記憶上でDLLを共有できないこともある[3]。Windowsでは個々のコンパイルされたプログラムは、自身のアドレス空間内で個々のDLLがどの位置にマッピングされるかを知っていなければならず、位置独立性はサポートされていない。
DLLは作成時に「希望の」ベースアドレスを指定する。これをRVAと呼ぶ。しかし、複数のDLLが同じ希望のベースアドレスを持っている場合、同じベースアドレスに配置することはできないので、配置位置が被った2つ目以降のDLL全てをリンク時に再配置しなければならない。Windowsのローダが実行ファイルを主記憶にロードする際、その実行ファイルが作られたときに決めたアドレス(RVAのことではない)に各DLLがロードされているかをチェックする。DLLがロードされていない場合、その実行ファイルが要求するベースアドレスに再配置する。これにより実行ファイルは複数プロセス間で共有できるが(Fast User Switching で複数アカウントから起動した場合など)、同じDLLを異なるプログラム間で共有しても再配置されるため、DLL自身は共有されないことがある[4]。
macOSやLinuxといったプラットフォームでは、事前バインディングの形式もサポートしている。macOSではこれを prebinding と呼ぶ。Linuxでは同じ機能を提供するプログラム prelink がある。ただし実装方式は Windows の memory mapping とは大きく異なる。
64ビットのx86(x64)は命令ポインタ(プログラムカウンタ)相対アドレッシングをサポートしているため、64ビット版WindowsではDLLに位置独立コードを使うようになり、再配置方式をやめた[5]。
位置独立実行形式
[編集]位置独立実行形式 (PIE) は、位置独立コードのみで構築された実行ファイルである。一部システムはPICの実行形式のみをサポートしているが、他にもPIEが使われる理由がある。例えば、セキュリティ指向のLinuxディストリビューションでPIEを使っており、PaXや Exec Shield がアドレス空間配置のランダム化を行い、Return-to-libc攻撃のように実行ファイルの配置アドレスを利用して行う攻撃を防いでいる。
脚注
[編集]- ^ John R. Levine (October 1999). “Chapter 8: Loading and overlays”. Linkers and Loaders. San Francisco: Morgan-Kauffman. pp. 170–171. ISBN 1-55860-496-0
- ^ Alexander Gabert (January 2004). “Position Independent Code internals”. Hardened Gentoo. 2009年12月3日閲覧。 “direct non-PIC-aware addressing is always cheaper (read: faster) than PIC addressing.”
- ^ Rick Anderson (January 2000). “The End of DLL Hell”. Microsoft Developer Network. 2007年4月19日時点のオリジナルよりアーカイブ。2007年4月26日閲覧。 “Sharing common DLLs does provide a significant savings on memory load. But Windows is not always able to share one instance of a DLL that is loaded by multiple processes.”
- ^ Matt Pietrek (February 2002). “An In-Depth Look into the Win32 Portable Executable File Format”. MSDN Magazine. 2012年1月28日閲覧。 “PE files can load just about anywhere in the process address space. While they do have a preferred load address, you can't rely on the executable file actually loading there. To avoid having hardcoded memory addresses in PE files, RVAs are used. An RVA is simply an offset in memory, relative to where the PE file was loaded.”
- ^ Matt Pietrek (December 2000). “MSDN Magazine - Under the Hood - Programming for 64-bit Windows”. 2011年2月9日閲覧。 “Position-independent code eliminates the need for the base relocations that are used in the x86 version of Windows.”
参考文献
[編集]- John R. Levine (October 1999). “Chapter 8: Loading and overlays”. Linkers and Loaders. Morgan-Kauffman. ISBN 1-55860-496-0