通过上篇文章 聊聊php脚本执行流程 我们知道 php 脚本执行流程大致分为以下几步:
加载命令行参数和环境变量
信号处理
模块初始化
请求初始化
执行请求
今个就来看看是如何加载命令行参数和环境变量的,这块是在 save_ps_args() 方法中处理的。
extern char** environ;
// save the original argv[] location here
static int save_argc;
static char** save_argv;
// This holds the 'locally' allocated environ from the save_ps_args method. This is subsequently free'd at exit.
static char** frozen_environ, **new_environ;
// will point to argv area
static char *ps_buffer;
// space determined at run time
static size_t ps_buffer_size;
/*
* Call this method early, before any code has used the original argv passed in from main().
* If needed, this code will make deep copies of argv and environ and return these to the caller for further use.
* The original argv is then 'clobbered' to store the process title.
*/
char** save_ps_args(int argc, char** argv)
{
save_argc = argc;
save_argv = argv;
/*
* If we're going to overwrite the argv area, count the available space.
* Also move the environment to make additional room.
*/
char* end_of_area = NULL;
int non_contiguous_area = 0;
int i;
// check for contiguous argv strings
for (i = 0; (non_contiguous_area == 0) && (i < argc); i++) {
if (i != 0 && end_of_area + 1 != argv[i])
non_contiguous_area = 1;
end_of_area = argv[i] + strlen(argv[i]);
}
// check for contiguous environ strings following argv
for (i = 0; (non_contiguous_area == 0) && (environ[i] != NULL); i++) {
if (end_of_area + 1 != environ[i])
non_contiguous_area = 1;
end_of_area = environ[i] + strlen(environ[i]);
}
if (non_contiguous_area != 0)
goto clobber_error;
ps_buffer = argv[0];
ps_buffer_size = end_of_area - argv[0];
// move the environment out of the way
new_environ = (char **) malloc((i + 1) * sizeof(char *));
frozen_environ = (char **) malloc((i + 1) * sizeof(char *));
if (!new_environ || !frozen_environ)
goto clobber_error;
for (i = 0; environ[i] != NULL; i++) {
new_environ[i] = strdup(environ[i]);
if (!new_environ[i])
goto clobber_error;
}
new_environ[i] = NULL;
environ = new_environ;
memcpy((char *)frozen_environ, (char *)new_environ, sizeof(char *) * (i + 1));
...
char** new_argv;
int i;
new_argv = (char **) malloc((argc + 1) * sizeof(char *));
if (!new_argv)
goto clobber_error;
for (i = 0; i < argc; i++) {
new_argv[i] = strdup(argv[i]);
if (!new_argv[i]) {
free(new_argv);
goto clobber_error;
}
}
new_argv[argc] = NULL;
...
argv = new_argv;
...
/* make extra argv slots point at end_of_area (a NUL) */
int i;
for (i = 1; i < save_argc; i++)
save_argv[i] = ps_buffer + ps_buffer_size;
...
return argv;
...
}
方法首先通过循环遍历 argv 和 environ 两个数组来确定需要当前所用空间的边界,并记录在 end_of_area 中,最终减去初始存储位置 argv[0] 便得到所需空间大小,也保存在 ps_buffer_size 中。数组 argv 很显然是运行 php 脚本的参数,可通过 gdb 打印验证:
(gdb) p argv
$17 = (char **) 0x7fffffffe3f8
(gdb) p argv[0]
$18 = 0x7fffffffe670 "/usr/bin/php"
(gdb) p argv[1]
$19 = 0x7fffffffe67d "test.php"
(gdb) p argv[2]
$20 = 0x0
而 environ 通过前面的关键字 extern 可以知道,是引入的外部变量,实际上就是指向当前进程的环境变量表的指针,本身也是存储在栈空间的。可打印几个看下:
(gdb) p environ
$21 = (char **) 0x7fffffffe410
(gdb) p environ[0]
$22 = 0x7fffffffe686 "HOSTNAME=localhost.localdomain"
(gdb) p environ[1]
$23 = 0x7fffffffe6a5 "SHELL=/bin/bash"
(gdb) p environ[33]
$24 = 0x7fffffffefae "BASH_FUNC_module()=() { eval `/usr/bin/modulecmd bash $*`\n}"
(gdb) p environ[34]
$25 = 0x0
也可通过进程 pid 来进行验证:
[root@localhost main]# ps -ef | grep test.php
lucas 4333 20199 0 16:28 pts/2 00:00:00 /usr/bin/php test.php
root 9042 21330 0 16:38 pts/0 00:00:00 grep test.php
[root@localhost main]# cat /proc/4333/environ
HOSTNAME=localhost.localdomainSHELL=/bin/bashTERM=xterm...BASH_FUNC_module()=() { eval `/usr/bin/modulecmd bash $*`
在 /proc/4333/environ 文件中保存的就是当前进程的全部环境变量,只不过并不是以数组形式保存,而是拼接成字符串。通过大写字母来标识下一个 key,如果上一个 value 的最后一个字符也是大写字母,就在中间加一个 ‘_’ 符号以作区分。插一句,如何知道变量是存储在栈区还是堆区呢?这里还是通过进程 pid 查看:
[root@localhost cli]# cat /proc/4333/maps
00400000-00ec3000 r-xp 00000000 08:01 268775 /usr/local/php-7.1/bin/php
010c3000-01181000 rw-p 00ac3000 08:01 268775 /usr/local/php-7.1/bin/php
01181000-011c8000 rw-p 00000000 00:00 0 [heap]
...
7ffffffea000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
由上可以看到,堆区间范围是0x1181000-011c8000,栈区间范围是0x7ffffffea000-7ffffffff000。回到 save_ps_args() 方法中,为什么程序要计算这两个变量的存储位置呢,而且在计算 argv 后,紧接着就计算 environ,难不成这两个变量在内存中是连着存储的?事实上确实如此!我们看下 c 程序在内存中存储空间分布情况:
从低到高分别是:正文->初始化数据->未初始化数据->堆->栈->命令行参数和环境变量。 可以看到参数和环境变量是一起存储在进程地址空间顶部的,再通过 gdb 打印看下:
(gdb) p argv[0]
$38 = 0x7fffffffe670 "/usr/bin/php"
(gdb) p argv[1]
$39 = 0x7fffffffe67d "test.php"
(gdb) p argv[2]
$40 = 0x0
(gdb) p environ[0]
$41 = 0x7fffffffe686 "HOSTNAME=localhost.localdomain"
(gdb) p argv[3]
$42 = 0x7fffffffe686 "HOSTNAME=localhost.localdomain"
argv[0] 存储的是字符串 “/usr/bin/php”,末尾还存储一个 '\0',共13个字符,存储地址也就是 0x7fffffffe670~0x7fffffffe67c;
argv[1] 存储的是字符串 "test.php",算上结束符,共9个字符,存储地址应该就是 0x7fffffffe67d~0x7fffffffe685;
而地址 0x7fffffffe686 存储的是啥呢?正是数组 environ 的第一个元素。所以数组 argv 存储地址后面紧接着就是数组 environ 的首地址!这样即使参数只有两个 argv[0] 和 argv[1],并在 argv[2] 被赋值为空的前提下,打印 argv[3] 也能看到具体的值,而不是报错。
通过循环遍历数组 environ 后,i 值就保存了环境变量的个数,由于程序是初次运行,所以需要添加环境变量。但是通过进程地址空间分布我们知道,该部分是存储在地址空间的顶部,所以它不能再向高地址方向扩展;同时它低地址处存储的是已经分配的各栈帧,所以也不能向低地址方向扩展。只能调用 malloc 为新的环境表分配空间,接着将原来的环境表复制到新分配区,并按顺序复制每个环境变量,然后再将一个空指针放在末尾,最后将 environ 指向新指针表。同时如果原来的环境表位于栈顶上方,就还得将此表移至堆中。
注意在将原环境表元素复制到新分配空间时,这里调用的是 strdup() 方法。是因为新申请的 new_environ 是在堆区间,变量 environ 是在栈区,而在 strdup() 方法内部,会自动调用 malloc() 方法先分配与参数 environ[i] 相同大小的一段空间,然后将参数内容复制到该地址空间,最后返回该地址。而从 new_environ 复制到 forzen_environ 过程中,由于这俩都是在堆区,所以直接通过 memcpy() 即可。
截止此时,环境变量已经存储在堆区了,而命令行参数依然存储在栈空间上方,也需要 copy 一份到堆空间来。这里是声明了变量 new_argv,同样也是循环遍历 argv 数组,通过 strdup() 方法复制内容,最终再将 argv 指向新分配的参数地址 new_argv,并返回。这里可以通过打印调用 save_ps_args() 前后 argv 存储地址看到:
(gdb) p argv
$43 = (char **) 0x7fffffffe3f8
(gdb) n
1212 argv = save_ps_args(argc, argv);
(gdb)
1214 cli_sapi_module.additional_functions = additional_functions;
(gdb) p argv
$44 = (char **) 0x11a7e20
调用前存储地址为 0x7fffffffe3f8,恰好位于栈地址空间范围内,调用后存储地址为 0x11a7e20,也正好位于堆区中。
至此,脚本参数和环境变量是如何处理的就基本理清,欢迎交流。