プロセスの生成
新しくプロセスを生成する目的は2つに分けられる。
同じプログラムの処理を複数のプロセスに分けて処理する
(例:Webサーバによる複数リクエストの受付)
別のプログラムを生成する
(例:bashから各種プログラムを新規生成)
Linuxはfork()関数とexecve()関数を使ってこれらを実現する。
内部的にはそれぞれclone()、execve()というシステムコールを呼び出す。
1. の場合はfork()関数のみを使い、2. の場合はfork()関数とexecve()関数の両方を使う。
同じプロセスを2つに分裂させるfork()関数
fork()関数を発行すると、発行したプロセスのコピーを作った上で、どちらもfork()関数から復帰させる。
生成元のプロセスを「親プロセス」、生成されたプロセスを「子プロセス」と呼ぶ。
この時の流れは次の通り。
親プロセスがfork()関数を呼ぶ
子プロセス用メモリ領域を確保し、親プロセスのメモリをコピーする
親プロセスと子プロセスはfork()関数から復帰する
親プロセスと子プロセスはfork()関数の戻り値が異なるため処理を分岐させることができる。
実際には、親プロセスから子プロセスへのメモリコピーは、コピーオンライト(Copy-on-Write)という機能によって非常に低コストで済む。
そのため、Linuxにおいて同じプログラムの処理を複数プロセスに分けて処理する際のオーバーヘッドは小さい。
別のプログラムを起動するexecve()関数
fork()関数でプロセスのコピーを作成した後、子プロセス上でexecve()関数を発行する。
これによって子プロセスは別のプログラムに置き換えられる。
この時の処理の流れは次の通り。
execve()関数を呼び出す
execve()関数の引数で指定した実行ファイルからプログラムを読み出してメモリ上に配置する(メモリマップ)ために必要な情報を読み出す
現在のプロセスのメモリを新しいプロセスのデータで上書きする
新プロセスの最初に実行すべき命令から実行開始する
execve()関数を実現するために、実行ファイルはプログラムのコードやデータに加えて、次のようなプログラムの起動に必要なデータを保持している。
コード領域のファイル上オフセット、サイズ、およびメモリマップ開始アドレス
データ領域についての上記と同じ情報
最初に実行する命令のメモリアドレス(エントリポイント)
Linuxの実行ファイルは通常「Executable and Linking Format」(ELF)というフォーマットになっており、
ELFの各種情報は`readelf`というコマンドで調べられる。
プログラムの開始アドレスは`readelf -h`によって得られる。
`Entry point address`という行の`0x400490`という値がこのプログラムのエントリポイント。
コードとデータのファイル内オフセット、サイズ、開始アドレスは`readelf -S`コマンドで得られる。
上の添付は出力の一部省略している。
実行ファイルは複数の領域に分けられており、それぞれをセクションと呼ぶ
セクションの情報は2行を1組として表示される
数値は全て16進数
セクションの主な情報
セクション名:1行目の第2フィールド(Name)
メモリマップ開始アドレス:1行目の第4フィールド(Address)
ファイル内オフセット:1行目の第5フィールド(Offset)
サイズ:2行目の第1フィールド(Size)
セクション名が.textであるものがコードセクション
セクション名が.dataであるものがデータセクション
プログラムから作成したプロセスのメモリマップは`/proc/<pid>/maps`というファイルによって得られる。
ASLRによるセキュリティ強化
Linuxカーネルは「Address Space Layout Randomization」(ASLR)というセキュリティ機能を持っている。
ASLRは、プログラムを実行するたびに各セクションを異なるアドレスにマップする機能で、これにより攻撃対象のコードやデータが特定のアドレスに存在することを前提とした攻撃が困難になる。
ASLRを利用できる条件
カーネルのASLR機能が有効になっている
(Ubuntu20.04はデフォで有効。無効化するには`sysctl`の`kernel.randomize_va_space`を0に設定する)
プログラムがASLRに対応している
(このようなプログラムを「Position Independent Executable」(PIE)と呼ぶ)
Ubuntu20.04のgccはデフォでプログラムをPIEとしてビルドするが、`-no-pie`オプションでPIEを無効化できる。
プログラムがPIEかどうかは`file`コマンドで調べられる。
fork()関数とexecve()関数以外のプロセス生成方法
UNIX系OSのC言語インターフェース規格の「POSIX」に定義されているposix_spawn()関数を使えば処理を簡略化できる。