前言
这一次,我围绕Hello World来展开Zend虚拟机的执行过程。Hello World的PHP版本:
? ? ?echo 'Hello World';
?>
前一篇文章聊到的词法分析阶段就会把上边的脚本分析出一个Token序列:
我们得到一个Token序列:T_OPEN_TAG, T_ECHO, T_CONSTANT_ENCAPSED_STRING, ';', T_CLOSE_TAG。但在Zend虚拟机执行的过程中,是怎么去分析这个Token序列的?
跟踪运行轨迹
我们还是从命令行入手,在$PHPSRC/sapi/cli/php_cli.c中的do_cli函数里边接收了命令行的参数输入(php -f HelloWorld.php表示执行HelloWorld.php文件)。
我们追踪到$PHPSRC/main/main.c里边有php_execute_script的定义,紧接着调用了zend_execute_scripts()
?EG(active_op_array) =?zend_compile_file(file_handle, type TSRMLS_CC); zend_execute(EG(active_op_array) TSRMLS_CC);
首先通过zend_compile_file把文件解析成opcode中间代码(这一步会经过词法语法分析),然后用zend_execute执行这个生成的中间代码(这里就是所谓的运行时)。
这里很像C语言的编译方式,先编译成汇编,然后再转成机器码,这里的opcode就类似C语言编译过程中生成的汇编。
还可以延伸出一个思路,因为每次解析PHP文件时,都需要经过词法语法分析得到对应的opcode,其实在脚本文件不变化的时候,生成的opcode也不需要变化,因此为了减少PHP脚本的执行时间,可以把脚本的opcode缓存起来(例如缓存在共享内存里边)。
我给出一个流程图,然后随着这个流程图,看看Zend做了些什么事情:
我们先看看如何编译出opcode的。
词法语法分析->opcode
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(); } |???? T_STRING ':' { zend_do_label(&$1 TSRMLS_CC); } ;unticked_statement: |???? T_ECHO echo_expr_list ';'echo_expr_list: echo_expr_list ',' expr { zend_do_echo(&$3 TSRMLS_CC); } |???? expr???????????????????????? { zend_do_echo(&$1 TSRMLS_CC); } ; expr: r_variable???????????????????????? { $$ = $1; } |???? expr_without_variable????????? { $$ = $1; } ; expr_without_variable: |???? scalar??????????????????? { $$ = $1; } scalar: |???? common_scalar?????????????? { $$ = $1; } ; common_scalar: |???? T_CONSTANT_ENCAPSED_STRING???? { $$ = $1; } ;
语法分析从start开始,自上而下的分析,一个PHP脚本就是对应一个top_statement_list,接着分成每一行一条语句statement,发现echo 'Hello World'是一条unticked_statement(留意一下echo_expr_list的声明,?我们还可以发现语法上是支持echo 'Hello', ' World'的)。最后递归到T_CONSTANT_ENCAPSED_STRING状态就结束了这一行的语法解析。在这里我们忽略掉编译原理在语法分析阶段是怎么去做回溯等等东西,我们关注一下Zend引擎自身的的问题。
在规则后边的块"{}"里边的代码就是用来处理扫描到此规则时的动作,可以看到echo的执行是调用了zend_do_echo函数的。在动作声明的块里边我们看到了$$, $1,$2,$3等,这些对应的就是该条规则里边的返回值,参数1,参数2……,这里的返回值以及参数都是YYSTYPE类型,这个类型在43行里边有定义:#define YYSTYPE znode。znode的定义在zend_compile.h里边:
zend_op の構造に気づいたので追跡してみたところ、最終的に各ステートメントに対応するオペコード構造であることがわかりました。 ! ! !
オペコードの構造はアセンブリと非常によく似ており、1 つの演算子と 2 つのオペランドがあります。
Zend エンジンでは、各オペコードの主なものはハンドラーです。このハンドラーが Zend でどのように生成されるかについては後で説明します。ここで少し待って、Hello World の例によって生成されたオペコードを振り返ってみましょう。
vld をインストールして、php -dvld.active=1 HelloWorld.php を実行すると、この PHP ファイルによってコンパイルされたオペコード リストが表示されます。
echo ステートメントのオペコード タイプは ECHO であり、return には戻り値がなく、オペランド「Hello World」が 1 つだけあることがわかります。
構文分析後、各ステートメントのオペコードをコンパイルし、Zend はそれを op_array (実際にはオペコードのリスト) に入れます。
戻って zend_do_echo が何をしたか見てみましょう:
まず get_next_op を通じて現在の op_array の末尾にオペコードを生成し、次にそのオペコード タイプを ZEND_ECHO に設定し、次にその最初のパラメータ op1 を設定し、2 番目のパラメータ op2 を未使用としてマークします。
非常に多くの手順を経て、op_array のリストが得られました。このリスト内の各オペコードは独自のタイプにバインドされています。次に、各オペコード ノードがどのようにハンドラーにバインドされているかを見てみましょう。
zend_vm_def.h は、ZEND_ECHO のハンドラーを定義します。ここでの 40 に注意してください。これは、定数、変数など、複数のタイプのエコー パラメーターがあり、異なるハンドラーに対応するためです。
オペコードに対応するすべてのハンドラーは zend_vm_execute.h で定義されています。ここでは、エコー関連のハンドラーのみに注目して、次のコードに注目します。
void zend_init_opcodes_handlers(void) { static const opcode_handler_t labels[] = {//40913行 ZEND_ECHO_SPEC_CONST_HANDLER,//41914行 ZEND_ECHO_SPEC_CONST_HANDLER, ZEND_ECHO_SPEC_CONST_HANDLER, ZEND_ECHO_SPEC_CONST_HANDLER, ZEND_ECHO_SPEC_CONST_HANDLER };
ここでのラベルと行番号を思い出してください。
ハンドラーを取得するメソッドの最後にある return ステートメントの計算を発見しました。先ほどの echo のオペコードによれば、これは 40 です (2 つのパラメーター op1 と op2 の型が両方とも 0 であると仮定します)。したがって、対応するハンドラーは次のようになります:
zend_opcode_handlers[40*25 0*5 0*5] =?zend_opcode_handlers[1000] =?labels[1000] =?ZEND_ECHO_SPEC_CONST_HANDLER (どうしてそうなったのですか?理由: 41914 行-40913 行-1=1000)。
仮想マシンがオペコードを実行します
前に、zend_compile_file がスクリプトをオペコードのリストにコンパイルすることを説明しました。
?EG(active_op_array) =?
zend_compile_file(file_handle、タイプ TSRMLS_CC); zend_execute(EG(active_op_array) TSRMLS_CC); ;
この後、Zend エンジンは zend_execute を使用して、返されたオペコードを実行します。
zend_execute の最後の実行は、Zend/zend_vm_execute.h の 337 行目にあります。
ご覧のとおり、仮想マシンが実行されると、現在のオペコード リストをループし、オペコードの各行のハンドラーを呼び出し、次に何を行うかを決定します (関数呼び出しなど)。後で展開されます) ハンドラーの戻り値に基づきます。
この記事では、Hello World に関連するもののみに焦点を当てます。最終的な配置により、エコーのハンドラーが ZEND_ECHO_SPEC_CONST_HANDLER であることがわかります。
zend_write = (zend_write_func_t)utility_functions->write_function;
ここでのutility_functionsには、いくつかの基本的なハンドラーが含まれています。たとえば、コマンドラインモードでは、これは最終的に
と呼ばれます。
sapi_cli_single_write:
ソース コードから、最後の書き込み操作は write/fwrite を呼び出して標準出力ストリーム (つまり、端末画面上) に書き込むことであることがわかります。
結論
最後に、前のプロセスに基づいて、フローチャートを再度展開します。