2022-03-31

[php-src読書録]その2: echo

php-srcのコードリーディングした内容をコツコツ残すテスト。その2

opcodeの確認

OPcacheを使う方法

$ sapi/cli/php -dopcache.enable_cli=1 \
    -dopcache.opt_debug_level=0x10000 \
    -dzend_extension=$(pwd)/modules/opcache.so \
    hoge.php
$_main:
; (lines=2, args=0, vars=0, tmps=0)
; (before optimizer)
; /path/to/hoge.php:1-4
; return  [] RANGE[0..0]
0000 ECHO int(123)
0001 RETURN int(1)

phpdbgを使う方法

$ ./sapi/phpdbg/phpdbg '-p*' hoge.php                                                                                                                         PHP-8.1.4 ⬆ ✭ ✱ ◼
$_main:
; (lines=2, args=0, vars=0, tmps=0)
; /path/to/hoge.php:1-4
L0003 0000 ECHO int(123)
L0004 0001 RETURN int(1)
[Script ended normally]

ECHOのハンドラーを見てみる

今回デバッグするスクリプト

<?php

echo "hoge";

opcodeはこんな感じ

$_main:
     ; (lines=2, args=0, vars=0, tmps=0)
     ; (before optimizer)
     ; /path/to/echo.php:1-4
     ; return  [] RANGE[0..0]
0000 ECHO string("hoge")
0001 RETURN int(1)

ECHO opcodeを詳しく見てみる。

まずはgdbでデバッグ

$ gdb --args sapi/cli/php fuga.php 
(gdb) b execute_ex
(gdb) r

で、↓あたりまでnextする

if (UNEXPECTED((ret = ((opcode_handler_t)OPLINE->handler)(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) != 0)) {

https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_vm_execute.h#L56971:L56971

execute_data->opline->handler が各opcode(opline)のハンドラー

p execute_data->opline->handler
(gdb) $2 = (const void *) 0x10057ec90 <ZEND_ECHO_SPEC_CONST_HANDLER>

ZEND_ECHO_SPEC_CONST_HANDLERがhandlerになっている。

static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ECHO_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
	USE_OPLINE
	zval *z;

	SAVE_OPLINE();
	z = RT_CONSTANT(opline, opline->op1);

	if (Z_TYPE_P(z) == IS_STRING) {
		zend_string *str = Z_STR_P(z);

		if (ZSTR_LEN(str) != 0) {
			zend_write(ZSTR_VAL(str), ZSTR_LEN(str));
		}
	} else {
	    // string以外
	    // ...
	}

	ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}

USE_OPLINE, SAVE_OPLINEはoplineを定義したり、oplineをexecute_dataに待避するマクロ

# define USE_OPLINE const zend_op *opline = EX(opline);
# define SAVE_OPLINE() EX(opline) = opline

まずはRT_CONSTANTで定数のzvalを取得している。

# define RT_CONSTANT(opline, node) \
	((zval*)(((char*)(opline)) + (int32_t)(node).constant))

定数はoplineのポインタからnode.constant分移動した先にある。 このポインタの差分は pass_two() 内で設定している。

Z_TYPE_Pはzvalのtypeを取得している。

#define Z_TYPE_P(zval_p)	Z_TYPE(*(zval_p))
#define Z_TYPE(zval)		zval_get_type(&(zval))
static zend_always_inline zend_uchar zval_get_type(const zval* pz) {
	return pz->u1.v.type;
}

マクロを展開すると z->u1.v.type を見ていることになる。

(gdb) p z->u1.v.type
$6 = 6 '\006'

IS_STRING(6)と比較してSTRINGであればifの分岐の処理に入る。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_types.h#L529:L529

Z_STR_Pでzend_stringを取得する。

#define Z_STR(zval)	(zval).value.str
#define Z_STR_P(zval_p)	Z_STR(*(zval_p))
(gdb) p *z.value.str
$10 = {gc = {refcount = 2, u = {type_info = 342}}, h = 9223372043240078536, len = 4, val = "h"}

ZSTR_VAL, ZSTR_LENはそれぞれzend_stringのvalueとlengthを取り出す

#define ZSTR_VAL(zstr)  (zstr)->val
#define ZSTR_LEN(zstr)  (zstr)->len

取り出したvalueとlengthを引数にzend_write()を呼び出している。zend_write()は後ほど説明。


IS_STRINGじゃない場合はこんな感じなコードになっている。

zend_string *str = zval_get_string_func(z);

if (ZSTR_LEN(str) != 0) {
	zend_write(ZSTR_VAL(str), ZSTR_LEN(str));
} else if (IS_CONST == IS_CV && UNEXPECTED(Z_TYPE_P(z) == IS_UNDEF)) {
	ZVAL_UNDEFINED_OP1();
}
zend_string_release_ex(str, 0);

zval_get_string_func()__zval_get_string_func() を呼び出す
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_operators.c#L906-L939

typeごとに分岐してよしなに変換していて

echo true; // => 1
echo false; // => 何も出力されない
echo []; // => WARNINGが出てArrayが表示

となる理由がわかる。

falseは ZSTR_EMPTY_ALLOC() を返し、trueは ZSTR_CHAR('1') を返す。 ZSTR_EMPTY_ALLOC()zend_empty_string を返し、 zend_empty_string はここで定義されている
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_string.c#L102-L104

zend_write()

zend_writeは関数ポインタで utility_functions->write_function がセットされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend.c#L879:L879

utility_functions->write_functionphp_output_write がセットされる
https://github.com/php/php-src/blob/PHP-8.1.4/main/main.c#L2078:L2078

php_output_writephp_output_op() を呼び出す
https://github.com/php/php-src/blob/PHP-8.1.4/main/output.c#L261

php_output_op()sapi_module.ub_write() を呼び出す
https://github.com/php/php-src/blob/PHP-8.1.4/main/output.c#L1083:L1083

SAPIごとにub_write()の処理を変えられる。CLIであれは標準出力に出力するし、mod_phpであればレスポンスボディとして出力する。

CLIのub_write()はwrite()を呼び出して標準出力に出力しているのはここでわかる
https://github.com/php/php-src/blob/PHP-8.1.4/sapi/cli/php_cli.c#L264

mod_phpはこんな感じ
https://github.com/php/php-src/blob/PHP-8.1.4/sapi/apache2handler/sapi_apache2.c#L71-L84

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