搜尋
首頁後端開發php教程PHP傳參原理深入解析

在寫php擴充時,似乎參數(即傳給zend_parse_parameters的變數)是不需要free的。 舉例:

  1. PHP_FUNCTION(test)

  2. {
  3. char* str;
  4. int str_len;
  5. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &str, &str_len) == FAILURE) {

  6. RETURN_FALSE;
  7. }
  8. // 不需要free(str)
  9. }
複製程式碼
運作正常: test("Hello World"); // 列印Hello World 這裡不用擔心test函數會發生記憶體洩露,php會自動幫我們回收這些用來保存參數的變數。

那php究竟是如何做到的呢?要解釋這個問題,還是要看php是怎麼傳遞參數的。

EG(argument_stack)簡介 簡單來講,在php中的EG中保存了一個專門用來存放參數的棧,名為argument_stack。每當發生函數呼叫時,php會將傳入的參數壓進EG(argument_stack)。一旦函數呼叫結束,則EG(argument_stack)被清理,並且等待下一次的函數呼叫。

關於EG(argument_stack)的struct結構、用途,php5.2和5.3實作有些差異。本文主要以5.2為例,5.3+的變化後面抽空再說。

PHP傳參原理深入解析

上圖是5.2中argument_stack的大概示意圖,看起來簡單明了。其中,棧頂和棧底固定為NULL。函數接收的參數依照從左到右的順序,依序被壓入堆疊。請注意,最後會被額外壓入一個long型的值,表示棧裡的參數個數(上圖為10)。

那被壓入argument_stack的參數究竟是什麼呢?其實是一個個zval類型的指標。 它們指向的zva有可能是CV變量,有可能是is_ref=1的變量,還有可能是常數數字,或是常數字串。

EG(argument_stack)在php5.2中被具體實作為zend_ptr_stack類型:

  1. typedef struct _zend_ptr_stack {
  2. int top; // 堆疊中目前元素的數量
  3. int max;的數量
  4. void **elements; // 棧底
  5. void **top_element; // 棧頂
  6. } zend_ptr_stack;
複製程式碼

複製程式碼

初始化argument_stack 初始化argument_stack的工作是發生在php處理特定的請求之前,更準確地說是處於php解釋器的啟動過程之中。
在init_executor函數裡我們發現如下2行:
zend_ptr_stack_init(&EG(argument_stack));zend_ptr_stack_push(&EG(argument)> 🎜>複製程式碼

這2行分別代表著,初始化EG(argument_stack),緊接著壓入一個NULL。由於EG是個全域變量,因此在實際呼叫zend_ptr_stack_init之前,EG(argument_stack)中的所有資料全部為0。

zend_ptr_stack_init實作很簡單。

  1. ZEND_API void zend_ptr_stack_init(zend_ptr_stack *stack)
  2. {
  3. stack->topp. sizeof(void *)*PTR_STACK_BLOCK_SIZE);
  4. stack->max = PTR_STACK_BLOCK_SIZE; // 堆疊的大小初始化成64
  5. stack->top = 0; // 目前元素個數為0
  6. }
  7. }
複製程式碼

一旦argument_stack被初始化完,則立即會被壓入NULL。這裡無須深究,這個NULL其實沒有任何的意義。

PHP傳參原理深入解析NULL入棧之後,整個argument_stack的實際記憶體分佈如下:

參數入棧 在壓入第一個NULL之後,一旦再有參數入棧,則argument_stack會發生以下動作: stack->top++; *(stack->top_element++) = 參數;

用一段簡單的php程式碼來說明問題:
  1. function foo( $str ){
  2. print_r(123);
  3. }
  4. foo("hello world");
複製程式碼

上述程式碼在呼叫foo時,傳入了一個字串常數。因此,實際上被壓入堆疊的是一個指向儲存「hello world」的zval。用vld來查看編譯之後的opcode:

  1. line # * op fetch ext return operands
  2. --------------------- -------------------------------------------------- ----------
  3. 3 0 > NOP
  4. 6 1 SEND_VAL OP1[ IS_CONST (458754) 'hello world' ]
  5. 2 DO_FCALL 1 OP1[ IS_CONST (458752) 'foo' ]
  6. 15 3 > RETURN OP1[ IS_CONST (0) 1 ]
複製程式碼

SEND_VALhello指令」壓入argument_stack。

  1. int ZEND_SEND_VAL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

  2. {
  3. value = &opline->op1.u.constant;

  4. ALLOC_ZVAL(valptr);
  5. INIT_PZVAL_COPY(valptr, value);
  6. if (!0) { }
  7. // 入棧,valptr指向存放hello world的zval

  8. zend_ptr_stack_push(&EG(argument_stack), valptr);
  9. ……
  10. ……
  11. >}
複製程式碼
入棧完成之後的argument_stack為:

PHP傳參原理深入解析3參數個數 前文說到,其實並非把所有參數入棧就完事了。 php也會額外壓入一個數字,表示參數的個數,這個工作並非發生在SEND_XXX指令時。實際上,在真正執行函數之前,php會將參數個數入堆疊。

繼續沿用上面的例子,DO_FCALL 指令用來呼叫foo函數。在呼叫foo之前,php會自動填入argument_stack最後一塊。

    static int zend_do_fcall_common_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS)
  1. {
  2. …extended_value)
  3. // 一個是標識棧頂的NULL
  4. zend_ptr_stack_2_push(&EG(argument_stack), (void *)(zend_uintptr_t));
  5. ……
  6. if (EX(function_state).function->type == ZEND_INTERNAL_FUNCTION) {
  7. ……
  8. }
  9. else if (EX(function_state).==function->type ==function-> == ZEND_USER_FUNCTION) {
  10. ……
  11. // 呼叫foo函數
  12. zend_execute(EG(active_op_array) TSRMLS_CC);
  13. }
  14. else { /* ZENDLS_CC);
  15. }
  16. else { /* ZEND_OVERP> }
  17. ……
  18. // 清理argument_stack
  19. zend_ptr_stack_clear_multiple(TSRMLS_C);
  20. ……
  21. ZEND_VM_NEXT_OPCODE();
  22. >
  23. 壓入參數個數和NULL之後,用於foo呼叫的整個argument_stack已然完成。
  24. 取得參數 繼續跟進上面的例子。 深入foo函數,看看foo的opcode是什麼樣子的。
line # * op fetch ext return operands

--------------------- -------------------------------------------------- ----------PHP傳參原理深入解析 3 0 > RECV OP1[ IS_CONST (0) 1 ]

4 1 SEND_VAL OP1[ IS_CONST (5) 123 ]
2 DO_FCALL 1 OP1[ IS_CONST (459027 ) 'print_r' ]
5 3 > RETURN OP1[ IS_CONST (0) null ]
  1. 複製程式碼
  2. 第一條指令是RECV,從字面上理解便是用來取得堆疊中參數的。實際上,SEND_VAL和RECV有點對應的感覺。每次函數呼叫之前SEND_VAL,在函數內部進行RECV。為什麼不說是完全對應,實際上RECV指令並非一定需要。只有當使用者定義的函數被呼叫是,才會產生RECV。我們寫的擴充函數,php自帶的內建函數,都不會有RECV。

    需要額外指出的是,每次SEND_VAL和RECV 均只能處理一個參數。也就是說如果傳參的過程中有多個參數,那麼就會產生若干SEND_VAL以及若干RECV。這裡引出一個很有趣的議題,傳入參數和取得參數的順序是怎麼樣的呢?

    答案是,SEND_VAL會將參數從左至右的進行壓棧,而RECV一樣的則由左至右取得參數。

    1. static int ZEND_RECV_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

    2. {
    3. {棧底
    4. if (zend_ptr_stack_get_arg(arg_num, (void **) ¶m TSRMLS_CC)==FAILURE) {
    5. ……
    6. } else {
    7. zend_free_op free_resval🎜> } *var_ptr;
    8. // 驗證參數

    9. zend_verify_arg_type((zend_function *) EG(active_op_array), arg_num, *param TSRMLS_CC);
    10. var_ptr = get_zparam TSRMLS_CC);
    11. var_ptr = get_zparam TSRMLS_CC);
    12. var_ptr = get_zparam TSRMLS_CC);
    13. var_ptr = get_zparam. EX(Ts), &free_res, BP_VAR_W);
    14. // 取得參數
    15. if (PZVAL_IS_REF(*param)) {
    16. zend_assign_to_variable_reference(varm)) {
    17. zend_assign_to_variable_reference(varm)) {
    18. zend_assign_to_variable_reference(var_pRMs) }; 🎜> zend_receive(var_ptr, *param TSRMLS_CC);
    19. }
    20. }
    21. ZEND_VM_NEXT_OPCODE();複製程式碼

    zend_assign_to_variable_reference 和zend_receive 都會完成「取得參數」 。 「獲取參數」不太好理解,實際它究竟是做哪些事情?

    說到底很簡單,「取得參數」就是將這個參數加入到目前函數執行期間的「符號表」中,具體對應為EG(current_execute_data)->symbol_table。在本範例中,RECV完成之後,函數體內的symbol_table中有了一個符號‘str’,它的值為“hello world”。

    但argument_stack並沒有發生一絲變化,因為RECV只是讀取參數,而不會對堆疊產生類似pop操作。

    PHP傳參原理深入解析5清理argument_stack foo內部的print_r也是一個函數調用,因此也會產生壓棧-->清棧的操作。因此print_r執行之前的argument_stack為:

    PHP傳參原理深入解析6print_r執行之後argument_stack又回到了foo剛RECV完的狀態。

    具體呼叫print_r的過程並非本文闡述的重點。我們關心的是當呼叫foo結束之後,php是如何清理argument_stack的。

    上面展示的do_fcall程式碼片段中可以看出,清理工作由zend_ptr_stack_clear_multiple完成的。

      static inline void zend_ptr_stack_clear_multiple(TSRMLS_D)
    1. {
    2. voidoidp = EG(argument_p. / 取棧頂保存的參數個數
    3. int delete_count = (int)(zend_uintptr_t) *p;
    4. EG(argument_stack).top -= (delete_count+2);
    5. // 從上至下,依序清理
    6. while (--delete_count>=0) {
    7. zval *q = *(zval **)(--p);
    8. *p = NULL;
    9. zval_ptr_dtor (&q);
    10. }
    11. EG(argument_stack).top_element = p;
    12. }
    13. 複製程式碼
    在本文範例中,foo調用完畢之後,儲存「hello world」的zval狀態為:

    value "hello world"
    refcount 1
      type 6
    1. is_ref 0
    2. type 6
    3. is_ref 0
    複製程式碼

    由於refcount只剩1,因此,zval_ptr_dtor會將「hello world」真正從記憶體中銷毀。

    消棧完畢之後的argument_stack記憶體狀態為:

    PHP傳參原理深入解析7

    可以看出上圖的argument_stack與剛被初始化之後是一樣的。此時argument_stack真正做好了迎接下一次函數呼叫的準備。

    回到文章剛開始的問題... 為何無需free(str)呢?弄清楚了argument_stack之後就很好理解這個問題了。

    因為str指向的是zval中實際存放「hello world」的記憶體位址。假設擴充函數如下:

    1. PHP_FUNCTION(test)

    2. {
    3. char* str;
    4. int str_len;
    5. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &str, &str_len) == FAILURE) {

    6. RETURN_FALSE;
    7. }}
    複製程式碼
    則調用

    1. $a = "hello world";
    2. test($a);
    3. echo $a;
    複製程式碼

    會輸出「Hello world」。儘管我們呼叫test時,並非是傳$a的引用,但實際效果相當於test(&$a)。

    簡單來說,記憶體中只有一份$a,不管是CV數組中,還是在argument_stack。而zend_parse_parameters並沒有拷貝一份資料用於函數執行,事實上它也不能這麼做。因此,當函數完成之後,如果沒有其他地方會用到$a,php清理argument_stack時會幫我們free。如果仍然其他程式碼在使用,就更加不能手動free了,否則會破壞$a的記憶體區域。

    注意,並非寫擴充函數中用到的每個變量,php都會自動回收。所以該free時,切勿手軟:)



陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
PHP:服務器端腳本語言的簡介PHP:服務器端腳本語言的簡介Apr 16, 2025 am 12:18 AM

PHP是一種服務器端腳本語言,用於動態網頁開發和服務器端應用程序。 1.PHP是一種解釋型語言,無需編譯,適合快速開發。 2.PHP代碼嵌入HTML中,易於網頁開發。 3.PHP處理服務器端邏輯,生成HTML輸出,支持用戶交互和數據處理。 4.PHP可與數據庫交互,處理表單提交,執行服務器端任務。

PHP和網絡:探索其長期影響PHP和網絡:探索其長期影響Apr 16, 2025 am 12:17 AM

PHP在過去幾十年中塑造了網絡,並將繼續在Web開發中扮演重要角色。 1)PHP起源於1994年,因其易用性和與MySQL的無縫集成成為開發者首選。 2)其核心功能包括生成動態內容和與數據庫的集成,使得網站能夠實時更新和個性化展示。 3)PHP的廣泛應用和生態系統推動了其長期影響,但也面臨版本更新和安全性挑戰。 4)近年來的性能改進,如PHP7的發布,使其能與現代語言競爭。 5)未來,PHP需應對容器化、微服務等新挑戰,但其靈活性和活躍社區使其具備適應能力。

為什麼要使用PHP?解釋的優點和好處為什麼要使用PHP?解釋的優點和好處Apr 16, 2025 am 12:16 AM

PHP的核心優勢包括易於學習、強大的web開發支持、豐富的庫和框架、高性能和可擴展性、跨平台兼容性以及成本效益高。 1)易於學習和使用,適合初學者;2)與web服務器集成好,支持多種數據庫;3)擁有如Laravel等強大框架;4)通過優化可實現高性能;5)支持多種操作系統;6)開源,降低開發成本。

揭穿神話:PHP真的是一種死語嗎?揭穿神話:PHP真的是一種死語嗎?Apr 16, 2025 am 12:15 AM

PHP沒有死。 1)PHP社區積極解決性能和安全問題,PHP7.x提升了性能。 2)PHP適合現代Web開發,廣泛用於大型網站。 3)PHP易學且服務器表現出色,但類型系統不如靜態語言嚴格。 4)PHP在內容管理和電商領域仍重要,生態系統不斷進化。 5)通過OPcache和APC等優化性能,使用OOP和設計模式提升代碼質量。

PHP與Python辯論:哪個更好?PHP與Python辯論:哪個更好?Apr 16, 2025 am 12:03 AM

PHP和Python各有優劣,選擇取決於項目需求。 1)PHP適合Web開發,易學,社區資源豐富,但語法不夠現代,性能和安全性需注意。 2)Python適用於數據科學和機器學習,語法簡潔,易學,但執行速度和內存管理有瓶頸。

PHP的目的:構建動態網站PHP的目的:構建動態網站Apr 15, 2025 am 12:18 AM

PHP用於構建動態網站,其核心功能包括:1.生成動態內容,通過與數據庫對接實時生成網頁;2.處理用戶交互和表單提交,驗證輸入並響應操作;3.管理會話和用戶認證,提供個性化體驗;4.優化性能和遵循最佳實踐,提升網站效率和安全性。

PHP:處理數據庫和服務器端邏輯PHP:處理數據庫和服務器端邏輯Apr 15, 2025 am 12:15 AM

PHP在數據庫操作和服務器端邏輯處理中使用MySQLi和PDO擴展進行數據庫交互,並通過會話管理等功能處理服務器端邏輯。 1)使用MySQLi或PDO連接數據庫,執行SQL查詢。 2)通過會話管理等功能處理HTTP請求和用戶狀態。 3)使用事務確保數據庫操作的原子性。 4)防止SQL注入,使用異常處理和關閉連接來調試。 5)通過索引和緩存優化性能,編寫可讀性高的代碼並進行錯誤處理。

您如何防止PHP中的SQL注入? (準備的陳述,PDO)您如何防止PHP中的SQL注入? (準備的陳述,PDO)Apr 15, 2025 am 12:15 AM

在PHP中使用預處理語句和PDO可以有效防範SQL注入攻擊。 1)使用PDO連接數據庫並設置錯誤模式。 2)通過prepare方法創建預處理語句,使用佔位符和execute方法傳遞數據。 3)處理查詢結果並確保代碼的安全性和性能。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它們
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

mPDF

mPDF

mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

WebStorm Mac版

WebStorm Mac版

好用的JavaScript開發工具