catalogue
1. SAPI接口 2. PHP CLI模式解釋執行腳本流程 3. PHP Zend Complile/Execute函數接口化(Hook Call架構基礎)
1. SAPI接口
PHP的SAPI層實現上層接口的封裝,使得PHP可以用在很多種模式場景下(例如apache、ningx、cgi、fastcgi、cli),以以cli SAPI為例子學習PHP解釋器引擎是如何處理PHP用戶態源代碼文件的
Cli(Command Line Interface)即PHP的命令行模式,現在此SAPI是默認安裝的,我們在服務器上安裝完PHP之后,一般會生成一個可執行文件
腳本執行的開始都是以SAPI接口實現開始的。只是不同的SAPI接口實現會完成他們特定的工作, 例如Apache的mod_php SAPI實現需要初始化從Apache獲取的一些信息,在輸出內容是將內容返回給Apache, 其他的SAPI實現也類似
0x1: sapi_module_struct
要定義個SAPI,首先要定義個sapi_module_struct
PHP-SRC/sapi/cli/php_cli.c
/* {{{ sapi_module_struct cli_sapi_module */ static sapi_module_struct cli_sapi_module = { "cli", /* name php_info()的時候被使用 */ "Command Line Interface", /* pretty name */ php_cli_startup, /* startup */ php_module_shutdown_wrapper, /* shutdown */ NULL, /* activate */ sapi_cli_deactivate, /* deactivate */ sapi_cli_ub_write, /* unbuffered write */ sapi_cli_flush, /* flush */ NULL, /* get uid */ NULL, /* getenv */ php_error, /* error handler */ sapi_cli_header_handler, /* header handler */ sapi_cli_send_headers, /* send headers handler */ sapi_cli_send_header, /* send header handler */ NULL, /* read POST data */ sapi_cli_read_cookies, /* read Cookies */ sapi_cli_register_variables, /* register server variables */ sapi_cli_log_message, /* Log message */ NULL, /* Get request time */ NULL, /* Child terminate */ STANDARD_SAPI_MODULE_PROPERTIES }; /* }}} */
這個結構,包含了一些常量,比如name, 這個會在我們調用php_info()的時候被使用。一些初始化,收尾函數,以及一些函數指針,用來告訴Zend,如何獲取,和輸出數據,我們在下面的流程介紹中就會逐個涉及到其中的字段
Relevant Link:
http://www.nowamagic.net/librarys/veda/detail/1285
2. PHP CLI模式解釋執行腳本流程
0x1: Process Startup
主進程main在進行一些必要的初始化工作后,就進入SAPI的邏輯流程,初始化的一些環境變量,這將在整個SAPI生命周期中發生作用
0x2: MINIT
進入特定的SAPI模式之后,PHP調用各個擴展的MINIT方法
\php-5.6.17\sapi\cli\php_cli.c
int main(int argc, char *argv[]) { .. sapi_module_struct *sapi_module = &cli_sapi_module; .. sapi_module->ini_defaults = sapi_cli_ini_defaults; sapi_module->php_ini_path_override = ini_path_override; sapi_module->phpinfo_as_text = 1; sapi_module->php_ini_ignore_cwd = 1; sapi_startup(sapi_module); sapi_started = 1; ..
php_cli_startup
static int php_cli_startup(sapi_module_struct *sapi_module) /* {{{ */ { if (php_module_startup(sapi_module, NULL, 0)==FAILURE) { return FAILURE; } return SUCCESS; }
PHP調用各個擴展的MINIT方法,從而使這些擴展切換到可用狀態
/* {{{ php_module_startup */ int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint num_additional_modules) { .. zend_module_entry *module; .. module_shutdown = 0; module_startup = 1; sapi_initialize_empty_request(TSRMLS_C); sapi_activate(TSRMLS_C); .. /* 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(); ..
MINIT的意思是"模塊初始化"。各個模塊都定義了一組函數、類庫等用以處理其他請求
一個典型的MINIT方法如下
PHP_MINIT_FUNCTION(extension_name){ /* Initialize functions, classes etc */ }
0x3: RINIT
當一個頁面請求發生時,SAPI層將控制權交給PHP層。於是PHP設置了用於回復本次請求所需的環境變量。同時,它還建立一個變量表,用來存放執行過程 中產生的變量名和值。PHP調用各個模塊的RINIT方法,即"請求初始化"
一個經典的例子是Session模塊的RINIT,如果在php.ini中 啟用了Session模塊,那在調用該模塊的RINIT時就會初始化$_SESSION變量,並將相關內容讀入
RINIT方法可以看作是一個准備過程, 在程序執行之前就會自動啟動。一個典型的RINIT方法如下
PHP_RINIT_FUNCTION(extension_name) { /* Initialize session variables,pre-populate variables, redefine global variables etc */ }
PHP會在每個request的時候,處理一些初始化,資源分配的事務。這部分就是activate字段要定義的,從上面的結構我們可以看出,從上面cli對應的cli_sapi_module結構體來看,對於CGI來說,它並沒有提供初始化處理句柄。對於mod_php來說,那就不同了,他要在apache的pool中注冊資源析構函數,申請空間, 初始化環境變量,等等
0x4: SCRIPT
PHP通過php_execute_script(&file_handle TSRMLS_CC)來執行PHP的腳本
\php-5.6.17\main\main.c
/* {{{ php_execute_script */ PHPAPI int php_execute_script(zend_file_handle *primary_file TSRMLS_DC) { //file_handle的類型為zend_file_handle,這個是zend對文件句柄的一個封裝,里面的內容和待執行腳本相關 zend_file_handle *prepend_file_p, *append_file_p; zend_file_handle prepend_file = {0}, append_file = {0}; .. //php_execute_script最終是調用的zend_execute_scripts retval = (zend_execute_scripts(ZEND_REQUIRE TSRMLS_CC, NULL, 3, prepend_file_p, primary_file, append_file_p) == SUCCESS); ..
php_execute_script最終是調用的zend_execute_scripts
{PHPSRC}/Zend/zend.c
//此函數具有可變參數,可以一次執行多個PHP文件 ZEND_API int zend_execute_scripts(int type TSRMLS_DC, zval **retval, int file_count, ...) /* {{{ */ { .. EG(active_op_array) = zend_compile_file(file_handle, type TSRMLS_CC); .. if (EG(active_op_array)) { EG(return_value_ptr_ptr) = retval ? retval : NULL; zend_execute(EG(active_op_array) TSRMLS_CC); ..
1. compile編譯過程
zend_compile_file是一個函數指針,其聲明在{PHPSRC}/Zend/zend_compile.c中
ZEND_API zend_op_array *(*zend_compile_file)(zend_file_handle *file_handle, int type TSRMLS_DC);
在引擎初始化的時候,會將compile_file函數的地址賦值給zend_compile_file,compile_file函數定義在{PHPSRC}/Zend/zend_language_scanner.l
//函數以zend_file_handle指針作為參數,返回一個指向zend_op_array的指針 ZEND_API zend_op_array *compile_file(zend_file_handle *file_handle, int type TSRMLS_DC) { .. //Lex詞法解析過程 ..
2. execute執行過程(逐條執行opcode)
zend_execute也是一個函數指針(利用compile過程得到的opcode array),其聲明在{PHPSRC}/Zend/zend_execute.c
ZEND_API extern void (*zend_execute)(zend_op_array *op_array TSRMLS_DC);
在引擎初始化的時候,會將execute函數的地址賦值給zend_execute,execute的定義在{PHPSRC}/Zend/zend_vm_execute.h
//zend_execute以一個指向zend_op_array結構的指針作為參數,這個指針即前面zend_compile_file的返回值,zend_execute就開始執行op_array中的op code,在執行op code的過程中,就實現了PHP語言的各種功能 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); }
0x5: RSHUTDOWN
一旦頁面執行完畢(無論是執行到了文件末尾還是用exit或die函數中止),PHP就會啟動清理程序。它會按順序調用各個模塊的RSHUTDOWN方法。 RSHUTDOWN用以清除程序運行時產生的符號表,也就是對每個變量調用unset函數
PHP_RSHUTDOWN_FUNCTION(extension_name) { /* Do memory management, unset all variables used in the last PHP call etc */ }
0x6: MSHUTDOWN
最后,所有的請求都已處理完畢,SAPI也准備關閉了,PHP開始執行第二步:PHP調用每個擴展的MSHUTDOWN方法,這是各個模塊最后一次釋放內存的機會
PHP_MSHUTDOWN_FUNCTION(extension_name) { /* Free handlers and persistent memory etc */ }
/main/main.c
/* {{{ php_module_shutdown_wrapper */ int php_module_shutdown_wrapper(sapi_module_struct *sapi_globals) { TSRMLS_FETCH(); php_module_shutdown(TSRMLS_C); return SUCCESS; }
Relevant Link:
http://www.nowamagic.net/librarys/veda/detail/1286 http://www.nowamagic.net/librarys/veda/detail/1322 http://www.nowamagic.net/librarys/veda/detail/1323 http://www.nowamagic.net/librarys/veda/detail/1332 http://blog.csdn.net/phpkernel/article/details/5716342 http://www.nowamagic.net/librarys/veda/detail/1287 http://www.nowamagic.net/librarys/veda/detail/1289
3. PHP Zend Complile/Execute函數接口化(Hook Call架構基礎)
PHP內核在設計架構實現的時候,除了提供了擴展機制,還在Zend的兩個關鍵流程(compile、execute)提供了Hook機制,PHP擴展開發人員可以Hook劫持Zend的編譯/解釋執行流程,在Zend編譯執行之前先執行自定義的代碼邏輯,然后再交還控制權給Zend。在引擎初始化(zend_startup)的時候
1. end_execute指向了默認的execute 2. zend_compile_file指向了默認的compile_file
我們可以在實際編譯和執行之前(RINIT階段中)將zend_execute和zend_compile_file重寫為其他的編譯和執行函數,這樣就為我們擴展引擎留下了鈎子,比如一個比較有名的查看PHP的op code的擴展vld,此擴展就是在每次請求初始化的鈎子函數(PHP_RINIT_FUNCTION)中,將zend_execute和zend_compile_file替換成自己的vld_execute和vld_compile_file,這兩個函數其實是對原始函數進行了封裝,添加了輸出opcode信息的附加功能,因為引擎初始化是發生在模塊請求初始化之前,而模塊請求初始化又是在編譯和執行之前,所以這樣的覆蓋能達到目的
Relevant Link:
Copyright (c) 2016 LittleHann All rights reserved