php-logo

PHP による hello world 入門

  • このエントリーをはてなブックマークに追加
  • Evernoteに保存


知る者は言わず、言う者は知らず
— 老子

世界で最も有名なプログラムの 1 つに、 hello world というものがあります。

<?php
echo 'hello, world';

出力先に対して「hello, world」という 12 字を書き込むだけの単純なプログラムで、プログラミング言語やライブラリの利用例を最小限の形で示すものです。

この記事ではメジャーな Web プログラミング言語の 1 つである PHP 処理系が、hello world をどのように実行するのかについて、簡単に解説します。

以下の 4 節で構成されています。

  1. PHP スクリプト実行の大体の流れ」は実行時の概略を箇条書きでまとめたものです。
  2. SAPI」では、PHP 処理系の起動のされ方についての基礎知識を解説します。
  3. Zend Engine とオペコード」は PHP 公式処理系の仮想マシンの命令と、PHP コードがどう仮想マシンの命令へ変換されるかというコンパイル処理の 2 つについて、hello world に必要な範囲で概要を説明します。
  4. PHP ソースコードレベルでの流れ」はこの記事のスクロールバーの半分以上を占める、非常に縦長の節です。mod_php 利用時に PHP 処理系がどう起動されるのかから、PHP ソース内の文字列 “hello, world” がいかにして Apache の出力バッファへたどり着くのか、までの挙動全体を、処理系のソースを読むことで追います。PHP の処理系は C 言語で書かれているため、C 言語についてのある程度の知識を仮定しています。

対象読者は以下のような人です。

  • とても暇な人

記事タイトルが「PHP による hello world 入門」であって、「hello world による PHP 入門」ではないことに注意してください。
この記事は PHP の入門記事ではありません

この記事は KOZOS坂井さん*1一方的にリスペクトしている株式会社リスペクトのプログラマ、@sji_ch が、坂井さんの OSC のスライド『ハロー・ワールド入門』を見て気分が盛り上がり、「そういや PHP のスクリプトがどう実行されるか全然知らねえや! よし調べてまとめよう!」と、ほぼ完全にノリだけで書いたものです*2

内容についてご指摘などあれば、ぜひ twitter やはてブのコメントにてお伝えください。

なお前提となる PHP のバージョンは 5.6 ですが、今年出るらしい PHP 7 についても、必要そうな箇所では少しだけ触れていきます*3

  • *1モノづくりが大好き! のホームページ
  • *2先日、坂井さんの書いたフィーリングでアセンブラを読んじゃう怪著『熱血!アセンブラ入門』の電子書籍版が発売されました!
    残念ながら紙版と同じ島本大先生の表紙は使えなかったようです……。が、「この記事に書いてるような C 言語とか PHP のバイトコードとかじゃまだ生ぬるいきがするけどどうすれば」という人がいれば、とりあえずアセンブラに飛び込んどきゃいいと思います!
  • *3PHP 公式処理系の挙動についてのみ解説し、Facebook の HHVM ついては特に触れません

PHP スクリプト実行の大体の流れ

  1. 実行環境が SAPI のエントリポイントを呼び出す
  2. PHP 処理系に SAPI から入出力用の処理が登録される
  3. PHP 処理系が SAPI から起動される
  4. PHP 処理系がリクエストの処理を開始し、スクリプトファイルを開く
  5. Zend Engine がスクリプトから VM で実行する中間コードを生成する
  6. Zend Engine が生成された中間コードを実行する
  7. 実行中に PHP 処理系からの出力処理が必要なら、SAPI で登録された処理を通して行われる
  8. PHP 処理系がリクエストの処理を終了する

SAPI: PHP の処理系はどのように起動され、外部環境へアクセスするのか

計算機科学のあらゆる問題は別のレベルの間接参照によって解決できる
— David John Wheeler

Q. PHP の処理系はどのように起動されるか
A. SAPI による

mod_php を前提としたよくあるセットアップにおいては、PHP の処理系は Apache の起動とともにメモリへ読み込まれます。
CGI として起動する際については、Apache がリクエストの際に PHP 用のプロセスを起動する場合もあれば、 PHP 用のプロセスがデーモンとして Apache と別に動く場合もあります。
また、コマンドラインから起動する場合は PHP 用に専用のプロセスが起動されます。

PHP の処理系が、対応する複数の動作環境を統一的に扱うためのサーバー抽象化レイヤーを、SAPI と呼びます。

SAPI

SAPI の基本的な発想はこうです。PHP は起動 / 終了時や実行結果の出力時など、幾つかの決まったタイミングで実行環境のシステムと連絡をとる必要があります。

  • それらのタイミングに PHP 側から呼び出す処理を、あらかじめコールバックとして PHP 処理系に登録した後で
  • PHP 処理系を起動する(処理を渡す)

というモジュールを各実行環境ごとに用意することで、各種の Web サーバやコマンドライン実行環境など、ほぼあらゆる環境に PHP 処理系を組み込むことができます。

SAPI 構造体

実際には以下のような C 言語構造体によって、各種のコールバックを登録します。

struct _sapi_module_struct {
	char *name;
	char *pretty_name;

	int (*startup)(struct _sapi_module_struct *sapi_module);
	int (*shutdown)(struct _sapi_module_struct *sapi_module);

	int (*activate)(TSRMLS_D);
	int (*deactivate)(TSRMLS_D);

	int (*ub_write)(const char *str, unsigned int str_length TSRMLS_DC);
	void (*flush)(void *server_context);
	struct stat *(*get_stat)(TSRMLS_D);
	char *(*getenv)(char *name, size_t name_len TSRMLS_DC);

	void (*sapi_error)(int type, const char *error_msg, ...);

	int (*header_handler)(sapi_header_struct *sapi_header, sapi_header_op_enum op, sapi_headers_struct *sapi_headers TSRMLS_DC);
	int (*send_headers)(sapi_headers_struct *sapi_headers TSRMLS_DC);
	void (*send_header)(sapi_header_struct *sapi_header, void *server_context TSRMLS_DC);

	int (*read_post)(char *buffer, uint count_bytes TSRMLS_DC);
	char *(*read_cookies)(TSRMLS_D);

	void (*register_server_variables)(zval *track_vars_array TSRMLS_DC);
	void (*log_message)(char *message TSRMLS_DC);
	double (*get_request_time)(TSRMLS_D);
	void (*terminate_process)(TSRMLS_D);

	char *php_ini_path_override;
	

	void (*block_interruptions)(void);
	void (*unblock_interruptions)(void);

	void (*default_post_reader)(TSRMLS_D);
	void (*treat_data)(int arg, char *str, zval *destArray TSRMLS_DC);
	char *executable_location;

	int php_ini_ignore;
	int php_ini_ignore_cwd; /* don't look for php.ini in the current directory */

	int (*get_fd)(int *fd TSRMLS_DC);

	int (*force_http_10)(TSRMLS_D);

	int (*get_target_uid)(uid_t * TSRMLS_DC);
	int (*get_target_gid)(gid_t * TSRMLS_DC);

	unsigned int (*input_filter)(int arg, char *var, char **val, unsigned int val_len, unsigned int *new_val_len TSRMLS_DC);
	
	void (*ini_defaults)(HashTable *configuration_hash);
	int phpinfo_as_text;

	char *ini_entries;
	const zend_function_entry *additional_functions;
	unsigned int (*input_filter_init)(TSRMLS_D);
};

雰囲気をつかむため、幾つかのコールバックについて起動タイミングや役割を紹介します。

コールバック名 起動タイミング 利用例
startup SAPI が最初に初期化される際 複数のリクエストを扱うアプリケーションにおいては、その最初の1回だけ(たとえば mod_php5 の場合、子プロセスを fork する前の親プロセスで)呼び出されます。
shutdown 処理系の終了処理 保持している SAPI の全データ構造を破棄します。
activate 各リクエストの最初 SAPI のリクエスト毎のデータ構造を初期化します。
deactivate 各リクエストの終わり バッファリング途中などのデータがアプリケーションへ正しく書き出されることを保証し、またリクエスト毎のデータを破棄します。
ub_write PHP からデータを出力する際 CLI SAPI の場合なら単に標準出力へ書き出し、mod_php5 の場合なら Apache のライブラリ関数 ap_rwrite を呼び出します。
flush バッファリングされた出力を書き出す際 CLI SAPI の場合なら fflush を呼び出し、mod_php5 の場合なら Apache のライブラリ関数 ap_rflush を呼び出します。
read_post SAPI の activation の際 リクエストメソッドが POST の場合に $HTTP_RAW_POST_DATA や $_POST に値を入れるのに使われます。

各実行環境にあわせ、SAPI モジュールがこの構造体によってコールバックを PHP 処理系に登録し、そして必要なタイミングで PHP 処理系を起動するというわけです。

もっと詳しく知るには?

残念ながら、知る限り PHP の SAPI に関する公式のまとまったドキュメントは存在しません。全容をつかみたければ、既存の SAPI モジュールの実装を追いかけるのが唯一確実な方法です。

少し古い本ですが、Advanced PHP Programming は SAPI やこれから触れる Zend Engine など、PHP の内部構造について数章を割いて解説しています。
また、PHP を他のアプリケーションへ組み込む基礎として使える embed SAPI というものがあり、Extending and Embedding PHP にはこれの使い方とあわせ、SAPI 自体についても多少の情報があります。

mod_php についてはこの記事の後半で、おおむね hello world 実行に必要な範囲で少しだけ触れています。

SAPI の略歴

SAPI の仕組み自体については先述の通り、「ソース嫁」以上の情報を出すのが難しいのですが、これがいつどのように、誰によって作られたのか、という成立背景を少しでも知っておくことは、実装からその存在について理解する際も多少の助けとなるかもしれません。

SAPI の基本部分は PHP 3 のリリース前となる 1998 年 1 月、サーバに依存する処理を PHP の言語処理系としてのコア部分から分離するための仕組みとして、Shane Caraveo さんによって作られました*1
その後 PHP 3.1 の開発ブランチにおいて開発が進められていましたが、同じ頃に Zeev と Andi によって PHP の実行エンジンを置き換える Zend Engine が作られ、PHP のメジャーバージョンアップを誘発した事で、結局 PHP 3.1 自体はリリースされませんでした*2*3。後にこの 3.1 のブランチから SAPI 部分の成果を取り込んだ形で、SAPI モジュール化された mod_php とともに PHP 4.0 がリリースされることになります。

かつては SAPI を PHP に限らず、Apache や IIS など複数のサーバ環境で動作する拡張機能を作るための独立したプロジェクトとしようとする試みもありましたが*4、結局現在のところ、SAPI は PHP 用のサーバ依存部分抽象化レイヤーとしての位置付けにおさまっています。

Zend Engine とオペコード

PHP は歯ブラシと同じくらいにはエキサイティングなものだ。毎日使い、役割をこなす、単純な道具だ。それで? いったい誰が歯ブラシについての話なんか読みたいと思う?
— Rasmus Lerdorf (PHP の原作者)

Q. Zend Engine ってなに?
A. Zeed Engine は PHP の公式スクリプト実行エンジンです。

Zend Engine は PHP スクリプトをパースし、メモリが許す限りの無限のレジスタ数と CISC 風の命令セットを持つ VMオペコードバイトコード*1列へ変換(コンパイル)するとともに、それをインタプリタによって逐次に実行していきます。

ZendEngine

最初のバージョンである Zend Engine 1 は当時イスラエル技術大学の学生だった Zeev Suraski と Andi Gutmans によって 1999 年に作られ、2000 年 5 月に PHP4 の実行エンジンとしてリリースされました。Zeev と Andi の 2 人の名前が、Zend という名前の由来となっています。

その後 PHP 5 とあわせて、オブジェクト指向機能の扱いを強化した Zend Engine 2 が 2004 年にリリースされました。今年出るとされている PHP 7 では主として性能面で更なる改良が加えられた、かつて PHPNG と呼ばれていた新エンジンが Zend Engine 3 として導入される予定です。

PHP のバージョンアップにあわせて少しずつその姿を変えてきている Zend Engine ですが、「ソースコードをバイトコードへコンパイルする」「バイトコードインタプリタがそれを逐次実行する」という基本的な部分については、その成立当初からさほど大きくは変わっていません。

この節ではこの Zend Engine の基本的な部分、バイトコードとそのコンパイルについてをざっくり解説していきます。

  • *1なお、この記事では「オペコード」と「バイトコード」を特に区別せずに使っています

phpdbg

Zend Engine の挙動の詳細へ踏み込む前に、PHP 5.6 から標準添付されている phpdbg という SAPI モジュールを紹介します。
phpdbg は PHP スクリプトのステップ実行を行ったり、ブレイクポイントを仕掛けたりすることのできるコマンドラインデバッガで、拡張機能ではなく SAPI モジュールの 1 つとして実装されています。SAPI レイヤーで実装されている phpdbg は拡張機能を通す事によるオーバーヘッドがなかったり(要出典)、PHP に標準添付されていることから、スクリプト自体のデバッグについては気軽に行えるという特徴があります*1

  • *1mod_php や fpm といった他の SAPI 環境での動作をデバッガで追いたい場合は、xdebug のような拡張機能として実装されたデバッガを使う必要があります。 phpdbg でもデバッガ内で $_POST や $_GET の値を設定することで、Web アプリのデバッグを行う事は可能ですが、今回の記事には関係のない話題のため、詳細については省略します。

hello world のオペコード

それではこの phpdbg を使って、hello.php が PHP の内部でどのような命令列へ変換されて実行されるかを確認してみましょう。

<?php echo 'hello, world';
phpdbg -e hello.php
phpdbg> print exec

以下のような出力が得られます。

[Context C:\Users\sj-i\work\test\hello.php]
        L0-0 {main}() C:\Users\sj-i\work\test\hello.php
                L1      0x23442c0 ZEND_ECHO                      C0      <unused>             <unused>
                L1      0x23442f0 ZEND_RETURN                    C1      <unused>             <unused>

最初の 2 行は実行コンテキストを指しています。
次の 2 行はそれぞれ命令(オペコード)とそのパラメータ(オペランド)を持つ行で、ZEND_ECHO と ZEND_RETURN という 2 つの命令があるのが分かります。
命令部分の行の各列は左から順に以下のものを表しています。

  • ソースコード上の行番号
  • 命令が存在するメモリアドレス
  • オペコードの命令名
  • オペランド1
  • オペランド2
  • 結果の格納先

オペランドの列にある C0 や C1 は定数を表しています。
phpdbg の以下のコマンドでその内容を確認できます。

phpdbg> info literal

[Literal Constants in C:\Users\sj-i\work\test\hello.php (1)]
 |-------- C0 -------> [hello, world]
 |-------- C1 -------> [1]

echo 文にあたる ZEND_ECHO 命令が ‘hello, world’ という定数パラメータで実行され、次に return 文にあたる ZEND_RETURN が 1 の定数パラメータで実行される、という流れになります。

ZEND_ECHO はオペランドの型に応じた形で PHP の出力バッファへデータの書き込みを行うもので、設定サイズが埋まるなどしてバッファがフラッシュされる際は、SAPI の ub_write を通して実行環境への出力処理が行われます。

最後の ZEND_RETURN は、PHP スクリプトの末尾へ暗黙的に付与されるものです。1 を返しているのは、include の成功時のデフォルト返り値が 1 であるという言語仕様*1と関係しています。PHP ファイルが明示的に return されない場合、この末尾の ZEND_RETURN が実行されることとなります。

Zend Engine の命令セット

Zend Engine 2 の命令セットはおおむね公式マニュアルにある通りのもので、150 以上の命令が実装されています。

Zend Engine 2 オペコード*1

見ての通り、言語機能と命令セットがかなり密接な関係を持っており、一般的な CPU にもあるような ADD や MUL、IS_EQUAL といった演算 / 比較の命令以外に、ECHO や EXIT、INCLUDE や CAST、DECLARE_CLASS といった「複雑な」命令を幾つか持っていることが分かります。

  • *12015年6月17日現在、公式マニュアルの表記だと ZEND_ の接頭辞が付いていないものが多いのですが、phpdbg ではなく PECL 拡張の vld で命令列をダンプした場合は、こちらの ZEND_ がない表記となっています。最近マニュアルへ書き加えられた比較的新しい命令については、vld ではなく処理系のソースコードなど別の情報源が使われたのか、ZEND_ の接頭辞が付いた表記なようです。vld は phpdbg と違って処理系に標準添付されていないため、自分でインストールする必要がありますが、vld だと命令ダンプの中にリテラルの内容も展開されるので、ぶっちゃけ vld の方が見やすいかもしれません。3v4l.org を使えば、簡単なスクリプトについては vld でのオペコードダンプを Web 上から確認することもできます。なお、現在の master ブランチでは phpdbg が更新されていて、PHP7 では print の出力にリテラルの内容が出るようになるはずです。

おまけ 1:コードを変えてその他の命令について確認してみる

すでに hello world の動作に必要な命令は必要十分に説明できました。が、せっかく150 以上の命令があるというのに、今回見るのがこの 2 命令だけというのは少し寂しい気もします。
元ネタが坂井さんのスライドであることを考えると邪道もいいところですが、せっかくなのでちょっとだけ PHP 側の例示コードを変えて寄り道してみましょう。

<?php
function hello($target) {
	$hi = 'hello, '.$target;
	return $hi;
}
    
printf('%s', hello('world'));

再度 phpdbg で命令列をダンプしてみます。

phpdbg -e hello2.php
phpdbg> print exec
[Context C:\Users\sj-i\work\test\hello2.php]
        L0-0 {main}() C:\Users\sj-i\work\test\hello2.php
                L2      0x22b4100 ZEND_NOP                       <unused> <unused>             <unused>
                L7      0x22b4130 ZEND_SEND_VAL                  C1       <unused>             <unused>
                L7      0x22b4160 ZEND_SEND_VAL                  C2       <unused>             <unused>
                L7      0x22b4190 ZEND_DO_FCALL                  C3       <unused>             @0
                L7      0x22b41c0 ZEND_SEND_VAR_NO_REF           @0       <unused>             <unused>
                L7      0x22b41f0 ZEND_DO_FCALL                  C4       <unused>             @1
                L7      0x22b4220 ZEND_RETURN                    C5       <unused>             <unused>
phpdbg> info literal
[Literal Constants in C:\Users\sj-i\work\test\hello2.php (5)]
|-------- C1 -------> [%s]
|-------- C2 -------> [world]
|-------- C3 -------> [hello]
|-------- C4 -------> [printf]
|-------- C5 -------> [1]

ちょっとは賑やかになりましたね!

しかしさて、勘の良い人ならこの時点で気付くかもしれませんが、関数 hello() 内の命令列がどこにもありません。
PHP ソースコードのバイトコードへのコンパイル結果は、関数ごと別々の配列へ格納されます。コンパイル結果の命令列を保持する配列は op_array と呼ばれています。phpdbg の print 命令はこの op_array の内容をダンプするものです。
関数外で書かれた処理は暗黙的な関数が存在するかのように独立した op_array に格納され、これが先ほど「実行コンテキスト」とだけ触れてスルーしていたダンプの2行目、 L0-0 {main}() の部分に対応します。
関数 hello() については別途で命令列のダンプを出す必要があります。

phpdbg> print func hello
[User Function hello]
        L2-5 hello() C:\Users\sj-i\work\test\hello2.php
                L2      0x1f24f98 ZEND_RECV                      <unused> <unused>             $target
                L3      0x1f24fc8 ZEND_CONCAT                    C0       $target              @0
                L3      0x1f24ff8 ZEND_ASSIGN                    $hi      @0                   @1
                L4      0x1f25028 ZEND_RETURN                    $hi      <unused>             <unused>
                L5      0x1f25058 ZEND_RETURN                    C1       <unused>             <unused>

こちらが関数 hello() の命令列ですが、これだけだとリテラルの方が見れません。
phpdbg のコンテキストを hello() 内に移すため、ブレークポイントを仕掛けて hello() 内で実行を止めることにしてみます。

phpdbg> break 4
[Breakpoint #0 added at C:\Users\sj-i\work\test\hello2.php:4]

これで PHP ソースコードの 4 行目、hello() 内の return $hi; の部分で実行が止まるようになりました。次はこのブレークポイントの部分までスクリプトを実行します。

phpdbg> run
[Breakpoint #0 at C:\Users\sj-i\work\test\hello2.php:4, hits: 1]
 00003:         $hi = 'hello, '.$target;
\>00004:         return $hi;
 00005: }
  00006:
phpdbg> info literal
[Literal Constants in hello() (1)]
|-------- C0 -------> [hello, ]

うまくいったようです! hello() 内のリテラルが出せました。

関数の呼び出し処理

PHP コード オペコード オペランド1 オペランド2 結果格納先
function hello($target) {
	$hi = 'hello, '.$target;
	return $hi;
}
ZEND_NOP
printf('%s', hello('world'));
ZEND_SEND_VAL “%s”
ZEND_SEND_VAL “world”
ZEND_DO_FCALL “hello” @0
ZEND_SEND_VAR_NO_REF @0
ZEND_DO_FCALL “printf” @1
ZEND_RETURN 1

まずは関数の呼び出し側、{main}() 部分の処理についてコードを見ていくことにしましょう。

最初の ZEND_NOP は NOP(No OPeration)、つまり何も実行されません。PHP ソースコード上では関数 hello() の定義部分と対応しています*1

次の ZEND_SEND_VAL はオペランドをスタックへ送る命令です。ここでいうスタックは CPU のスタックポインタが指すものではなく、Zend Engine が関数引数の受け渡し用に使う LIFO のデータ構造です。’%s’、’world’ の順で printf() と hello() の引数がスタックに積み上げられます。

ZEND_DO_FCALL は関数を呼び出す命令です。オペランドは関数名で、これによって hello() が呼び出され、結果が @0 というレジスタに保存されます。このレジスタは必要に応じて @1、@2、@3……とメモリ上に確保されていくもので、実際の CPU のような有限個のものではありません(もちろん、利用可能なメモリ容量による制限はあるのですが)。

ZEND_SEND_VAR_NO_REF はこちらもオペランドをスタックへ送る命令で、 @0 に入った hello() の結果がスタックへ積まれます。スタックへ最後に積まれた ‘world’ は hello() 内で消費されているため、この時点のスタックには ‘%s’、 hello() からの返り値の順でデータが積まれていることになります。

最後に ZEND_DO_FCALL で標準関数の printf() が呼ばれ、暗黙的に付与される ZEND_RETURN 1 で実行が終了します。

  • *1この ZEND_NOP は早期バインドの残骸です。実際には、コンパイル時点で最初に関数の定義部分は ZEND_DECLARE_FUNCTION というオペコードを生成します。PHP では if 文の中で関数定義を行うなどすることで、実際に使う関数定義を実行時に切り替えることができるのですが、ZEND_DECLARE_FUNCTION はこの関数名と関数定義の紐付け(や多重定義のチェック)を実行時に行うための命令といえます。しかし if 文などの外で無条件に定義される関数については、コンパイル時点であらかじめ紐付けを済ませておくという早期バインドが行われます。早期バインドが行われる場合、 ZEND_DECLARE_FUNCTION はコンパイル時のオペコード生成直後に ZEND_NOP へ置き換えられます。このあたりの詳細について興味があれば、Andy Wharmby さんが PHP のオペコードについての素晴らしい資料を残しているため*2、参考にしつつ zend_do_early_binding() の処理を追ってみるとよいでしょう。
  • *2Understanding PHP Opcodes

呼び出される側の処理

次は hello() 内の命令を順に見ていきましょう。一応、解説済みの呼び出し側の処理とまとめて表にしておきます。

PHP コード オペコード オペランド1 オペランド2 結果格納先
printf('%s', hello('world'));
ZEND_SEND_VAL “%s”
ZEND_SEND_VAL “world”
ZEND_DO_FCALL “hello” @0
ZEND_SEND_VAR_NO_REF @0
ZEND_DO_FCALL “printf” @1
ZEND_RETURN 1
function hello($target) {
	$hi = 'hello, '.$target;
	return $hi;
}
ZEND_RECV $target
ZEND_CONCAT “hello, “ $target @0
ZEND_ASSIGN $hi @0 @1
ZEND_RETURN $hi
ZEND_RETURN (NULL)

最初の ZEND_RECV はスタックから値を取り出す命令です。結果は変数 $target に格納され、{main}() から hello() を呼び出す前に最後に積んでいた ‘world’ が入ります。Zend Engine の VM はこのように、レジスタや定数表の他、変数についてもシンボルテーブルを直接参照して値の格納や読み込みが可能です。

次の ZEND_CONCAT は文字列の連結命令で、’hello, ‘ と $target 内の ‘world’ が連結され、結果はレジスタ @0 に格納されます。

ZEND_ASSIGN は代入命令で、第 1 オペランドの $hi へ連結結果の ‘hello, world’ が格納されます。代入式の結果は レジスタ @1 にも格納されますが、このプログラム上では使われません。

そして return 文に対応する ZEND_RETURN で変数 $hi を呼び出し元へ返します。

最後に付け加えられている ZEND_RETURN ですが、これは関数内で明示的な return が行われなかった際に、NULL を返したものと見なされる言語仕様*1と対応したものです。リテラル表にない C1 を参照していますが、実際には NULL が返ります。

なんとなく、Zend Engine のバイトコードがどのようなものなのかのノリがつかめてきたのではないでしょうか?

おまけ2: include と require

もう 1 つおまけで、PHP の include / require についても少しだけ触れておきます。
include / require (と eval)には専用のオペコードが存在しています。

PHP: INCLUDE_OR_EVAL – Manual

<?php
echo include 'hello.php';
<?php
return 'hello, world';
phpdbg -e main.php
phpdbg> print exec
[Context C:\Users\sj-i\work\test\main.php]
        L0-0 {main}() C:\Users\sj-i\test\main.php
                L2      0x2234100 ZEND_INCLUDE_OR_EVAL           C0       <unused>             @0
                L2      0x2234130 ZEND_ECHO                      @0       <unused>             <unused>
                L3      0x2234160 ZEND_RETURN                    C1       <unused>             <unused>
phpdbg> info literal
[Literal Constants in C:\Users\sj-i\work\test\main.php (1)]
|-------- C0 -------> [hello.php]
|-------- C1 -------> [1]

まず ZEND_INCLUDE_OR_EVAL 命令*1にファイル名が渡され、この段階で hello.php が処理系に読み込まれます。そして hello.php 内にある「return ‘hello, world';」の部分がバイトコードへ変換され、実行されます。図示すると以下の様な形になります。

include

  • *1ZEND_INCLUDE_OR_EVAL 命令は名前の通り include / require の他に eval にも対応しているのですが、命令列のダンプ結果にはこれらを区別する情報がないのを不思議に思うかもしれません。これは phpdbg の問題です。実際には一部の命令は、2 つのオペランドの他に extended_value というデータをとる場合があり、PHP 5.6 の phpdbg は ZEND_INCLUDE_OR_EVAL 命令の extended_value の表示に対応していないようです。vld だと extended_value での違いもちゃんと表示してくれます。

コンパイルの流れ

Zend Engine のバイトコードが実際にどのようなものなのかを説明してきました。
一応、PHP スクリプトがバイトコードへ変換されるまでの流れも簡単に触れておきます。

字句解析

PHP ファイルが処理系に読み込まれた時、最初に字句解析器(lexer や tokenizerとも呼ばれます)がスクリプトを字句(トークン)に分解します。これは PHP スクリプト自体からもアクセスできるのと同じものです。

PHP: Tokenizer – Manual

他の多くの言語と同様、字句解析器の作成にはジェネレータが使われており、PHP の場合は re2c から自動生成されています。

試しに、この Tokenizer を使って最初の hello world の字句解析結果を取得してみましょう。

<?php
$source = file_get_contents('hello.php');
$tokens = token_get_all($source);

var_dump(array_map(function($token) {
   if (is_array($token)) {
       return [
           token_name($token[0]),
           $token[1]
       ];
   }
   return [
       '(terminal)',
       $token
   ];
}, $tokens));

実行結果は以下のようになります。

php tokenize.php
array(5) {
  [0]=>
  array(2) {
    [0]=>
    string(10) "T_OPEN_TAG"
    [1]=>
    string(6) "<?php
"
  }
  [1]=>
  array(2) {
    [0]=>
    string(6) "T_ECHO"
    [1]=>
    string(4) "echo"
  }
  [2]=>
  array(2) {
    [0]=>
    string(12) "T_WHITESPACE"
    [1]=>
    string(1) " "
  }
  [3]=>
  array(2) {
    [0]=>
    string(26) "T_CONSTANT_ENCAPSED_STRING"
    [1]=>
    string(14) "'hello, world'"
  }
  [4]=>
  array(2) {
    [0]=>
    string(10) "(terminal)"
    [1]=>
    string(1) ";"
  }
}
トークン名 ソースコードでの表現
T_OPEN_TAG <?php
T_ECHO echo
T_WHITESPACE
T_CONSTANT_ENCAPSED_STRING ‘hello, world’
(terminal) ;

構文解析+コード生成

トークン列は構文解析器(パーサ)が字句解析器のトークン取得処理を必要に応じ呼び出していくことで、先頭から順に取得されます。この際に T_OPEN_TAG と T_WHITESPACE は読み飛ばされ、実質的に構文解析には使われません。

構文解析とは特定の文法・構文規則に従って入力文字列(ソースファイル)を解析し、入力がその言語で正しく書かれたものであるか(文法エラーがないかどうか)を検証する処理です。この構文解析を行うプログラムを、構文解析器やパーサと呼びます。

PHP のパーサはこの構文解析処理の過程において、与えられたトークン列が構文規則に沿っているかを解析しつつ、直接バイトコードを生成します。他の言語では最初にパーサが構文解析を行い、抽象構文木(AST: Abstract Syntax Tree)を生成した上でコードの生成へと進むものも多いのですが、5.6 までの PHP ではパーサから直接バイトコードが生成されます*1

構文解析の処理は、一般的なパーサジェネレータである bison を使って作られています。詳しい説明は bison のマニュアルや構文解析についての入門書を読んでほしいのですが、ざっくり言うと、変形 BNF 的な書式で構文規則を与えると、ボトムアップの構文解析法である LALR 法にもとづいた構文解析器の C 言語コードを自動生成してくれるツールです。

bison では構文規則へ C 言語で書いた処理(アクション)を添えておくと、生成される構文解析器はその非終端記号への還元が発生する際、ついでにその添えておいた処理を実行してくれるものとなります。このついでに実行される処理によって、スクリプトに構文エラーがないかの確認という構文解析処理と同時に、PHP のバイトコードが op_array へ生成されていきます。

hello world 専用 PHP 処理系を使っての解説

構文解析の大体のノリを説明するため、パーサのソースである zend_language_parser.y から、hello world の実行に関係する構文規則のみをここに抜き出します。zend_language_parser.y から「%expects 3」の行を削除し、「%%」から「%%」までの部分を以下の内容に置き換えると、文字列定数 1 つの echo 文のみを受け付ける、ほぼ hello world 専用の PHP 処理系を作ることができます。

start: top_statement_list { zend_do_end_compilation(TSRMLS_C); }

top_statement_list: top_statement_list { zend_do_extended_info(TSRMLS_C); } top_statement { HANDLE_INTERACTIVE(); } | /* empty */

top_statement: statement { zend_verify_namespace(TSRMLS_C); }

statement: unticked_statement { DO_TICKS(); }

unticked_statement: T_ECHO echo_expr_list ';'

echo_expr_list: expr { zend_do_echo(&$1 TSRMLS_CC); }

expr: expr_without_variable { $$ = $1; }

expr_without_variable: scalar { $$ = $1; }

scalar: common_scalar { $$ = $1; }

common_scalar: T_CONSTANT_ENCAPSED_STRING { $$ = $1; }
  1. start は構文規則の起点で、この文法では top_statement_list が許容されており、
  2. top_statement_list というのは top_statement_list 自身(再帰表現)と top_statement の並び、もしくは空(つまり top_statement の繰り返し)で、
  3. top_statement というのは statement から成り、
  4. statement というのは unticked_statement から成り、
  5. unticked_statement というのは T_ECHO echo_expr_list ‘;’ の並びから成り、……

というような読み方ができます。言語構造のある部分の名前を左辺、またその部分の構成要素を右辺とした書式(BNF 記法)によって、構文規則が定義されています。

この構文規則にもとづき、開始点(start)から順に左辺を右辺へ置き換えるという変換(導出)を次々に適用していった結果として、ある入力文字列を生成できるのであれば、その入力文字列はその構文規則によって受理される、つまり、その言語で正しく書かれたプログラムである、ということが判定できます。

T_ECHO や T_CONSTANT_ENCAPSED_STRING、';’ のように、この構文規則でそれ以上置き換えの行われない要素を終端記号と呼びます(実際にソースコードへ現れるトークンと同じもの)。common_scalar や expr のように、置き換えの過程でのみ使われる構造を非終端記号と呼びます。

さて、実はこの構文規則にはもう 1 つの読み方があります。開始点である start からの導出を繰り返して終端記号の並びへ置き換えられるまで続ける、というのの逆に、

  1. T_CONSTANT_ENCAPSED_STRING は common_scalar であり、scalar であり、expr_without_variable であり、expr であり、echo_expr_list であり
  2. T_ECHO echo_expr_list ‘;’ の並びは unticked_statement であり、……

と、終端記号の方から構文規則をたどっていき、最終的に start まで置き換えを行えるのであれば、やはりその入力文字列はその構文規則によって受理される、という判定ができます。このように終端記号の方から開始点へさかのぼる形で行う構文解析を、ボトムアップ型の構文解析と呼びます。またこの際に行われる、右辺を左辺の非終端記号へ置き換える、という導出と逆向きの変換を還元と呼びます。ボトムアップの構文解析器を人手で書くのは難しいものの、プログラムで自動生成させる方法については手法が確立されています。

bison で生成されるボトムアップ型の構文解析器はスタックを持つステートマシンで、まず入力の先頭から 1 つトークンを読み込んでスタックへ積み、そのトークンを右辺に持つ構文規則の中で、適切なものが見つかり次第還元を行います。還元に使ったスタック上のトークン列は、還元先の非終端記号へ置き換えられます。適切な構文規則が見つからなければ、入力から更にトークンを 1 つ読み込んでスタックへ積み、再び適切な構文規則を探して還元を行おうとする、ということを繰り返していきます。最終的に開始点である start まで還元できれば、受理=構文エラーなくパースに成功したということになります。また還元の際に実行される zend_do_echo() の呼び出しによって、コンパイル結果のバイトコード(PHP 7 の場合は AST)も無事生成されたということになります。

hello world 専用構文をパースする

さて、この hello world(文字列定数の echo)のみを受け付ける構文規則において、実際に hello world でどのように構文解析が行われるかをシミュレーションしてみましょう*1

hello world のトークンは T_ECHO(echo)、T_CONSTANT_ENCAPSED_STRING(’hello, world’)、';’ の順に、3 つが字句解析器から読み込みます。

最初に T_ECHO を読み込み、これをスタックへ置きます*2

T_ECHO 単体を還元できる構文規則はありませんので、T_CONSTANT_ENCAPSED_STRING を更にスタックへ積み上げます。ここでスタック上の T_CONSTANT_ENCAPSED_STRING を common_scalar、scalar、expr_without_variable、echo_expr_list まで還元します。この段階で zend_do_echo()、つまり ZEND_ECHO 命令の op_array への生成処理が実行されます。

ここまでの入力で、スタック上には T_ECHO echo_expr_list の順で記号が置かれています。

‘;’ を更に読み込み、スタックへ積んで unticked_statement へ還元します。

それから statement、top_statement、top_statement_list と還元していき、最後に開始記号である start へ還元されたところで入力が受理、つまりエラーなくコンパイルが終了した、という状態になります。

終端記号から start までの還元の過程を図示すると、以下のような木構造(解析木 / 導出木 / 具象構文木)が得られます。

php_hello_tree

この木の完成が構文解析の成功を意味します。

さて、この木構造自体に何か使い道があるかどうかですが、大抵のプログラミング言語でコード生成にこの具象構文木が使われることはありません。具象構文木には導出の過程など、プログラムの実行自体には不要な情報が多く含まれます。大抵の言語は具象構文木をデータ構造としてメモリ上へ構築することはせず、構文解析の過程で不要な情報を取り除いた抽象構文木(AST)を生成するか、3番地コード(4つ組)のような別の中間表現を構築し、実際のマシン語へ変換したりインタプリタで実行したりするのに使います。5.6 までの PHP は後者ということになります(Zend Engine のオペコードと、Wikipedia の 3 番地コードの構造を見比べてみてください)。

  • *1この例は実際よりもだいぶ話を単純化しています。たとえば実際の PHP は、定数 1 つの echo 以外にも様々な構文を受け付ける言語です。本物の PHP の構文規則においては、ある時点の構文解析器が、トークンを入力文字列から取得するのか、それともスタック上のトークンを還元するのか、またその場合どの構文規則を使って還元するか、ということがそれほど自明ではないケースもあります。構文解析器がいつどのように動くのか、という問題を、LALR(1) の構文解析器は状態遷移表や入力トークンの先読みといった機構を利用して効率的に解決します。
  • *2bison で使われる LALR(1) では、1 トークン分の先読みが行われるため、実際にはこの時点で次にある T_CONSTANT_ENCAPSED_STRING の先読みも行われます。

PHP ソースコードレベルでの流れ

しょうきですか?
しょうきならやめなさい
きょうきのさたならしかたない
かきたいならなおしかたない
— このネタで記事を書くと伝えたときの私の上司

最もポピュラーな SAPI である mod_php5 を例に、PHP のソースコード(内部実装のC言語関数)のレベルで、スクリプトがどのように実行されるかを見てみます。

現時点で自分がこのページのスクロールバーのどの位置を見ているか確認すると、一瞬気が遠くなってしまうかもしれません。まあ実際に多少長い旅にはなるのですが、大丈夫です。大部分は C 言語のコードで、実質的な文量はさほど多くありません(大丈夫、ですよね?)。もうこの記事の本文については、半分以上読み終わったところです。

PHP 5.6 に標準添付されている SAPI で、apache と名の付くモジュールは幾つか存在します。Apache2 の mod_php5 に対応するのは apache2handler です。
PHP4 で当初導入された Apache2 用の SAPI は apache2filter というもので、これは名前通り Apache の Filter として動作するものでした*1

後に Justin Erenkrantz がこれへのパッチとして Filter 機能を使わない apache2handler を作り*2、PHP 4.3.2 からのデフォルトとなっています*3*4

PHP 5.6 の段階ではまだ apache2filter も PHP のソースツリー内に含まれているのですが、特にメンテナンスされているものではないため、Apache 1.x 系用の SAPI である apache / apache_hooks とともに、PHP7 では削除されています*5

Apache へのフックの登録

php_source_map_register_hook

AP_MODULE_DECLARE_DATA

Developing modules for the Apache HTTP Server 2.4

Apache 2 の拡張モジュールは公式ドキュメントに書かれている通り、AP_MODULE_DECLARE_DATA の宣言からはじまります。

AP_MODULE_DECLARE_DATA module php5_module = {
	STANDARD20_MODULE_STUFF,
	create_php_config,		/* create per-directory config structure */
	merge_php_config,		/* merge per-directory config structures */
	NULL,					/* create per-server config structure */
	NULL,					/* merge per-server config structures */
	php_dir_cmds,			/* command apr_table_t */
	php_ap2_register_hook	/* register hooks */
};

これは Apache の拡張モジュールで必要なコールバック関数を登録するための宣言です。

コメントに書かれている通りなのですが、これにより mod_php 内の create_php_config() という関数が Apache のディレクトリごとの設定を構築するのに使われ、また merge_php_config() によってディレクトリごとの設定が合成されるようになります。php_dir_cmds は .htaccess や httpd.conf において、php_value や php_flag といった PHP 専用ディレクティブが使われた際に呼び出される処理を登録する関数テーブルです。

そして今回特に重要なのは php_ap2_register_hook() で、これは Apache が mod_php をその動作に組み込む際、どのタイミングで mod_php 内の処理を呼び出すかという、フックの登録に使われる関数です。

php_ap2_register_hook

void php_ap2_register_hook(apr_pool_t *p)
{
	ap_hook_pre_config(php_pre_config, NULL, NULL, APR_HOOK_MIDDLE);
	ap_hook_post_config(php_apache_server_startup, NULL, NULL, APR_HOOK_MIDDLE);
	ap_hook_handler(php_handler, NULL, NULL, APR_HOOK_MIDDLE);
	ap_hook_child_init(php_apache_child_init, NULL, NULL, APR_HOOK_MIDDLE);
}

ap_hook_* は Apache のフックを登録する際に呼び出す関数です。* の部分がフックポイントの名前となっています。
フック登録関数の各引数はそれぞれ以下のような意味を持ち、呼び出す関数と呼び出しタイミングの細かな調整を行います。

第一引数 フックポイントで呼び出される関数
第ニ引数 同じフックポイントでより前に呼び出されるべき関数名のリスト
第三引数 同じフックポイントでより後に呼び出されるべき関数名のリスト
第四引数 呼び出しの大体の位置(順序)

よって、php_ap2_register_hook() の動作は pre_config、post_config、handler、child_init の各フックポイントで呼び出す関数を登録し、またそれらの関数は同じフックポイントに複数の Apache 拡張モジュールから関数が登録されている際、だいたい真ん中くらいの順番で呼び出されることが期待されていて、それ以上の細かい実行順は特に気にしていない、というものになります。

pre_config は Apache の設定が読み込まれた後、設定を処理する前の段階のフックポイントです。php_pre_config() は Apache が threaded MPM で、かつ PHP がスレッドセーフの設定でコンパイルされていない場合はエラーを吐いて、処理を開始する前にサーバを止める、という処理を行います。

post_config は Apache の設定が読み込まれ、各設定が処理された後の段階のフックポイントです。php_apache_server_startup() は SAPI から PHP 処理系の初期化を行う処理を持ちます。このタイミングで PHP の処理系が起動され、Apache とともにブラウザからのリクエストを待つことになります。

handler は Apache にブラウザからリクエストが送られ、リクエストの処理を開始した段階のフックポイントです。php_handler() はリクエストに応じて適切な PHP ファイルを読み込み、バイトコードへコンパイルし、バイトコードを逐次に処理しながらレスポンス(出力)を形成します。

PHP 処理系の起動

php_source_map_startup

php_apache_server_startup

それでは、Apache の post_config フックである php_apache_server_startup() から順に、処理の流れを追っていきましょう。

static int
php_apache_server_startup(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s)
{
	void *data = NULL;
	const char *userdata_key = "apache2hook_post_config";

	/* Apache will load, unload and then reload a DSO module. This
	 * prevents us from starting PHP until the second load. */
	apr_pool_userdata_get(&data, userdata_key, s->process->pool);
	if (data == NULL) {
		/* We must use set() here and *not* setn(), otherwise the
		 * static string pointed to by userdata_key will be mapped
		 * to a different location when the DSO is reloaded and the
		 * pointers won't match, causing get() to return NULL when
		 * we expected it to return non-NULL. */
		apr_pool_userdata_set((const void *)1, userdata_key, apr_pool_cleanup_null, s->process->pool);
		return OK;
	}

	/* Set up our overridden path. */
	if (apache2_php_ini_path_override) {
		apache2_sapi_module.php_ini_path_override = apache2_php_ini_path_override;
	}
#ifdef ZTS
	tsrm_startup(1, 1, 0, NULL);
#endif
	sapi_startup(&apache2_sapi_module);
	apache2_sapi_module.startup(&apache2_sapi_module);
	apr_pool_cleanup_register(pconf, NULL, php_apache_server_shutdown, apr_pool_cleanup_null);
	php_apache_add_version(pconf);

	return OK;
}

最初の分岐の部分は Apache の拡張モジュールで使われるイディオムです。Apache は起動時に拡張モジュールがきちんとロードできるかを確認するため、試しに一旦全拡張モジュールをロードし、これをアンロードした後で実際のロードをもう一度行います。この 2 回のロードの両方で post_config のフックポイントを通るため、単に post_config で初期化処理を行おうとすると、初期化処理が 2 回行われてしまうことになります。これを避けるため、最初の実行ではメモリにフラグをセットするだけでさっさと OK を返し、2 回目以降の実行(フラグがセットされているとき)で実際の初期化処理へ進むようになっています*1*2

その実際の初期化処理の箇所で呼び出されるのが、sapi_startup() です。

sapi_startup

sapi_startup() は SAPI の初期化関数で、これの呼び出しにより引数で渡される SAPI 構造体が PHP 処理系へ登録されます。SAPI 構造体はこの記事の最初の方で紹介した宣言を持つ C 言語構造体です。Apache の機能を呼び出すコールバックをあらかじめ登録しておくことにより、後で PHP 処理系へ処理を移してからも、Apache の機能を PHP 処理系側から利用することができるようになります。たとえば PHP の echo が Apache のレスポンス(ページ出力)形成に影響できるようになるわけです。

SAPI 構造体は mod_php5 では以下のような定義を持ちます。

static sapi_module_struct apache2_sapi_module = {
	"apache2handler",
	"Apache 2.0 Handler",

	php_apache2_startup,				/* startup */
	php_module_shutdown_wrapper,			/* shutdown */

	NULL,						/* activate */
	NULL,						/* deactivate */

	php_apache_sapi_ub_write,			/* unbuffered write */
	php_apache_sapi_flush,				/* flush */
	php_apache_sapi_get_stat,			/* get uid */
	php_apache_sapi_getenv,				/* getenv */

	php_error,					/* error handler */

	php_apache_sapi_header_handler,			/* header handler */
	php_apache_sapi_send_headers,			/* send headers handler */
	NULL,						/* send header handler */

	php_apache_sapi_read_post,			/* read POST data */
	php_apache_sapi_read_cookies,			/* read Cookies */

	php_apache_sapi_register_variables,
	php_apache_sapi_log_message,			/* Log message */
	php_apache_sapi_get_request_time,		/* Request Time */
	NULL,						/* Child Terminate */

	STANDARD_SAPI_MODULE_PROPERTIES
};

php_apache2_startup

php_apache_server_startup() 内で sapi_startup() の直後に呼び出している apache2_sapi_module.startup()、つまり php_apache2_startup() は、php_module_startup() を呼び出すだけの関数です。

static int php_apache2_startup(sapi_module_struct *sapi_module)
{
	if (php_module_startup(sapi_module, &php_apache_module, 1)==FAILURE) {
		return FAILURE;
	}
	return SUCCESS;
}

この php_module_startup() の中で、ほとんどの PHP 処理系初期化処理が行われます。ちょっと長い関数ですが、かいつまんで説明していくことにします。

php_module_startup

int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint num_additional_modules)
{
	zend_utility_functions zuf;
	zend_utility_values zuv;
	int retval = SUCCESS, module_number=0;	/* for REGISTER_INI_ENTRIES() */
	char *php_os;
	zend_module_entry *module;
#ifdef ZTS
	zend_executor_globals *executor_globals;
	void ***tsrm_ls;
	php_core_globals *core_globals;
#endif

#if defined(PHP_WIN32) || (defined(NETWARE) && defined(USE_WINSOCK))
	WORD wVersionRequested = MAKEWORD(2, 0);
	WSADATA wsaData;
#endif
#ifdef PHP_WIN32
	php_os = "WINNT";
#if _MSC_VER >= 1400
	old_invalid_parameter_handler =
		_set_invalid_parameter_handler(dummy_invalid_parameter_handler);
	if (old_invalid_parameter_handler != NULL) {
		_set_invalid_parameter_handler(old_invalid_parameter_handler);
	}

	/* Disable the message box for assertions.*/
	_CrtSetReportMode(_CRT_ASSERT, 0);
#endif
#else
	php_os=PHP_OS;
#endif

#ifdef ZTS
	tsrm_ls = ts_resource(0);
#endif

#ifdef PHP_WIN32
	php_win32_init_rng_lock();
#endif

	module_shutdown = 0;
	module_startup = 1;
	sapi_initialize_empty_request(TSRMLS_C);
	sapi_activate(TSRMLS_C);

	if (module_initialized) {
		return SUCCESS;
	}

	sapi_module = *sf;

	php_output_startup();

	zuf.error_function = php_error_cb;
	zuf.printf_function = php_printf;
	zuf.write_function = php_output_wrapper;
	zuf.fopen_function = php_fopen_wrapper_for_zend;
	zuf.message_handler = php_message_handler_for_zend;
	zuf.block_interruptions = sapi_module.block_interruptions;
	zuf.unblock_interruptions = sapi_module.unblock_interruptions;
	zuf.get_configuration_directive = php_get_configuration_directive_for_zend;
	zuf.ticks_function = php_run_ticks;
	zuf.on_timeout = php_on_timeout;
	zuf.stream_open_function = php_stream_open_for_zend;
	zuf.vspprintf_function = vspprintf;
	zuf.getenv_function = sapi_getenv;
	zuf.resolve_path_function = php_resolve_path_for_zend;
	zend_startup(&zuf, NULL TSRMLS_CC);

#ifdef ZTS
	executor_globals = ts_resource(executor_globals_id);
	ts_allocate_id(&core_globals_id, sizeof(php_core_globals), (ts_allocate_ctor) core_globals_ctor, (ts_allocate_dtor) core_globals_dtor);
	core_globals = ts_resource(core_globals_id);
#ifdef PHP_WIN32
	ts_allocate_id(&php_win32_core_globals_id, sizeof(php_win32_core_globals), (ts_allocate_ctor) php_win32_core_globals_ctor, (ts_allocate_dtor) php_win32_core_globals_dtor);
#endif
#else
	php_startup_ticks(TSRMLS_C);
#endif
	gc_globals_ctor(TSRMLS_C);

#ifdef PHP_WIN32
	{
		OSVERSIONINFOEX *osvi = &EG(windows_version_info);

		ZeroMemory(osvi, sizeof(OSVERSIONINFOEX));
		osvi->dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
		if( !GetVersionEx((OSVERSIONINFO *) osvi)) {
			php_printf("\nGetVersionEx unusable. %d\n", GetLastError());
			return FAILURE;
		}
	}
#endif
	EG(bailout) = NULL;
	EG(error_reporting) = E_ALL & ~E_NOTICE;
	EG(active_symbol_table) = NULL;
	PG(header_is_being_sent) = 0;
	SG(request_info).headers_only = 0;
	SG(request_info).argv0 = NULL;
	SG(request_info).argc=0;
	SG(request_info).argv=(char **)NULL;
	PG(connection_status) = PHP_CONNECTION_NORMAL;
	PG(during_request_startup) = 0;
	PG(last_error_message) = NULL;
	PG(last_error_file) = NULL;
	PG(last_error_lineno) = 0;
	EG(error_handling)  = EH_NORMAL;
	EG(exception_class) = NULL;
	PG(disable_functions) = NULL;
	PG(disable_classes) = NULL;
	EG(exception) = NULL;
	EG(objects_store).object_buckets = NULL;

#if HAVE_SETLOCALE
	setlocale(LC_CTYPE, "");
	zend_update_current_locale();
#endif

#if HAVE_TZSET
	tzset();
#endif

#if defined(PHP_WIN32) || (defined(NETWARE) && defined(USE_WINSOCK))
	/* start up winsock services */
	if (WSAStartup(wVersionRequested, &wsaData) != 0) {
		php_printf("\nwinsock.dll unusable. %d\n", WSAGetLastError());
		return FAILURE;
	}
#endif

	le_index_ptr = zend_register_list_destructors_ex(NULL, NULL, "index pointer", 0);

	/* Register constants */
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_VERSION", PHP_VERSION, sizeof(PHP_VERSION)-1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_MAJOR_VERSION", PHP_MAJOR_VERSION, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_MINOR_VERSION", PHP_MINOR_VERSION, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_RELEASE_VERSION", PHP_RELEASE_VERSION, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_EXTRA_VERSION", PHP_EXTRA_VERSION, sizeof(PHP_EXTRA_VERSION) - 1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_VERSION_ID", PHP_VERSION_ID, CONST_PERSISTENT | CONST_CS);
#ifdef ZTS
	REGISTER_MAIN_LONG_CONSTANT("PHP_ZTS", 1, CONST_PERSISTENT | CONST_CS);
#else
	REGISTER_MAIN_LONG_CONSTANT("PHP_ZTS", 0, CONST_PERSISTENT | CONST_CS);
#endif
	REGISTER_MAIN_LONG_CONSTANT("PHP_DEBUG", PHP_DEBUG, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_OS", php_os, strlen(php_os), CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_SAPI", sapi_module.name, strlen(sapi_module.name), CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("DEFAULT_INCLUDE_PATH", PHP_INCLUDE_PATH, sizeof(PHP_INCLUDE_PATH)-1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PEAR_INSTALL_DIR", PEAR_INSTALLDIR, sizeof(PEAR_INSTALLDIR)-1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PEAR_EXTENSION_DIR", PHP_EXTENSION_DIR, sizeof(PHP_EXTENSION_DIR)-1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_EXTENSION_DIR", PHP_EXTENSION_DIR, sizeof(PHP_EXTENSION_DIR)-1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_PREFIX", PHP_PREFIX, sizeof(PHP_PREFIX)-1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_BINDIR", PHP_BINDIR, sizeof(PHP_BINDIR)-1, CONST_PERSISTENT | CONST_CS);
#ifndef PHP_WIN32
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_MANDIR", PHP_MANDIR, sizeof(PHP_MANDIR)-1, CONST_PERSISTENT | CONST_CS);
#endif
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_LIBDIR", PHP_LIBDIR, sizeof(PHP_LIBDIR)-1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_DATADIR", PHP_DATADIR, sizeof(PHP_DATADIR)-1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_SYSCONFDIR", PHP_SYSCONFDIR, sizeof(PHP_SYSCONFDIR)-1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_LOCALSTATEDIR", PHP_LOCALSTATEDIR, sizeof(PHP_LOCALSTATEDIR)-1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_CONFIG_FILE_PATH", PHP_CONFIG_FILE_PATH, strlen(PHP_CONFIG_FILE_PATH), CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_CONFIG_FILE_SCAN_DIR", PHP_CONFIG_FILE_SCAN_DIR, sizeof(PHP_CONFIG_FILE_SCAN_DIR)-1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_SHLIB_SUFFIX", PHP_SHLIB_SUFFIX, sizeof(PHP_SHLIB_SUFFIX)-1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_STRINGL_CONSTANT("PHP_EOL", PHP_EOL, sizeof(PHP_EOL)-1, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_MAXPATHLEN", MAXPATHLEN, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_INT_MAX", LONG_MAX, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_INT_SIZE", sizeof(long), CONST_PERSISTENT | CONST_CS);

#ifdef PHP_WIN32
	REGISTER_MAIN_LONG_CONSTANT("PHP_WINDOWS_VERSION_MAJOR",      EG(windows_version_info).dwMajorVersion, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_WINDOWS_VERSION_MINOR",      EG(windows_version_info).dwMinorVersion, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_WINDOWS_VERSION_BUILD",      EG(windows_version_info).dwBuildNumber, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_WINDOWS_VERSION_PLATFORM",   EG(windows_version_info).dwPlatformId, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_WINDOWS_VERSION_SP_MAJOR",   EG(windows_version_info).wServicePackMajor, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_WINDOWS_VERSION_SP_MINOR",   EG(windows_version_info).wServicePackMinor, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_WINDOWS_VERSION_SUITEMASK",  EG(windows_version_info).wSuiteMask, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_WINDOWS_VERSION_PRODUCTTYPE", EG(windows_version_info).wProductType, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_WINDOWS_NT_DOMAIN_CONTROLLER", VER_NT_DOMAIN_CONTROLLER, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_WINDOWS_NT_SERVER", VER_NT_SERVER, CONST_PERSISTENT | CONST_CS);
	REGISTER_MAIN_LONG_CONSTANT("PHP_WINDOWS_NT_WORKSTATION", VER_NT_WORKSTATION, CONST_PERSISTENT | CONST_CS);
#endif

	php_binary_init(TSRMLS_C);
	if (PG(php_binary)) {
		REGISTER_MAIN_STRINGL_CONSTANT("PHP_BINARY", PG(php_binary), strlen(PG(php_binary)), CONST_PERSISTENT | CONST_CS);
	} else {
		REGISTER_MAIN_STRINGL_CONSTANT("PHP_BINARY", "", 0, CONST_PERSISTENT | CONST_CS);
	}

	php_output_register_constants(TSRMLS_C);
	php_rfc1867_register_constants(TSRMLS_C);

	/* this will read in php.ini, set up the configuration parameters,
	   load zend extensions and register php function extensions
	   to be loaded later */
	if (php_init_config(TSRMLS_C) == FAILURE) {
		return FAILURE;
	}

	/* Register PHP core ini entries */
	REGISTER_INI_ENTRIES();

	/* Register Zend ini entries */
	zend_register_standard_ini_entries(TSRMLS_C);

	/* Disable realpath cache if an open_basedir is set */
	if (PG(open_basedir) && *PG(open_basedir)) {
		CWDG(realpath_cache_size_limit) = 0;
	}

	/* initialize stream wrappers registry
	 * (this uses configuration parameters from php.ini)
	 */
	if (php_init_stream_wrappers(module_number TSRMLS_CC) == FAILURE)	{
		php_printf("PHP:  Unable to initialize stream url wrappers.\n");
		return FAILURE;
	}

	zuv.html_errors = 1;
	zuv.import_use_extension = ".php";
	php_startup_auto_globals(TSRMLS_C);
	zend_set_utility_values(&zuv);
	php_startup_sapi_content_types(TSRMLS_C);

	/* startup extensions statically compiled in */
	if (php_register_internal_extensions_func(TSRMLS_C) == FAILURE) {
		php_printf("Unable to start builtin modules\n");
		return FAILURE;
	}

	/* start additional PHP extensions */
	php_register_extensions_bc(additional_modules, num_additional_modules TSRMLS_CC);

	/* load and startup extensions compiled as shared objects (aka DLLs)
	   as requested by php.ini entries
	   theese are loaded after initialization of internal extensions
	   as extensions *might* rely on things from ext/standard
	   which is always an internal extension and to be initialized
	   ahead of all other internals
	 */
	php_ini_register_extensions(TSRMLS_C);
	zend_startup_modules(TSRMLS_C);

	/* start Zend extensions */
	zend_startup_extensions();

	zend_collect_module_handlers(TSRMLS_C);

	/* register additional functions */
	if (sapi_module.additional_functions) {
		if (zend_hash_find(&module_registry, "standard", sizeof("standard"), (void**)&module)==SUCCESS) {
			EG(current_module) = module;
			zend_register_functions(NULL, sapi_module.additional_functions, NULL, MODULE_PERSISTENT TSRMLS_CC);
			EG(current_module) = NULL;
		}
	}

	/* disable certain classes and functions as requested by php.ini */
	php_disable_functions(TSRMLS_C);
	php_disable_classes(TSRMLS_C);

	/* make core report what it should */
	if (zend_hash_find(&module_registry, "core", sizeof("core"), (void**)&module)==SUCCESS) {
		module->version = PHP_VERSION;
		module->info_func = PHP_MINFO(php_core);
	}


#ifdef PHP_WIN32
	/* Disable incompatible functions for the running platform */
	if (php_win32_disable_functions(TSRMLS_C) == FAILURE) {
		php_printf("Unable to disable unsupported functions\n");
		return FAILURE;
	}
#endif

	zend_post_startup(TSRMLS_C);

	module_initialized = 1;

	/* Check for deprecated directives */
	/* NOTE: If you add anything here, remember to add it to Makefile.global! */
	{
		struct {
			const long error_level;
			const char *phrase;
			const char *directives[16]; /* Remember to change this if the number of directives change */
		} directives[2] = {
			{
				E_DEPRECATED,
				"Directive '%s' is deprecated in PHP 5.3 and greater",
				{
					NULL
				}
			},
			{
				E_CORE_ERROR,
				"Directive '%s' is no longer available in PHP",
				{
					"allow_call_time_pass_reference",
					"define_syslog_variables",
					"highlight.bg",
					"magic_quotes_gpc",
					"magic_quotes_runtime",
					"magic_quotes_sybase",
					"register_globals",
					"register_long_arrays",
					"safe_mode",
					"safe_mode_gid",
					"safe_mode_include_dir",
					"safe_mode_exec_dir",
					"safe_mode_allowed_env_vars",
					"safe_mode_protected_env_vars",
					"zend.ze1_compatibility_mode",
					NULL
				}
			}
		};

		unsigned int i;

		zend_try {
			/* 2 = Count of deprecation structs */
			for (i = 0; i < 2; i++) {
				const char **p = directives[i].directives;

				while(*p) {
					long value;

					if (cfg_get_long((char*)*p, &value) == SUCCESS && value) {
						zend_error(directives[i].error_level, directives[i].phrase, *p);
					}

					++p;
				}
			}
		} zend_catch {
			retval = FAILURE;
		} zend_end_try();
	}

	sapi_deactivate(TSRMLS_C);
	module_startup = 0;

	shutdown_memory_manager(1, 0 TSRMLS_CC);
	zend_interned_strings_snapshot(TSRMLS_C);
 	virtual_cwd_activate(TSRMLS_C);

	/* we're done */
	return retval;
}

この関数の定義を持つ main.c というファイルがある main というディレクトリですが、ここには PHP のソース内で SAPI と連絡をとる部分、及びその他の入出力に関係する処理、つまり実行環境の基盤となる部分で環境非依存の処理が入っています。大雑把に言えば、PHP 処理系は SAPI 層 > PHP 実行環境層 > Zend Engine 層というように構成されている訳です。

php-components

php_output_startup() によって、PHP 実行環境層で使われる出力バッファの初期化を行います。

その後 zend_startup()*1 によって、言語処理系のコア部分である Zend Engine を初期化します。
引数で PHP 実行環境層、及び SAPI の処理をコールバックとして Zend Engine へ登録しているのが分かると思います。

その他、処理系内で利用される定数の登録や設定ファイルの読み込み、拡張機能のロードといった初期化処理が php_module_startup() 内で行われ、ここまでで Apache の post_config フックによって開始された初期化処理は終了です。
サーバ初期化処理の終了後、Apache がブラウザからリクエストを受け付けるごとに、次で説明する handler フックが起動され、PHP スクリプトについては mod_php がその処理を行うことになります。

  • *1zend_startup() 内では zend_compile_filezend_execute_ex といったグローバル変数を初期化しており、それぞれ PHP スクリプトファイルのバイトコードへのコンパイル、および実行を行う関数へのポインタとなっています。詳細についてはこの記事のスコープを外れてしまうため省きますが、このことはつまり関数ポインタの内容差し替えによって、Zend Engine のバイトコード実行エンジンを差し替えたり、構文を拡張することも可能だということを意味しています。
EG() とか PG() とか SG() とか

PHP のソースコード内では EG() とか PG()、SG() といった奇妙な関数マクロが使われていますが、これらはそれぞれ Executor Globals、PHP Globals、SAPI Globals の略です。いずれもコンパイル時のスレッドセーフモードに応じ、TSRM(スレッドセーフリソースマネージャ)*1もしくはグローバル変数からそれぞれの対応するリクエストグローバルな構造体メンバを参照するのに使われます。これらのマクロが使われている箇所では、リクエストごとの(プロセス/スレッドごとの)グローバルなデータ構造の初期化や参照が行われています。

EG() 主としてエンジン内で現在のリクエストの状態を把握するのに使われ、シンボルテーブルや関数/クラステーブル、定数やリソースといったデータが保持されます
PG() php.ini の設定など、主として PHP 実行環境層で設定され保持/利用するデータが保存されます
SG() SAPI 層で利用されるデータが保持されます
  • *1TSRM は SAPI と同じく、PHP 4 が出る前の頃に Shane Caraveo によって最初の基礎が作られました。Windows 環境や Apache の worker MPM のように、サーバがマルチプロセスではなくマルチスレッド動作する際、リクエストごとのグローバルなデータ構造を単にグローバル変数としてアクセスしようとすると、問題が出てしまいます。そこで ZTS(Zend Thread Safety) 有効で PHP がコンパイルされている場合は、グローバル変数ではなくスレッドごとに固有の領域からデータを取得するよう、スレッド固有のデータ構造を各関数呼び出しで引き回すようになっています。しかし当然ながら、これには一定のオーバーヘッドがあります。データ構造の引き回しは TSRMLS_* のようなマクロを各関数呼び出し時の引数へ付け加える形で実現されており、ZTS が無効なら TSRMLS_* は単に C 言語の プリプロセッサで消去され、単にグローバル変数を参照するようになることで、このオーバーヘッドが出なくなります。なお、PHP 7 では OS ネイティブの TLS がサポートされるようになったため*2、これら関数引数経由で引き回す TSRMLS_* のマクロは一斉に姿を消しています*3
  • *2PHP RFC: Native TLS
  • *3first shot remove TSRMLS_* things

リクエスト処理の開始

php_source_map_process_request

php_handler

ここからは Apache の handler フック、つまり ap_hook_handler() で登録される php_handler()、リクエストごとに呼び出されるスクリプト処理の本体部分についての解説です。

static int php_handler(request_rec *r)
{
	php_struct * volatile ctx;
	void *conf;
	apr_bucket_brigade * volatile brigade;
	apr_bucket *bucket;
	apr_status_t rv;
	request_rec * volatile parent_req = NULL;
	TSRMLS_FETCH();

#define PHPAP_INI_OFF php_apache_ini_dtor(r, parent_req TSRMLS_CC);

	conf = ap_get_module_config(r->per_dir_config, &php5_module);

	/* apply_config() needs r in some cases, so allocate server_context early */
	ctx = SG(server_context);
	if (ctx == NULL || (ctx && ctx->request_processed && !strcmp(r->protocol, "INCLUDED"))) {
normal:
		ctx = SG(server_context) = apr_pcalloc(r->pool, sizeof(*ctx));
		/* register a cleanup so we clear out the SG(server_context)
		 * after each request. Note: We pass in the pointer to the
		 * server_context in case this is handled by a different thread.
		 */
		apr_pool_cleanup_register(r->pool, (void *)&SG(server_context), php_server_context_cleanup, apr_pool_cleanup_null);
		ctx->r = r;
		ctx = NULL; /* May look weird to null it here, but it is to catch the right case in the first_try later on */
	} else {
		parent_req = ctx->r;
		ctx->r = r;
	}
	apply_config(conf);

	if (strcmp(r->handler, PHP_MAGIC_TYPE) && strcmp(r->handler, PHP_SOURCE_MAGIC_TYPE) && strcmp(r->handler, PHP_SCRIPT)) {
		/* Check for xbithack in this case. */
		if (!AP2(xbithack) || strcmp(r->handler, "text/html") || !(r->finfo.protection & APR_UEXECUTE)) {
			PHPAP_INI_OFF;
			return DECLINED;
		}
	}

	/* Give a 404 if PATH_INFO is used but is explicitly disabled in
	 * the configuration; default behaviour is to accept. */
	if (r->used_path_info == AP_REQ_REJECT_PATH_INFO
		&& r->path_info && r->path_info[0]) {
		PHPAP_INI_OFF;
		return HTTP_NOT_FOUND;
	}

	/* handle situations where user turns the engine off */
	if (!AP2(engine)) {
		PHPAP_INI_OFF;
		return DECLINED;
	}

	if (r->finfo.filetype == 0) {
		php_apache_sapi_log_message_ex("script '%s' not found or unable to stat", r TSRMLS_CC);
		PHPAP_INI_OFF;
		return HTTP_NOT_FOUND;
	}
	if (r->finfo.filetype == APR_DIR) {
		php_apache_sapi_log_message_ex("attempt to invoke directory '%s' as script", r TSRMLS_CC);
		PHPAP_INI_OFF;
		return HTTP_FORBIDDEN;
	}

	/* Setup the CGI variables if this is the main request */
	if (r->main == NULL ||
		/* .. or if the sub-request environment differs from the main-request. */
		r->subprocess_env != r->main->subprocess_env
	) {
		/* setup standard CGI variables */
		ap_add_common_vars(r);
		ap_add_cgi_vars(r);
	}

zend_first_try {

	if (ctx == NULL) {
		brigade = apr_brigade_create(r->pool, r->connection->bucket_alloc);
		ctx = SG(server_context);
		ctx->brigade = brigade;

		if (php_apache_request_ctor(r, ctx TSRMLS_CC)!=SUCCESS) {
			zend_bailout();
		}
	} else {
		if (!parent_req) {
			parent_req = ctx->r;
		}
		if (parent_req && parent_req->handler &&
				strcmp(parent_req->handler, PHP_MAGIC_TYPE) &&
				strcmp(parent_req->handler, PHP_SOURCE_MAGIC_TYPE) &&
				strcmp(parent_req->handler, PHP_SCRIPT)) {
			if (php_apache_request_ctor(r, ctx TSRMLS_CC)!=SUCCESS) {
				zend_bailout();
			}
		}

		/*
		 * check if coming due to ErrorDocument
		 * We make a special exception of 413 (Invalid POST request) as the invalidity of the request occurs
		 * during processing of the request by PHP during POST processing. Therefor we need to re-use the exiting
		 * PHP instance to handle the request rather then creating a new one.
		*/
		if (parent_req && parent_req->status != HTTP_OK && parent_req->status != 413 && strcmp(r->protocol, "INCLUDED")) {
			parent_req = NULL;
			goto normal;
		}
		ctx->r = r;
		brigade = ctx->brigade;
	}

	if (AP2(last_modified)) {
		ap_update_mtime(r, r->finfo.mtime);
		ap_set_last_modified(r);
	}

	/* Determine if we need to parse the file or show the source */
	if (strncmp(r->handler, PHP_SOURCE_MAGIC_TYPE, sizeof(PHP_SOURCE_MAGIC_TYPE) - 1) == 0) {
		zend_syntax_highlighter_ini syntax_highlighter_ini;
		php_get_highlight_struct(&syntax_highlighter_ini);
		highlight_file((char *)r->filename, &syntax_highlighter_ini TSRMLS_CC);
	} else {
		zend_file_handle zfd;

		zfd.type = ZEND_HANDLE_FILENAME;
		zfd.filename = (char *) r->filename;
		zfd.free_filename = 0;
		zfd.opened_path = NULL;

		if (!parent_req) {
			php_execute_script(&zfd TSRMLS_CC);
		} else {
			zend_execute_scripts(ZEND_INCLUDE TSRMLS_CC, NULL, 1, &zfd);
		}

		apr_table_set(r->notes, "mod_php_memory_usage",
			apr_psprintf(ctx->r->pool, "%" APR_SIZE_T_FMT, zend_memory_peak_usage(1 TSRMLS_CC)));
	}

} zend_end_try();

	if (!parent_req) {
		php_apache_request_dtor(r TSRMLS_CC);
		ctx->request_processed = 1;
		bucket = apr_bucket_eos_create(r->connection->bucket_alloc);
		APR_BRIGADE_INSERT_TAIL(brigade, bucket);

		rv = ap_pass_brigade(r->output_filters, brigade);
		if (rv != APR_SUCCESS || r->connection->aborted) {
zend_first_try {
			php_handle_aborted_connection();
} zend_end_try();
		}
		apr_brigade_cleanup(brigade);
	} else {
		ctx->r = parent_req;
	}

	return OK;
}

コードの前半部分は hello world の解説にはさほど必要ない部分で、重要なのは php_execute_script() の呼び出し部分です。しかし退屈かもしれませんが、そこへ至るまでの流れについても一応簡単な解説を添えておきます。

php_handler() がポインタでとる引数 request_rec 構造体*1*2は、Apache から渡される HTTP リクエストについての情報を保持します。

ap_get_module_config() に request_rec 構造体の per_dir_config を渡すことで、.htaccess の php_value などによる設定内容をリクエストに応じて取得できます。これを mod_php5 内のルーチン apply_config() へ渡すことで、これから実行する PHP スクリプトの環境へ設定値を適用します。

request_rec 構造体の handler メンバの内容には、Apache 設定の SetHandler ディレクティブで指定されたハンドラ名が渡ってきます。よくあるセットアップでは、.php という拡張子のファイルに application/x-httpd-php というハンドラ名が設定されていることかと思います。
リクエストが mod_php5 によって処理されるべきものかをハンドラ名などの情報から判断し、そうでなければ DECLINED を返して他のハンドラに処理を任せたり、HTTP_NOT_FOUND や HTTP_FORBIDDEN を返してリクエストを終了したりします。

上記のような判定を乗り越え、リクエストが mod_php5 の担当範囲であることが明らかになった段階で、php_apache_request_ctor() を呼び出して PHP のリクエストの初期化処理を行います。

それから request_rec 構造体の filename でリクエストされた PHP ファイルを取得し、php_execute_script() によって実行します。ここでようやく hello world の PHP スクリプトが処理系に読み込まれます。

php_apache_request_ctor

static int php_apache_request_ctor(request_rec *r, php_struct *ctx TSRMLS_DC)
{
	char *content_length;
	const char *auth;

	SG(sapi_headers).http_response_code = !r->status ? HTTP_OK : r->status;
	SG(request_info).content_type = apr_table_get(r->headers_in, "Content-Type");
	SG(request_info).query_string = apr_pstrdup(r->pool, r->args);
	SG(request_info).request_method = r->method;
	SG(request_info).proto_num = r->proto_num;
	SG(request_info).request_uri = apr_pstrdup(r->pool, r->uri);
	SG(request_info).path_translated = apr_pstrdup(r->pool, r->filename);
	r->no_local_copy = 1;

	content_length = (char *) apr_table_get(r->headers_in, "Content-Length");
	SG(request_info).content_length = (content_length ? atol(content_length) : 0);

	apr_table_unset(r->headers_out, "Content-Length");
	apr_table_unset(r->headers_out, "Last-Modified");
	apr_table_unset(r->headers_out, "Expires");
	apr_table_unset(r->headers_out, "ETag");

	auth = apr_table_get(r->headers_in, "Authorization");
	php_handle_auth_data(auth TSRMLS_CC);

	if (SG(request_info).auth_user == NULL && r->user) {
		SG(request_info).auth_user = estrdup(r->user);
	}

	ctx->r->user = apr_pstrdup(ctx->r->pool, SG(request_info).auth_user);

	return php_request_startup(TSRMLS_C);
}

まずは php_apache_request_ctor() によるリクエストの初期化処理です。Apache からリクエスト情報を取得し、PHP 環境へ設定値を取り込んだ上で、最終的に php_request_startup() を呼び出します。

php_request_startup

この php_request_startup() の中で、コンパイラやバイトコード実行エンジンの初期化を行う zend_activate()、POST メソッドの場合に POST データを取得する sapi_activate() が順繰りに呼び出されます。

int php_request_startup(TSRMLS_D)
{
	int retval = SUCCESS;

#ifdef HAVE_DTRACE
	DTRACE_REQUEST_STARTUP(SAFE_FILENAME(SG(request_info).path_translated), SAFE_FILENAME(SG(request_info).request_uri), (char *)SAFE_FILENAME(SG(request_info).request_method));
#endif /* HAVE_DTRACE */

#ifdef PHP_WIN32
	PG(com_initialized) = 0;
#endif

#if PHP_SIGCHILD
	signal(SIGCHLD, sigchld_handler);
#endif

	zend_try {
		PG(in_error_log) = 0;
		PG(during_request_startup) = 1;

		php_output_activate(TSRMLS_C);

		/* initialize global variables */
		PG(modules_activated) = 0;
		PG(header_is_being_sent) = 0;
		PG(connection_status) = PHP_CONNECTION_NORMAL;
		PG(in_user_include) = 0;

		zend_activate(TSRMLS_C);
		sapi_activate(TSRMLS_C);

#ifdef ZEND_SIGNALS
		zend_signal_activate(TSRMLS_C);
#endif

		if (PG(max_input_time) == -1) {
			zend_set_timeout(EG(timeout_seconds), 1);
		} else {
			zend_set_timeout(PG(max_input_time), 1);
		}

		/* Disable realpath cache if an open_basedir is set */
		if (PG(open_basedir) && *PG(open_basedir)) {
			CWDG(realpath_cache_size_limit) = 0;
		}

		if (PG(expose_php)) {
			sapi_add_header(SAPI_PHP_VERSION_HEADER, sizeof(SAPI_PHP_VERSION_HEADER)-1, 1);
		}

		if (PG(output_handler) && PG(output_handler)[0]) {
			zval *oh;

			MAKE_STD_ZVAL(oh);
			ZVAL_STRING(oh, PG(output_handler), 1);
			php_output_start_user(oh, 0, PHP_OUTPUT_HANDLER_STDFLAGS TSRMLS_CC);
			zval_ptr_dtor(&oh);
		} else if (PG(output_buffering)) {
			php_output_start_user(NULL, PG(output_buffering) > 1 ? PG(output_buffering) : 0, PHP_OUTPUT_HANDLER_STDFLAGS TSRMLS_CC);
		} else if (PG(implicit_flush)) {
			php_output_set_implicit_flush(1 TSRMLS_CC);
		}

		/* We turn this off in php_execute_script() */
		/* PG(during_request_startup) = 0; */

		php_hash_environment(TSRMLS_C);
		zend_activate_modules(TSRMLS_C);
		PG(modules_activated)=1;
	} zend_catch {
		retval = FAILURE;
	} zend_end_try();

	SG(sapi_started) = 1;

	return retval;
}

リクエストの初期化処理はここまでです。
この後 php_handler() 内に処理が戻り、php_execute_script() が呼ばれ、最終的にこの内部で zend_execute_scripts() が呼ばれ、スクリプトのコンパイルと実行が行われます。

zend_execute_scripts

ZEND_API int zend_execute_scripts(int type TSRMLS_DC, zval **retval, int file_count, ...) /* {{{ */
{
	va_list files;
	int i;
	zend_file_handle *file_handle;
	zend_op_array *orig_op_array = EG(active_op_array);
	zval **orig_retval_ptr_ptr = EG(return_value_ptr_ptr);
    long orig_interactive = CG(interactive);

	va_start(files, file_count);
	for (i = 0; i < file_count; i++) {
		file_handle = va_arg(files, zend_file_handle *);
		if (!file_handle) {
			continue;
		}

        if (orig_interactive) {
            if (file_handle->filename[0] != '-' || file_handle->filename[1]) {
                CG(interactive) = 0;
            } else {
                CG(interactive) = 1;
            }
        }
       
		EG(active_op_array) = zend_compile_file(file_handle, type TSRMLS_CC);
		if (file_handle->opened_path) {
			int dummy = 1;
			zend_hash_add(&EG(included_files), file_handle->opened_path, strlen(file_handle->opened_path) + 1, (void *)&dummy, sizeof(int), NULL);
		}
		zend_destroy_file_handle(file_handle TSRMLS_CC);
		if (EG(active_op_array)) {
			EG(return_value_ptr_ptr) = retval ? retval : NULL;
			zend_execute(EG(active_op_array) TSRMLS_CC);
			zend_exception_restore(TSRMLS_C);
			if (EG(exception)) {
				if (EG(user_exception_handler)) {
					zval *orig_user_exception_handler;
					zval **params[1], *retval2, *old_exception;
					old_exception = EG(exception);
					EG(exception) = NULL;
					params[0] = &old_exception;
					orig_user_exception_handler = EG(user_exception_handler);
					if (call_user_function_ex(CG(function_table), NULL, orig_user_exception_handler, &retval2, 1, params, 1, NULL TSRMLS_CC) == SUCCESS) {
						if (retval2 != NULL) {
							zval_ptr_dtor(&retval2);
						}
						if (EG(exception)) {
							zval_ptr_dtor(&EG(exception));
							EG(exception) = NULL;
						}
						zval_ptr_dtor(&old_exception);
					} else {
						EG(exception) = old_exception;
						zend_exception_error(EG(exception), E_ERROR TSRMLS_CC);
					}
				} else {
					zend_exception_error(EG(exception), E_ERROR TSRMLS_CC);
				}
			}
			destroy_op_array(EG(active_op_array) TSRMLS_CC);
			efree(EG(active_op_array));
		} else if (type==ZEND_REQUIRE) {
			va_end(files);
			EG(active_op_array) = orig_op_array;
			EG(return_value_ptr_ptr) = orig_retval_ptr_ptr;
            CG(interactive) = orig_interactive;
			return FAILURE;
		}
	}
	va_end(files);
	EG(active_op_array) = orig_op_array;
	EG(return_value_ptr_ptr) = orig_retval_ptr_ptr;
    CG(interactive) = orig_interactive;

	return SUCCESS;
}

重要なのは zend_compile_file()、そして zend_execute() の呼び出しです。それぞれスクリプトのバイトコードへのコンパイルと実行を行います。zend_execute() は内部で zend_execute_ex() を呼び出します。つまり、先に zend_startup() 内で設定された 2 つの関数ポインタの指す処理が実行されるということです。
先に述べたように、このタイミングまでにこれらの関数ポインタを PHP の拡張モジュールから差し替え、構文を変更したりJIT コンパイラを導入したりすることも可能ですが、今回は PHP 処理系デフォルトのものを使う前提で話を進めます。

スクリプトのコンパイル

php_source_map_compile

zend_compile_file(compile_file)

ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type TSRMLS_DC)
{
	zend_lex_state original_lex_state;
	zend_op_array *op_array = (zend_op_array *) emalloc(sizeof(zend_op_array));
	zend_op_array *original_active_op_array = CG(active_op_array);
	zend_op_array *retval=NULL;
	int compiler_result;
	zend_bool compilation_successful=0;
	znode retval_znode;
	zend_bool original_in_compilation = CG(in_compilation);

	retval_znode.op_type = IS_CONST;
	INIT_PZVAL(&retval_znode.u.constant);
	ZVAL_LONG(&retval_znode.u.constant, 1);

	zend_save_lexical_state(&original_lex_state TSRMLS_CC);

	retval = op_array; /* success oriented */

	if (open_file_for_scanning(file_handle TSRMLS_CC)==FAILURE) {
		if (type==ZEND_REQUIRE) {
			zend_message_dispatcher(ZMSG_FAILED_REQUIRE_FOPEN, file_handle->filename TSRMLS_CC);
			zend_bailout();
		} else {
			zend_message_dispatcher(ZMSG_FAILED_INCLUDE_FOPEN, file_handle->filename TSRMLS_CC);
		}
		compilation_successful=0;
	} else {
		init_op_array(op_array, ZEND_USER_FUNCTION, INITIAL_OP_ARRAY_SIZE TSRMLS_CC);
		CG(in_compilation) = 1;
		CG(active_op_array) = op_array;
		zend_stack_push(&CG(context_stack), (void *) &CG(context), sizeof(CG(context)));
		zend_init_compiler_context(TSRMLS_C);
		compiler_result = zendparse(TSRMLS_C);
		zend_do_return(&retval_znode, 0 TSRMLS_CC);
		CG(in_compilation) = original_in_compilation;
		if (compiler_result != 0) { /* parser error */
			zend_bailout();
		}
		compilation_successful=1;
	}

	if (retval) {
		CG(active_op_array) = original_active_op_array;
		if (compilation_successful) {
			pass_two(op_array TSRMLS_CC);
			zend_release_labels(0 TSRMLS_CC);
		} else {
			efree(op_array);
			retval = NULL;
		}
	}
	zend_restore_lexical_state(&original_lex_state TSRMLS_CC);
	return retval;
}

compile_file() は関数ポインタ zend_compile_file にデフォルトで設定される関数で、字句解析処理と構文解析処理とオペコード生成処理のすべてがここで行われます。
この関数の定義を持つ zend_language_scanner.c 自体は zend_language_scanner.l から re2c によって生成されるもので、字句解析用の処理を含んでいます。
コンパイル後の命令列を格納する op_array を初期化した後、open_file_for_scanning() で字句解析用にファイルを開き、構文解析+バイトコード生成処理となる zendparse() を呼び出します。

#define yyparse         zendparse

zendparse() の実体は zend_lauguage_parser.c の yyparse() で、こちらは bison によって zend_language_parser.y から生成されます。yyparse() は zend_language_scanner.c 内の字句解析処理によってファイルからトークンを順繰りに取得し、構文規則へこれを当てはめていき、バイトコードを生成して op_array へ格納していきます。

そしてここまでの流れが正常に終了した後、pass_two() という関数が呼び出されます。
最初に op_array へバイトコードを格納した時点では、まだ生成されていない情報が幾つかあります。pass_two() は op_array 内の各命令について幾つかの情報を補足し(例えばジャンプ命令の宛先アドレスを実際に使われる絶対アドレスへ書き換えるなど)、また各命令に対応するインタプリタの処理(関数など)を紐付けていきます。

ZEND_API int pass_two(zend_op_array *op_array TSRMLS_DC)
{
	zend_op *opline, *end;

	if (op_array->type!=ZEND_USER_FUNCTION && op_array->type!=ZEND_EVAL_CODE) {
		return 0;
	}
	if (op_array->has_finally_block) {
		zend_resolve_finally_calls(op_array TSRMLS_CC);
	}
	if (CG(compiler_options) & ZEND_COMPILE_EXTENDED_INFO) {
		zend_update_extended_info(op_array TSRMLS_CC);
	}
	if (CG(compiler_options) & ZEND_COMPILE_HANDLE_OP_ARRAY) {
		zend_llist_apply_with_argument(&zend_extensions, (llist_apply_with_arg_func_t) zend_extension_op_array_handler, op_array TSRMLS_CC);
	}

	if (!(op_array->fn_flags & ZEND_ACC_INTERACTIVE) && CG(context).vars_size != op_array->last_var) {
		op_array->vars = (zend_compiled_variable *) erealloc(op_array->vars, sizeof(zend_compiled_variable)*op_array->last_var);
		CG(context).vars_size = op_array->last_var;
	}
	if (!(op_array->fn_flags & ZEND_ACC_INTERACTIVE) && CG(context).opcodes_size != op_array->last) {
		op_array->opcodes = (zend_op *) erealloc(op_array->opcodes, sizeof(zend_op)*op_array->last);
		CG(context).opcodes_size = op_array->last;
	}
	if (!(op_array->fn_flags & ZEND_ACC_INTERACTIVE) && CG(context).literals_size != op_array->last_literal) {
		op_array->literals = (zend_literal*)erealloc(op_array->literals, sizeof(zend_literal) * op_array->last_literal);
		CG(context).literals_size = op_array->last_literal;
	}

	opline = op_array->opcodes;
	end = opline + op_array->last;
	while (opline < end) {
		if (opline->op1_type == IS_CONST) {
			opline->op1.zv = &op_array->literals[opline->op1.constant].constant;
		}
		if (opline->op2_type == IS_CONST) {
			opline->op2.zv = &op_array->literals[opline->op2.constant].constant;
		}
		switch (opline->opcode) {
			case ZEND_GOTO:
				if (Z_TYPE_P(opline->op2.zv) != IS_LONG) {
					zend_resolve_goto_label(op_array, opline, 1 TSRMLS_CC);
				}
				/* break omitted intentionally */
			case ZEND_JMP:
			case ZEND_FAST_CALL:
				opline->op1.jmp_addr = &op_array->opcodes[opline->op1.opline_num];
				break;
			case ZEND_JMPZ:
			case ZEND_JMPNZ:
			case ZEND_JMPZ_EX:
			case ZEND_JMPNZ_EX:
			case ZEND_JMP_SET:
			case ZEND_JMP_SET_VAR:
				opline->op2.jmp_addr = &op_array->opcodes[opline->op2.opline_num];
				break;
			case ZEND_RETURN:
			case ZEND_RETURN_BY_REF:
				if (op_array->fn_flags & ZEND_ACC_GENERATOR) {
					if (opline->op1_type != IS_CONST || Z_TYPE_P(opline->op1.zv) != IS_NULL) {
						CG(zend_lineno) = opline->lineno;
						zend_error_noreturn(E_COMPILE_ERROR, "Generators cannot return values using \"return\"");
					}

					opline->opcode = ZEND_GENERATOR_RETURN;
				}
				break;
		}
		ZEND_VM_SET_OPCODE_HANDLER(opline);
		opline++;
	}

	op_array->fn_flags |= ZEND_ACC_DONE_PASS_TWO;
	return 0;
}

ZEND_VM_SET_OPCODE_HANDLER() という関数マクロに注目してください。
これは単に zend_vm_get_opcode_handler() という関数の呼び出しに展開されます。

#define ZEND_VM_SET_OPCODE_HANDLER(opline) zend_vm_set_opcode_handler(opline)

zend_vm_set_opcode_handler() は関数テーブルからオペコードに対応するハンドラ関数を取り出し、関数ポインタをそのオペコードを保持する構造体へ格納して紐付けていくという処理です。zend_vm_execute.h に定義があります。
そしてこのファイル zend_vm_execute.h は zend_vm_gen.php という PHP スクリプトによって、雛形となる zend_vm_def.h と zend_vm_execute.skl から自動生成されるものです。PHP 処理系のビルドには(部分的に)PHP 自体が使われているということですね。

zend_vm_gen.php

たとえば、zend_vm_def.h の ZEND_ECHO に対応する部分は以下のようになっています。

ZEND_VM_HANDLER(40, ZEND_ECHO, CONST|TMP|VAR|CV, ANY)
{
	USE_OPLINE
	zend_free_op free_op1;
	zval *z;

	SAVE_OPLINE();
	z = GET_OP1_ZVAL_PTR(BP_VAR_R);

	if (OP1_TYPE == IS_TMP_VAR && Z_TYPE_P(z) == IS_OBJECT) {
		INIT_PZVAL(z);
	}
	zend_print_variable(z);

	FREE_OP1();
	CHECK_EXCEPTION();
	ZEND_VM_NEXT_OPCODE();
}

zend_vm_gen.php を通すことで、デフォルトの設定ではこのコードは以下のようなコードへ展開されます。

static int ZEND_FASTCALL  ZEND_ECHO_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
	USE_OPLINE

	zval *z;

	SAVE_OPLINE();
	z = opline->op1.zv;

	if (IS_CONST == IS_TMP_VAR && Z_TYPE_P(z) == IS_OBJECT) {
		INIT_PZVAL(z);
	}
	zend_print_variable(z);

	CHECK_EXCEPTION();
	ZEND_VM_NEXT_OPCODE();
}
static int ZEND_FASTCALL  ZEND_ECHO_SPEC_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
	USE_OPLINE
	zend_free_op free_op1;
	zval *z;

	SAVE_OPLINE();
	z = _get_zval_ptr_tmp(opline->op1.var, execute_data, &free_op1 TSRMLS_CC);

	if (IS_TMP_VAR == IS_TMP_VAR && Z_TYPE_P(z) == IS_OBJECT) {
		INIT_PZVAL(z);
	}
	zend_print_variable(z);

	zval_dtor(free_op1.var);
	CHECK_EXCEPTION();
	ZEND_VM_NEXT_OPCODE();
}
static int ZEND_FASTCALL  ZEND_ECHO_SPEC_VAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
	USE_OPLINE
	zend_free_op free_op1;
	zval *z;

	SAVE_OPLINE();
	z = _get_zval_ptr_var(opline->op1.var, execute_data, &free_op1 TSRMLS_CC);

	if (IS_VAR == IS_TMP_VAR && Z_TYPE_P(z) == IS_OBJECT) {
		INIT_PZVAL(z);
	}
	zend_print_variable(z);

	zval_ptr_dtor_nogc(&free_op1.var);
	CHECK_EXCEPTION();
	ZEND_VM_NEXT_OPCODE();
}
static int ZEND_FASTCALL  ZEND_ECHO_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
	USE_OPLINE

	zval *z;

	SAVE_OPLINE();
	z = _get_zval_ptr_cv_BP_VAR_R(execute_data, opline->op1.var TSRMLS_CC);

	if (IS_CV == IS_TMP_VAR && Z_TYPE_P(z) == IS_OBJECT) {
		INIT_PZVAL(z);
	}
	zend_print_variable(z);

	CHECK_EXCEPTION();
	ZEND_VM_NEXT_OPCODE();
}

元々の 1 つの ZEND_ECHO 用の処理の定義から、対応する PHP 処理系内での各パラメータタイプに応じて、4 つの C 言語関数の定義が生成されているのが分かります。C++ などの template が利用時の各型に応じたコードへ、コンパイル時に展開される作りと少し似ています。

CONST、TMP、VAR、CV というのはそれぞれオペランドの種類を表しており、オペランドが使われていないことを示す UNUSED とあわせて 5 種類のものがあります。

種類 概要
CONST PHP スクリプト内で定数として表される値
TMP この記事の先の説明で「レジスタ」と呼んでいたもの、PHP スクリプト内では式の途中でのみ使われるなど、参照カウントを必要としない一時変数
VAR シンボルテーブル内に名前を持つ、PHP スクリプト内で変数として表されるもの
UNUSED オペランドが指定されていないときにあてられるもの
CV Compiled Variables の略で、キャッシュを使い関数内での一部の変数参照についてシンボルテーブルのルックアップを減らせるようにした高速版 VAR

各オペコード用の関数定義の生成と同時に、zend_vm_gen.php は(オペコード, オペランド)の組のデコードによって必要な関数を得られるような関数テーブルをディスパッチャとして生成します。

なお zend_vm_gen.php はこのディスパッチャの生成について、デフォルトの関数テーブル方式以外に 2 つの方法を提供しています。switch 文によるディスパッチャを生成したり、goto によるディスパッチャを生成することもできます。

スクリプトの実行

php_source_map_execute

zend_execute

ZEND_API void execute_ex(zend_execute_data *execute_data TSRMLS_DC)
{
	DCL_OPLINE
	zend_bool original_in_execution;



	original_in_execution = EG(in_execution);
	EG(in_execution) = 1;

	if (0) {
zend_vm_enter:
		execute_data = i_create_execute_data_from_op_array(EG(active_op_array), 1 TSRMLS_CC);
	}

	LOAD_REGS();
	LOAD_OPLINE();

	while (1) {
    	int ret;
#ifdef ZEND_WIN32
		if (EG(timed_out)) {
			zend_timeout(0);
		}
#endif

		if ((ret = OPLINE->handler(execute_data TSRMLS_CC)) > 0) {
			switch (ret) {
				case 1:
					EG(in_execution) = original_in_execution;
					return;
				case 2:
					goto zend_vm_enter;
					break;
				case 3:
					execute_data = EG(current_execute_data);
					break;
				default:
					break;
			}
		}

	}
	zend_error_noreturn(E_ERROR, "Arrived at end of main loop which shouldn't happen");
}

ZEND_API void zend_execute(zend_op_array *op_array TSRMLS_DC)
{
	if (EG(exception)) {
		return;
	} 
	zend_execute_ex(i_create_execute_data_from_op_array(op_array, 0 TSRMLS_CC) TSRMLS_CC);
}

PHP ファイルが無事コンパイルされた後、zend_execute() が op_array に格納されたバイトコードを実行します。
zend_execute 内では zend_execute_ex が呼び出されていますが、これは関数ポインタで、デフォルトでは zend_startup() 内で設定される execute_ex() が使われます。
execute_ex() は zend_vm_execute.h に定義があり、zend_vm_gen.php によって雛形である zend_vm_execute.skl からディスパッチ方式に応じて生成されます。

ZEND_API void {%EXECUTOR_NAME%}_ex(zend_execute_data *execute_data TSRMLS_DC)
{
	DCL_OPLINE
	zend_bool original_in_execution;

	{%HELPER_VARS%}

	{%INTERNAL_LABELS%}

	original_in_execution = EG(in_execution);
	EG(in_execution) = 1;

	if (0) {
zend_vm_enter:
		execute_data = i_create_execute_data_from_op_array(EG(active_op_array), 1 TSRMLS_CC);
	}

	LOAD_REGS();
	LOAD_OPLINE();

	while (1) {
    {%ZEND_VM_CONTINUE_LABEL%}
#ifdef ZEND_WIN32
		if (EG(timed_out)) {
			zend_timeout(0);
		}
#endif

		{%ZEND_VM_DISPATCH%} {
			{%INTERNAL_EXECUTOR%}
		}

	}
	zend_error_noreturn(E_ERROR, "Arrived at end of main loop which shouldn't happen");
}

デフォルトでは関数テーブル方式のディスパッチとなり、pass_two() で op_array の各命令へ紐付けられた処理ハンドラを、ループで順繰りに呼び出していきます。

なお単純な hello world では関係ありませんが、一応補足しておきます。ユーザ定義関数の呼び出しでは ZEND_DO_FCALL が実行されるのですが、内部では op_array とローカル変数用のシンボルテーブルとを切り替え、再帰的に zend_execute() を呼び出すような処理となっています。

出力処理

php_source_map_output

ZEND_ECHO の処理

先に見たように、hello world で使われる ZEND_ECHO は出力する値を zend_print_variable() に渡す処理となっています。zend_print_variable()zend_print_zval() のラッパーで、内部では zend_write() によって値を出力しようとします。

ZEND_API int zend_print_variable(zval *var) 
{
	return zend_print_zval(var, 0);
}
ZEND_API int zend_print_zval(zval *expr, int indent) /* {{{ */
{
	return zend_print_zval_ex(zend_write, expr, indent);
}
/* }}} */

ZEND_API int zend_print_zval_ex(zend_write_func_t write_func, zval *expr, int indent) /* {{{ */
{
	zval expr_copy;
	int use_copy;

	zend_make_printable_zval(expr, &expr_copy, &use_copy);
	if (use_copy) {
		expr = &expr_copy;
	}
	if (Z_STRLEN_P(expr) == 0) { /* optimize away empty strings */
		if (use_copy) {
			zval_dtor(expr);
		}
		return 0;
	}
	write_func(Z_STRVAL_P(expr), Z_STRLEN_P(expr));
	if (use_copy) {
		zval_dtor(expr);
	}
	return Z_STRLEN_P(expr);
}
/* }}} */

zend_write は関数ポインタで、デフォルトでの実体は php_module_startup() から zend_startup() を通して Zend Engine へ渡された、php_output_wrapper() となります。

static int php_output_wrapper(const char *str, uint str_length)
{
	TSRMLS_FETCH();
	return php_output_write(str, str_length TSRMLS_CC);
}

php_output_wrapper() はその名の通り php_output_write() という別の関数のラッパーで、php_output_write() の処理は以下のようになっています。

/* {{{ int php_output_write(const char *str, size_t len TSRMLS_DC)
 * Buffered write */
PHPAPI int php_output_write(const char *str, size_t len TSRMLS_DC)
{
#if PHP_DEBUG
	if (len > UINT_MAX) {
		php_error(E_WARNING, "Attempt to output more than UINT_MAX bytes at once; "
				"output will be truncated %lu => %lu",
				(unsigned long) len, (unsigned long) (len % UINT_MAX));
	}
#endif
	if (OG(flags) & PHP_OUTPUT_DISABLED) {
		return 0;
	}
	if (OG(flags) & PHP_OUTPUT_ACTIVATED) {
		php_output_op(PHP_OUTPUT_HANDLER_WRITE, str, len TSRMLS_CC);
		return (int) len;
	}
	return php_output_direct(str, len);
}
/* }}} */

hello world において実行されるのはこの中の php_output_op() の呼び出しです。
この関数が出力のバッファリング機能を提供し、最終的には sapi_startup() で SAPI へ登録された ub_write() を呼び出して、PHP の実行環境、つまりこの場合 Apache のレスポンスへと出力を書き出します。

/* {{{ static void php_output_op(int op, const char *str, size_t len TSRMLS_DC)
 * Output op dispatcher, passes input and output handlers output through the output handler stack until it gets written to the SAPI */
static inline void php_output_op(int op, const char *str, size_t len TSRMLS_DC)
{
	php_output_context context;
	php_output_handler **active;
	int obh_cnt;

	if (php_output_lock_error(op TSRMLS_CC)) {
		return;
	}

	php_output_context_init(&context, op TSRMLS_CC);

	/*
	 * broken up for better performance:
	 *  - apply op to the one active handler; note that OG(active) might be popped off the stack on a flush
	 *  - or apply op to the handler stack
	 */
	if (OG(active) && (obh_cnt = zend_stack_count(&OG(handlers)))) {
		context.in.data = (char *) str;
		context.in.used = len;

		if (obh_cnt > 1) {
			zend_stack_apply_with_argument(&OG(handlers), ZEND_STACK_APPLY_TOPDOWN, php_output_stack_apply_op, &context);
		} else if ((SUCCESS == zend_stack_top(&OG(handlers), (void *) &active)) && (!((*active)->flags & PHP_OUTPUT_HANDLER_DISABLED))) {
			php_output_handler_op(*active, &context);
		} else {
			php_output_context_pass(&context);
		}
	} else {
		context.out.data = (char *) str;
		context.out.used = len;
	}

	if (context.out.data && context.out.used) {
		php_output_header(TSRMLS_C);

		if (!(OG(flags) & PHP_OUTPUT_DISABLED)) {
#if PHP_OUTPUT_DEBUG
			fprintf(stderr, "::: sapi_write('%s', %zu)\n", context.out.data, context.out.used);
#endif
			sapi_module.ub_write(context.out.data, context.out.used TSRMLS_CC);

			if (OG(flags) & PHP_OUTPUT_IMPLICITFLUSH) {
				sapi_flush(TSRMLS_C);
			}

			OG(flags) |= PHP_OUTPUT_SENT;
		}
	}
	php_output_context_dtor(&context);
}
/* }}} */

ub_write → ap_rwrite

さて、mod_php5 の Apache への登録から始まり、PHP 処理系の奥底へと深く潜ってきた長い旅もそろそろ終わりが近付いてきました。

最初に sapi_startup() において登録した SAPI 構造体 で、ub_write のコールバックに登録していたのは php_apache_sapi_ub_write() です。

static int
php_apache_sapi_ub_write(const char *str, uint str_length TSRMLS_DC)
{
	request_rec *r;
	php_struct *ctx;

	ctx = SG(server_context);
	r = ctx->r;

	if (ap_rwrite(str, str_length, r) < 0) {
		php_handle_aborted_connection();
	}

	return str_length; /* we always consume all the data passed to us. */
}

php_apache_sapi_ub_write() は Apache のライブラリ関数である ap_rwrite() を呼び出します。

これによって Apache のブラウザへの出力内容バッファへ ‘hello, world’ の 12 字が書き込まれます。
いや、ちょっと待ってください。php_output_op() の内容は PHP 側でもバッファリングされると書きました。このたった 12 文字分の echo だけでは、実際にはバッファのフラッシュが引き起こされず、ブラウザには何も出力されないのではないでしょうか?

リクエスト処理の終了

php_source_map_shutdown

php_apache_request_dtor → php_request_shutdown

出力はリクエストの終了時にちゃんと行われます。
リクエストを処理する php_handler() は、スクリプトのバイトコードの実行を終えた後に php_apache_request_dtor() を呼び出し、これがリクエストの終了処理を行う php_request_shutdown() を呼び出します。

void php_request_shutdown(void *dummy)
{
	zend_bool report_memleaks;
	TSRMLS_FETCH();

	report_memleaks = PG(report_memleaks);

	/* EG(opline_ptr) points into nirvana and therefore cannot be safely accessed
	 * inside zend_executor callback functions.
	 */
	EG(opline_ptr) = NULL;
	EG(active_op_array) = NULL;

	php_deactivate_ticks(TSRMLS_C);

	/* 1. Call all possible shutdown functions registered with register_shutdown_function() */
	if (PG(modules_activated)) zend_try {
		php_call_shutdown_functions(TSRMLS_C);
	} zend_end_try();

	/* 2. Call all possible __destruct() functions */
	zend_try {
		zend_call_destructors(TSRMLS_C);
	} zend_end_try();

	/* 3. Flush all output buffers */
	zend_try {
		zend_bool send_buffer = SG(request_info).headers_only ? 0 : 1;

		if (CG(unclean_shutdown) && PG(last_error_type) == E_ERROR &&
			(size_t)PG(memory_limit) < zend_memory_usage(1 TSRMLS_CC)
		) {
			send_buffer = 0;
		}

		if (!send_buffer) {
			php_output_discard_all(TSRMLS_C);
		} else {
			php_output_end_all(TSRMLS_C);
		}
	} zend_end_try();

	/* 4. Reset max_execution_time (no longer executing php code after response sent) */
	zend_try {
		zend_unset_timeout(TSRMLS_C);
	} zend_end_try();

	/* 5. Call all extensions RSHUTDOWN functions */
	if (PG(modules_activated)) {
		zend_deactivate_modules(TSRMLS_C);
		php_free_shutdown_functions(TSRMLS_C);
	}

	/* 6. Shutdown output layer (send the set HTTP headers, cleanup output handlers, etc.) */
	zend_try {
		php_output_deactivate(TSRMLS_C);
	} zend_end_try();

	/* 7. Destroy super-globals */
	zend_try {
		int i;

		for (i=0; i<NUM_TRACK_VARS; i++) {
			if (PG(http_globals)[i]) {
				zval_ptr_dtor(&PG(http_globals)[i]);
			}
		}
	} zend_end_try();

	/* 7.5 free last error information */
	if (PG(last_error_message)) {
		free(PG(last_error_message));
		PG(last_error_message) = NULL;
	}
	if (PG(last_error_file)) {
		free(PG(last_error_file));
		PG(last_error_file) = NULL;
	}

	/* 7. Shutdown scanner/executor/compiler and restore ini entries */
	zend_deactivate(TSRMLS_C);

	/* 8. Call all extensions post-RSHUTDOWN functions */
	zend_try {
		zend_post_deactivate_modules(TSRMLS_C);
	} zend_end_try();

	/* 9. SAPI related shutdown (free stuff) */
	zend_try {
		sapi_deactivate(TSRMLS_C);
	} zend_end_try();

	/* 9.5 free virtual CWD memory */
	virtual_cwd_deactivate(TSRMLS_C);

	/* 10. Destroy stream hashes */
	zend_try {
		php_shutdown_stream_hashes(TSRMLS_C);
	} zend_end_try();

	/* 11. Free Willy (here be crashes) */
	zend_try {
		shutdown_memory_manager(CG(unclean_shutdown) || !report_memleaks, 0 TSRMLS_CC);
	} zend_end_try();
	zend_interned_strings_restore(TSRMLS_C);

	/* 12. Reset max_execution_time */
	zend_try {
		zend_unset_timeout(TSRMLS_C);
	} zend_end_try();

#ifdef PHP_WIN32
	if (PG(com_initialized)) {
		CoUninitialize();
		PG(com_initialized) = 0;
	}
#endif

#ifdef HAVE_DTRACE
	DTRACE_REQUEST_SHUTDOWN(SAFE_FILENAME(SG(request_info).path_translated), SAFE_FILENAME(SG(request_info).request_uri), (char *)SAFE_FILENAME(SG(request_info).request_method));
#endif /* HAVE_DTRACE */
}

この中の php_output_end_all() の呼び出しにより、全出力バッファの内容が php_output_write() で SAPI へ書き出されます。ap_rwrite() で出力内容が Apache の出力バッファにわたってしまえば、あとは明示的に ap_rflush() が呼ばれなくとも、リクエストの終了時にはブラウザへ ‘hello, world’ が書き出されることになります。

さて、最初に示した ‘hello, world’ の実行に必要な情報ではないのですが、もし興味を持って php_output_end_all() の処理をのぞいてみた人がいたとして、その人があまり PHP の出力バッファの構成に詳しくない場合、この構造に少し面食らうかもしれません。ほんの少しだけここに説明を書いておきます。

PHPAPI void php_output_end_all(TSRMLS_D)
{
	while (OG(active) && php_output_stack_pop(PHP_OUTPUT_POP_FORCE TSRMLS_CC));
}

php_output_stack_pop() はスタックされた出力バッファの内容を php_output_write() で書き出しながら、1 つ 1 つスタックから取り除いていくという処理になっています。

PHP の出力バッファは階層構造をなしており、スタックで複数の出力バッファが管理されています。php.ini や起動時のオプションで無効化されていない場合、PHP 実行環境は最初にデフォルトで 1 つだけの出力バッファを持っています。PHP スクリプト内で ob_start() が呼び出された場合、これに加えて新たな出力バッファがスタック上に生成されていきます。この構造によって、ob_start() を入れ子にして利用するようなことが可能となっています。

このあたりの構造については、Julien Pauli さんが分かりやすい解説を書いています*1

最後に、ここへ至るまでの全体の流れを図にまとめます。

php_source_map

おわりに

以上、簡単にですが、PHP で hello world がどのように実行されるかを述べてきました。
正直なところ、ほぼノリだけで書いた記事をここまで読みきった人がいるか、というのはだいぶ怪しいように思うのですが、PHP の内部構造には上で説明してきた以外にもさらに多くの側面があります。
たとえば

  • 拡張機能がどのタイミングでどのようにスクリプトの実行に関与するのか
  • 処理系内部で PHP の変数などのデータがどのように保持されるのか(zval)
  • Zend Engine のメモリマネージャについて
  • Stream について

などは今回一切触れていませんし、またオペコードについても、オブジェクト指向機能などで使われる少し複雑な部分へは踏み込んでいません。

PHP 5.5.0 以降では OPcache が標準搭載されており、OPcache はその名の通りコンパイル結果のオペコードをキャッシュして2度目以降の実行を高速化する拡張機能なのですが、実は単純なキャッシュだけではなくオペコードの最適化まで行います。実行時の実際の挙動は、この記事の内容からほんの少しだけ違ってくることになります。

PHP 処理系の世界の下には更に C 言語やその実行環境の世界が広がってるわけで、たかが hello world ひとつといえども、真面目に調べてみるとわりと果てしない諸々が隠れてるんだなー、という感じがして、世の中面白いです。

元ネタである坂井さんの『ハロー・ワールド入門』が出て、更に下の世界へコンニチワできるのを超楽しみに待ってます!

参考情報

PHP の内部構造についての情報を探すのは結構めんどっちいことなので、似たような情報を探す人のため、最後に少しだけ参考リンクを並べておきます。

  • PHP internals 参考記事まとめ
    PHP 内部構造についての参考リンク集で、よくまとまっています。似たような時期に似たようなもの見てた人がいるんだなー、というのがちょっとおもしろかったです。
  • Understanding PHP Opcodes
    すでに記事内でも少しだけ触れてありますが、再度載せておきます。Andy Wharmby さんによる 2008 年のスライドで、少し古いものではありますが、バイトコードインタプリタまわりの挙動について非常に詳細に書かれています
  • How to add new (syntactic) features to PHP
    若さが妬ましい PHP 開発者の nikic さんによる記事で、PHP へ in 演算子を追加する段取りの解説記事です。言語機能の拡張にあたって必要となるオペコードまわりの扱いが詳しく説明されています。
  • Inside PHP
    Tom Lee さんによるスライドで、こちらは PHP に until を加える際の段取りについて簡単に紹介されています。より詳細について知ろうとすると別の情報源が欲しくなると思いますが、ある程度基礎知識がある人にはわかりやすいまとめになると思います。
  • Write your own SAPI
    Andrey Hristov さんによるスライドで、こちらは C++ の http ライブラリ PION に SAPI を通して PHP を組み込む際の流れが説明されています。

他にも「闇PHP勉強会」というフレーズでぐぐると、超かっこいい PHP ハックの日本語情報がいくつか出てきます。難しいこと考えずとりあえずいじってみようぜ!というノリで構文追加したりする感じの発表があったりして、とてもアツいです。

  • このエントリーをはてなブックマークに追加
  • Evernoteに保存