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