PHP源码分析-命令行模式-cli模式-下echo实现的源码分析和执行追踪

一般的php程序员打断点的方式都是echo ,而echo最为我们最常用的语句对于其源码剖析也是值得我们去好好探究一番的,今天就以cli模式下的echo来具体追踪下php是如何实现"echo"的
php版本:PHP 7.3.18
系统:linux
首先看这段代码

<?php
echo 1234;

非常简短,就是在终端打印一个字符1234,执行结果也比较简单明了

[root@localhost ~]# php 20200820.php 
1234[root@localhost ~]#

没有包含任何特殊字符

先分析这种现象,打印在终端的字符一般都是通过文件描述符1(前边的文章有详细的介绍)来控制输出的,这段代码是把1234直接输出在了终端.再有PHP是通过c语言来实现的.到这里我们可以推测PHP在实现这块的时候调用了系统函数来写入到标准输出然后显示在了终端.

再来分析php听过了一整套的对外接口sapi其中在cli模式下为

echo内核代码 echo源码_c语言


我们找到其中的一个方法

sapi_cli_ub_write

/* {{{ sapi_module_struct cli_sapi_module
 */
static sapi_module_struct cli_sapi_module = {
	"cli",							/* name */
	"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
};

然后我们通过代码一路追踪这个函数

PHP_CLI_API ssize_t sapi_cli_single_write(const char *str, size_t str_length) /* {{{ */
{
	ssize_t ret;

	if (cli_shell_callbacks.cli_shell_write) {
		cli_shell_callbacks.cli_shell_write(str, str_length);
	}

#ifdef PHP_WRITE_STDOUT
	do {
		ret = write(STDOUT_FILENO, str, str_length);
	} while (ret <= 0 && errno == EAGAIN && sapi_cli_select(STDOUT_FILENO));
#else
	ret = fwrite(str, 1, MIN(str_length, 16384), stdout);
#endif
	return ret;
}
/* }}} */

static size_t sapi_cli_ub_write(const char *str, size_t str_length) /* {{{ */
{
	const char *ptr = str;
	size_t remaining = str_length;
	ssize_t ret;

	if (!str_length) {
		return 0;
	}

	if (cli_shell_callbacks.cli_shell_ub_write) {
		size_t ub_wrote;
		ub_wrote = cli_shell_callbacks.cli_shell_ub_write(str, str_length);
		if (ub_wrote != (size_t) -1) {
			return ub_wrote;
		}
	}

	while (remaining > 0)
	{
		ret = sapi_cli_single_write(ptr, remaining);
		if (ret < 0) {
#ifndef PHP_CLI_WIN32_NO_CONSOLE
			EG(exit_status) = 255;
			php_handle_aborted_connection();
#endif
			break;
		}
		ptr += ret;
		remaining -= ret;
	}

	return (ptr - str);
}
/* }}} */

从这里我们可以看到通过两次调用最后我们猜测调用了

write(STDOUT_FILENO, str, str_length)

这个方法来实现的echo的动作

如何验证我们的推测呢

首先通过gdb追踪到echo标记所对应的函数指针(依然不想使用句柄的概念)

echo内核代码 echo源码_PHP_02


然后查看ZEND_ECHO_SPEC_CONST_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 {
		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)) {
			GET_OP1_UNDEF_CV(z, BP_VAR_R);
		}
		zend_string_release_ex(str, 0);
	}

	ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}

后逐步追踪到

echo内核代码 echo源码_echo内核代码_03


而zend_writede 赋值在

int zend_startup(zend_utility_functions *utility_functions, char **extensions) /* {{{ */
{
	//省略好多
	zend_write = (zend_write_func_t) utility_functions->write_function;
	//省略好多
}
//调用zend_startup的地方在
int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint32_t num_additional_modules)
{
	//省略好多
	zuf.error_function = php_error_cb;
	zuf.printf_function = php_printf;
	zuf.write_function = php_output_wrapper;//注意这个函数
	zuf.fopen_function = php_fopen_wrapper_for_zend;
	zuf.message_handler = php_message_handler_for_zend;
	zuf.get_configuration_directive = php_get_configuration_directive_for_zend;
	zuf.ticks_function = php_run_ticks;
	zuf.on_timeout = php_on_timeout;
	zuf.stream_open_function = php_stream_open_for_zend;
	zuf.printf_to_smart_string_function = php_printf_to_smart_string;
	zuf.printf_to_smart_str_function = php_printf_to_smart_str;
	zuf.getenv_function = sapi_getenv;
	zuf.resolve_path_function = php_resolve_path_for_zend;
	zend_startup(&zuf, NULL);

	//省略好多
}

而php_module_startup顾名思义在php启动之初就会根据当前运行环境被初始化
再来看php_output_wrapper
下边这段代码的调用过程需要结合调试工具来看

static size_t php_output_wrapper(const char *str, size_t str_length)
{
	return php_output_write(str, str_length);
}

PHPAPI size_t php_output_write(const char *str, size_t len)
{
	if (OG(flags) & PHP_OUTPUT_ACTIVATED) {
		php_output_op(PHP_OUTPUT_HANDLER_WRITE, str, len);
		return len;
	}
	if (OG(flags) & PHP_OUTPUT_DISABLED) {
		return 0;
	}
	return php_output_direct(str, len);
}

static inline void php_output_op(int op, const char *str, size_t len)
{
		//省略好多
		if (!(OG(flags) & PHP_OUTPUT_DISABLED)) {
#if PHP_OUTPUT_DEBUG
			fprintf(stderr, "::: sapi_write('%s', %zu)\n", context.out.data, context.out.used);
#endif
		//就是这里了
			sapi_module.ub_write(context.out.data, context.out.used);

			if (OG(flags) & PHP_OUTPUT_IMPLICITFLUSH) {
				sapi_flush();
			}

			OG(flags) |= PHP_OUTPUT_SENT;
		}
	}
	php_output_context_dtor(&context);
	//省略好多
}

代码追踪过程

echo内核代码 echo源码_echo内核代码_04


echo内核代码 echo源码_c语言_05


echo内核代码 echo源码_PHP_06


正如我们所预料的到这里了

echo内核代码 echo源码_php_07


然后就到了之前的地方

echo内核代码 echo源码_PHP_08


好了这大概就是cli模式下的echo的最后实现方法.