分享
 
 
 

Apache中的进程剖析(4)

王朝system·作者佚名  2006-05-27
窄屏简体版  字體: |||超大  

6.2.2 Window系统中进程创建

6.2.2.1进程创建概述

Window系统中创建进程毫无疑问,肯定是使用CreateProcess函数,或者是Unicode版本的CreateProcessW,或者是ASCII版本的CreateProcessA。不过与Unix中创建进程不单是调用fork一样,Window中创建进程也不仅是调用CreateProcess这么简单而已。事实上Window中对进程的创建要比Unix中还要复杂的多,一方面是Window操作系统的版本比较多,为了考虑移植性,必须考虑到多个操作系统;另一方面,Window中对。。。。

在大部分的Window相关的程序中我们都会看到下面几个预处理宏,这里有必要解释一下。

_WIN32_WCE

该宏意味着当前的应用程序运行在Window CE平台上;因此宏内部的代码仅仅适用于Window CE平台。

APR_HAS_UNICODE_FS

该宏意味着当前的文件系统支持Unicode。对于Window系统而言,则主要只Window NT以上的版本;

APR_HAS_ANSI_FS

该宏意味着当前的文件系统是ASCII编码,对于Window系统而言,则主要指Window 9X系列,包括Window 95,Window 98以及Window ME。

Window中进程的创建过程可以用下面的伪码描述整体概况:

apr_proc_create

{

if (attr->errchk)

对attr做有效性检查,让错误尽量发生在parent process中,而不是留给child process; ----(1)

fork子进程;

{ /* 在子进程中 */

清理一些不必要的从父进程继承下来的描述符等,为exec提供一个“干净的”环境;------(2)

关闭attr->parent_in、parent_out和parent_err,

并分别重定向attr->child_in、child_out和child_err为STDIN_FILENO、

STDOUT_FILENO和STDERR_FILENO; -----(3)

判断attr->cmdtype,选择执行exec函数; ------(4)

}

/* 在父进程中 */

关闭attr->child_in、child_out和child_err;

}

下面的部分我们将针对每一部分详细展开描述。

6.2.2.2创建过程

new->in = attr->parent_in;

new->out = attr->parent_out;

new->err = attr->parent_err;

if (attr->detached) {

if (apr_os_level >= APR_WIN_NT) {

dwCreationFlags |= DETACHED_PROCESS;

}

}

DETACHED_PROCESS标志是一个与控制台相关的创建标志。默认情况下,如果应用程序创建一个控制台应用程序,那么该进程将继承共享父进程的控制台,并且该子进程的所有的输出信息都将在父进程的控制台中显示,而且交互也只能通过父进程的控制台显示。不过这并不能保证一定会成功。

有的时候并不希望子进程继承父进程的控制台,而是拥有自己的控制台。此时有几种途径可以实现这种效果:

1)、A GUI or console process can use the CreateProcess function with CREATE_NEW_CONSOLE to create a console process with a new console. (By default, a console process inherits its parent's console, and there is no guarantee that input is received by the process for which it was intended.)

2)、A graphical user interface (GUI) or console process that is not currently attached to a console can use the AllocConsole function to create a new console. (GUI processes are not attached to a console when they are created. Console processes are not attached to a console if they are created using CreateProcess with DETACHED_PROCESS.)

不过只有Window NT以上的版本才能支持新的DETACHED_PROCESS标志,而Win9X系列的操作系统则无能为力。

if (progname[0] == '\"') {

progname = apr_pstrndup(pool, progname + 1, strlen(progname) - 2);

}

Window中不允许传入的运行程序名称中包含双引号””,因此如果发现程序名称被””包含,则首先必须将””剔除,才能继续往下操作。

if (attr->cmdtype == APR_PROGRAM || attr->cmdtype == APR_PROGRAM_ENV) {

char *fullpath = NULL;

if ((rv = apr_filepath_merge(&fullpath, attr->currdir, progname,

APR_FILEPATH_NATIVE, pool)) != APR_SUCCESS) {

if (attr->errfn) {

attr->errfn(pool, rv,

apr_pstrcat(pool, "filepath_merge failed.",

" currdir: ", attr->currdir,

" progname: ", progname, NULL));

}

return rv;

}

progname = fullpath;

}

else {

char *fullpath = NULL;

if ((rv = apr_filepath_merge(&fullpath, "", progname,

APR_FILEPATH_NATIVE, pool)) == APR_SUCCESS) {

progname = fullpath;

}

}

在前面的部分我们曾经描述过五种应用程序类型的实际含义。对于一些应用类型,用户只需要指定应用程序的名称,而不需要指定完整的路径名称就可以执行,比如APR_SHELLCMD、APR_SHELLCMD_ENV和APR_PROGRAM_PATH。但是不管哪一种应用程序类型,最终它们的执行都是CreateProcess函数,而该函数需要完整的程序路径作为参数,因此函数内部必须能够根据传入的程序名称和程序类型确定出完整的。不同的程序类型,绝对路径确定的方法可以用下表描述:

程序类型cmd_type

确定绝对路径的方法

APR_PROGRAM

使用启动进程的当前路径作为路径

APR_PROGRAM_ENV

使用启动进程的当前路径作为路径

APR_PROGRAM_PATH

查找环境变量”PATH”指定的路径下是否存在该程序,如果存在,使用该路径作为绝对路径

APR_SHELLCMD

使用”COMSPEC”指定的路径作为绝对路径

APR_SHELLCMD_ENV

使用”COMSPEC”指定的路径作为绝对路径

从上表中可以看出,对于APR_PROGRAM和APR_PROGRAM_ENV类型的程序,它们的绝对路径实际上是执行进程的当前路径currdir和程序名称的组合,即currdir+progname。而对于其余三种类型,暂时只是简单的处理,在后面的部分它们将被继续处理。

if (has_space(progname)) {

argv0 = apr_pstrcat(pool, "\"", progname, "\"", NULL); u

}

else {

argv0 = progname;

}

/* Handle the args, seperate from argv0 */

cmdline = "";

for (i = 1; args && args[i]; ++i) {

if (has_space(args[i])) {

cmdline = apr_pstrcat(pool, cmdline, " \"", args[i], "\"", NULL);

} v

else {

cmdline = apr_pstrcat(pool, cmdline, " ", args[i], NULL);

}

}

对于CreateProcess函数,Window规定如果传入的启动程序名称和参数中包含空格,那么这些名称和参数在传入给CreateProcess函数之前必须用双引号””进行包含,比如c:\program files\sub dir\program name,如果不用””包含,则Window可能会产生歧异,因为解释有多种(黑体部分为可执行程序名称,而细体部分为参数):

c:\program.exe files\sub dir\program name

c:\program files\sub.exe dir\program name

c:\program files\sub dir\program.exe name

c:\program files\sub dir\program name.exe

在u中,函数首先判断程序名称中是否包含空格,如果是,则将各部分用””包含起来。同样在v中,对于传入的执行程序需要的参数列表args,应用程序也必须检查各个参数中是否包含空格,比如如果某个参数为”hello world”,那么直接传入,可能会被程序误解为两个不同的参数”hello”和”world”,因此,对于这些包含空格的参数也必须使用””包含起来。这些处理后的参数最终保存在cmdline中。

if (attr->cmdtype == APR_SHELLCMD || attr->cmdtype == APR_SHELLCMD_ENV) {

char *shellcmd = getenv("COMSPEC");

if (!shellcmd) {

if (attr->errfn) {

attr->errfn(pool, APR_EINVAL, "COMSPEC envar is not set");

}

return APR_EINVAL;

}

if (shellcmd[0] == '"') {

progname = apr_pstrndup(pool, shellcmd + 1, strlen(shellcmd) - 2);

}

else {

progname = shellcmd;

if (has_space(shellcmd)) {

shellcmd = apr_pstrcat(pool, "\"", shellcmd, "\"", NULL);

}

}

/* Command.com does not support a quoted command, while cmd.exe demands one.

*/

i = strlen(progname);

if (i >= 11 && strcasecmp(progname + i - 11, "command.com") == 0) {

cmdline = apr_pstrcat(pool, shellcmd, " /C ", argv0, cmdline, NULL);

}

else {

cmdline = apr_pstrcat(pool, shellcmd, " /C \"", argv0, cmdline, "\"", NULL);

}

}

在前面我们描述过,对于APR_SHELLCMD和APR_SHELLCMD_ENV类型的应用程序,它的绝对路径由环境变量COMSPEC指定。如果COMSPEC环境变量不存在,则执行将失败。如果存在,则同样将其中的空格字符串用””包围起来。

shell应用程序的程序名称或者是cmd.exe(Window NT以上版本)或者是command.com(Window 9X系列)。command.com程序不支持命令中出现双引号,而cmd.exe则必须用双引号将命令包括起来。

i = strlen(progname);

if (i >= 4 && (strcasecmp(progname + i - 4, ".bat") == 0

|| strcasecmp(progname + i - 4, ".cmd") == 0))

{

char *shellcmd = getenv("COMSPEC");

if (!shellcmd) {

if (attr->errfn) {

attr->errfn(pool, APR_EINVAL, "COMSPEC envar is not set");

}

return APR_EINVAL;

}

if (shellcmd[0] == '"') {

progname = apr_pstrndup(pool, shellcmd + 1, strlen(shellcmd) - 2);

}

else {

progname = shellcmd;

if (has_space(shellcmd)) {

shellcmd = apr_pstrcat(pool, "\"", shellcmd, "\"", NULL);

}

}

i = strlen(progname);

if (i >= 11 && strcasecmp(progname + i - 11, "command.com") == 0) {

cmdline = apr_pstrcat(pool, shellcmd, " /C ", argv0, cmdline, NULL);

}

else {

cmdline = apr_caret_escape_args(pool, cmdline);

if (*argv0 != '"') {

cmdline = apr_pstrcat(pool, shellcmd, " /C \"\"", argv0, "\"", cmdline, "\"", NULL);

}

else {

cmdline = apr_pstrcat(pool, shellcmd, " /C \"", argv0, cmdline, "\"", NULL);

}

}

}

如果应用程序是批处理程序(.bat)或者命令程序(.com),则处理过程是一样的。

为了创建一个新的进程,至少必须具备下面的几个要素:

1、程序所在的所在的绝对路径

2、进程所需要的环境变量

在下面的函数中代码将会因为操作系统编码的不同而导致差异。Window 9X系列的操作系统是基于ASCII编码,而Window NT以上版本则是基于Unicode编码,这由宏APR_HAS_UNICODE_FS和APR_HAS_ANSI_FS区分:

#if APR_HAS_UNICODE_FS

……

#endif

#if APR_HAS_ANSI_FS

……

#endif

我们首先讨论简单的Window 9X操作系统中的细节。

if (!env || attr->cmdtype == APR_PROGRAM_ENV ||

attr->cmdtype == APR_SHELLCMD_ENV) {

pEnvBlock = NULL;

}

Window在调用CreateProcess的时候需要传递一个环境变量块,如果为NULL,则新进程将使用调用进程的环境变量。

An environment block consists of a null-terminated block of null-terminated strings. Each string is in the form:

name=value

Because the equal sign is used as a separator, it must not be used in the name of an environment variable.

An environment block can contain either Unicode or ANSI characters. If the environment block pointed to by lpEnvironment contains Unicode characters, be sure that dwCreationFlags includes CREATE_UNICODE_ENVIRONMENT.

Note that an ANSI environment block is terminated by two zero bytes: one for the last string, one more to terminate the block. A Unicode environment block is terminated by four zero bytes: two for the last string, two more to terminate the block.

对于APR_PROGRAM_ENV和APR_SHELLCMD_ENV程序类型,它们使用父进程的环境变量。因此传递给CreateProcess的环境变量块pEnvBlock为NULL。

else {

apr_size_t iEnvBlockLen;

i = 0;

iEnvBlockLen = 1;

while (env[i]) {

iEnvBlockLen += strlen(env[i]) + 1;

i++;

}

if (!i)

++iEnvBlockLen;

{

char *pNext;

pEnvBlock = (char *)apr_palloc(pool, iEnvBlockLen);

i = 0;

pNext = pEnvBlock;

while (env[i]) {

strcpy(pNext, env[i]);

pNext = strchr(pNext, '\0') + 1;

i++;

}

if (!i)

*(pNext++) = '\0';

*pNext = '\0';

}

}

由于CreateProcess所需要的环境变量块实际上是一个字符串,而传入的参数env则是字符串数组,因此必须完成转换。转换的过程对于ASCII操作系统而言,无非是执行strcpy进行拷贝而已。转换前后示意如下所示:

new->invoked = cmdline;

{

STARTUPINFOA si;

memset(&si, 0, sizeof(si));

si.cb = sizeof(si);

if (attr->detached) {

si.dwFlags |= STARTF_USESHOWWINDOW; u

si.wShowWindow = SW_HIDE;

}

if ((attr->child_in && attr->child_in->filehand)

|| (attr->child_out && attr->child_out->filehand)

|| (attr->child_err && attr->child_err->filehand))

{

si.dwFlags |= STARTF_USESTDHANDLES;

si.hStdInput = (attr->child_in)

? attr->child_in->filehand v

: INVALID_HANDLE_VALUE;

si.hStdOutput = (attr->child_out)

? attr->child_out->filehand

: INVALID_HANDLE_VALUE;

si.hStdError = (attr->child_err)

? attr->child_err->filehand

: INVALID_HANDLE_VALUE;

}

rv = CreateProcessA(progname, cmdline, /* Command line */

NULL, NULL, /* Proc & thread security attributes */

TRUE, w /* Inherit handles */

dwCreationFlags, /* Creation flags */

pEnvBlock, /* Environment block */

attr->currdir, /* Current directory name */

&si, &pi);

}

if (!rv)

return apr_get_os_error();

new->hproc = pi.hProcess;

new->pid = pi.dwProcessId;

if (attr->child_in) {

apr_file_close(attr->child_in); x

}

if (attr->child_out) {

apr_file_close(attr->child_out);

}

if (attr->child_err) {

apr_file_close(attr->child_err);

}

CloseHandle(pi.hThread);

return APR_SUCCESS;

与Unix下的操作相似,创建子进程的最后一个任务就是创建父进程和子进程之间的通信管道。不过由于Unix和Window中产生进程以及进程执行的机制不同,而导致通信管道的建立也存在差异。

我们回忆一下,在Unix中,起始的时候主进程中连同管道描述符,一共拥有九个描述符,而子进程从父进程fork之后将继承所有九个描述符。由于fork之后,子进程和父进程同时可以执行,因此对于它们来说,可以在各自的代码中进行重定向以及关闭多余的描述符,流程可以用下图描述:

但是对于Window而言,则并没有fork这样的机制。Window中调用CreateProcess创建进程之后,尽管进程也可以运行,但是与Unix相比,父进程并无法在自己的代码中过多的进行控制,如果想控制,只能由子进程本身去完成。在Unix下,管道的建立由父进程和子进程协作创建,而在Window中,更多的则必须由父进程完成。对于Window下的父进程,创建管道包括下面的三个步骤:

1)、默认情况下,子进程将继承父进程中所有的句柄。不过有些句柄对于子进程并不需要,比如parent_XXX,只用于父进程,因此它就没有必要被子进程继承。因此父进程在创建管道的时候就必须指定所有parent_xxx不被子进程继承。不需要继承的描述符在函数apr_create_nt_pipe中由函数apr_file_inherit_unset指定,比如父进程在创建管道child_in-parent_in的时候同时指定parent_in不被子进程进程,代码如下:

if (in) {

stat = apr_create_nt_pipe(&attr->child_in, &attr->parent_in, in,

attr->pool);

if (stat == APR_SUCCESS)

stat = apr_file_inherit_unset(attr->parent_in);

}

因此当子进程创建后,实际的描述符如下图的(b)所示,其内部只有六个描述符。

2)、设置父进程的继承标志。

子进程默认情况下并不会继承父进程中的句柄。为了允许继承,父进程必须设置进程标志。通过两种方法可以设置继承标志,或者在创建句柄的时候设置SECURITY_ATTRIBUTES参数中的bInheritHandle成员为TRUE,或者在使用CreateProcess创建子进程的时候设置函数的bInheritHandles参数为TRUE,如w所示。

3)、子进程中描述符重定向

正如前面所分析的,子进程中不允许直接从控制台接受输入或者进行输出,而必须通过父进程来完成这些。因此子进程的标准输入,标准输出,以及标准错误都必须重定向到child_xxx中。在CreateProcess创建过程中,通过设置STARUUPINFO参数可以实现子进程的重定向。不过子进程重定向必须具备下面的几个条件:

▉ STARTUPINFOA结构中的dwFlags必须设置STARTF_USESTDHANDLES标志位。该标志位的设立允许进程将它的标准输入、标准输出以及标准错误设备用该结构中的指定的输入hStdInput、输出hStdOutput以及错误设备hStdError替换,从而实现重定向。如果该标志位不设定,hStdInput,hStdOutput和hStdError将被忽略。

▉ CreateProcess函数中的fInheritHandles参数必须设置为TRUE。

重定向代码如v所示。

4)、父进程关闭多余的child_xxx描述符。

最终在创建管道的过程中,父进程和子进程中的描述符的变化如下图所示。

上面的描述只是针对Window 9X的ASCII版本的操作系统,对于Unicode版本的操作系统,比如window NT,Window 2003等处理则存在一些差异。ASCII和Unicode版本对进程创建的差异包括在下面两个方面:

1)、环境变量块的差异

2)、进程路径的差异

3)、进程在安全性方面的考虑。

首先我们看第一个差异:环境变量块的差异。

如前所述,使用CreateProcess的过程中可能需要传递环境变量。环境变量块中既允许包含ANSI字符,又允许包含Unicode字符。对于Unicode的操作系统,环境变量块总是Unicode编码的,但是如果要CreateProcess将传递的环境变量当Unicode编码处理,则CreateProcess的dwCreationFlags标志必须包含CREATE_UNICODE_ENVIRONMENT,否则即使环境变量块是Unicode编码,也会被视为ANSI处理。

对于ANSI编码环境变量块,它总是以两个’\0’作为结束符:一个作为最后一个字符串的结束符,另一个作为整个环境变量块的结束符。而由于Unicode是双字节编码,因此Unicode编码的环境变量块则以四个’\0’做为结束标志:两个’\0’作为最后一个字符的结束符,另外两个则作为整个环境变量块的结束符。

与ANSI中使用char定义一个字符类似,Unicode版本则用apr_wchar_t定义一个双字节字符,它的原始定义为apr_uint16_t。Unicode存在两种编码方式:UTF或者UCS。UTF-8通常使用进行网络传输,比如网页的传输,URL的传输,路径的传输。主要的原因就是对于本地编码为Unicode的系统,由于网络传输以字节作为单位,因此如果传输中某个字节为0的话,将会干扰正常传输。这在Window中页不例外,Window中的环境变量块和路径名称都是基于UTF-8编码,因此必须将它们转换为本地的Unicode编码。转换由apr_conv_utf8_to_ucs2函数完成。反之,当使用路径的时候,则必须使用apr_conv_ucs2_to_utf8将本地Unicode编码转换为UTF-8。

环境变量块的处理如下所示:

#if APR_HAS_UNICODE_FS

IF_WIN_OS_IS_UNICODE

{

apr_wchar_t *pNext;

pEnvBlock = (char *)apr_palloc(pool, iEnvBlockLen * 2);

dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT;

i = 0;

pNext = (apr_wchar_t*)pEnvBlock;

while (env[i]) {

apr_size_t in = strlen(env[i]) + 1;

if ((rv = apr_conv_utf8_to_ucs2(env[i], &in,

pNext, &iEnvBlockLen))

!= APR_SUCCESS) {

if (attr->errfn) {

……

}

return rv;

}

pNext = wcschr(pNext, L'\0') + 1;

i++;

}

if (!i)

*(pNext++) = L'\0';

*pNext = L'\0';

}

#endif /* APR_HAS_UNICODE_FS */

路径的处理过程与之类似,此处不在赘述。

我们现在来看ANSI和Unicode版本在进程安全性方面的差异。Unicode版本的进程创建代码如下所示:

if (attr->user_token) {

si.lpDesktop = L"Winsta0\\Default"; u

if (!ImpersonateLoggedOnUser(attr->user_token)) {

rv = apr_get_os_error();

CloseHandle(attr->user_token); v

attr->user_token = NULL;

return rv;

}

rv = CreateProcessAsUserW(attr->user_token,

wprg, wcmd,

attr->sa, w

NULL,

TRUE,

dwCreationFlags,

pEnvBlock,

wcwd,

&si, &pi);

RevertToSelf();

}

else {

rv = CreateProcessW(wprg, wcmd, /* Executable & Command line */

NULL, NULL, /* Proc & thread security attributes */

TRUE, /* Inherit handles */ x

dwCreationFlags, /* Creation flags */

pEnvBlock, /* Environment block */

wcwd, /* Current directory name */

&si, &pi);

}

如果指定了模拟用户,即user_token不为NULL,那么进程将进行用户模拟。模拟又分两种,进程内使用线程模拟和创建新进程模拟。这两种模拟方法在Apache中都有使用。只不过在该函数中是创建新进程,因此函数使用后一种方法。在进程内的通常使用 ImpersonateLoggedOnUser函数模拟用户时,这个线程就是该模拟令牌代表的用户的身份,处理完成后使用 RevertToSelf函数恢复自己的身份。如果该用户能够模拟成功,则立即使用该用户身份创建一个新的进程。这由函数 CreateProcessAsUser完成,与CreateProcess相比, CreateProcessAsUser 函数多一个主要令牌的参数,这样启动的新进程就不是父进程的身份,而是user_token令牌代表的登录用户。

当然,如果不需要进行用户模拟,则只要在调用CreateProcess的时候将进程的安全属性设置为NULL就可以了。

关于作者

张中庆,目前主要的研究方向是嵌入式浏览器,移动中间件以及大规模服务器设计。目前正在进行Apache的源代码分析,计划出版《Apache源代码全景分析》上下册。Apache系列文章为本书的草案部分,对Apache感兴趣的朋友可以通过flydish1234 at sina.com.cn与之联系!

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有