分享
 
 
 

Unix编程/应用问答中文版---2.堆栈相关问题

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

作者:不祥 [文章出自: www.fanqiang.com]

2. 堆栈相关问题

2.1 如何理解pstack的输出信息

2.2

2.3 Solaris中如何获取一个C程序的调用栈回溯

2.4 如何编程获取栈底地址

2.5 如何得到一个运行中进程的内存映像

2.6 调试器如何工作的

2.7 x86/Linux上如何处理SIGFPE信号

--------------------------------------------------------------------------

2. 堆栈相关问题

2.1 如何理解pstack的输出信息

Q: 080603a7 main (1, 80479b8, 80479c0) + d53

结尾的d53是什么

A: Roger A. Faulkner <raf@sunraf.Sun.COM>

在代码段绝对地址0x080603a7处,main()调用了一个函数,0x080603a7正是

main + 0xd53,换句话说,从main()函数开始的0xd53偏移处。

2.3 Solaris中如何获取一个C程序的调用栈回溯

Q: 我想在Solaris 2.6极其后续版本上获取一个C程序的调用栈回溯,类似如下输出

(10) 0x00045e08 integ + 0x408 [./two_brn.e]

(11) 0x0006468c trajcem + 0x128 [./two_brn.e]

(12) 0x00055490 fly_traj + 0xf58 [./two_brn.e]

(13) 0x0004052c top_level + 0x14 [./two_brn.e]

(14) 0x000567e4 _start + 0x34 [./two_brn.e]

这样我就可以知道当程序崩溃、死锁的时候代码执行到了何处。在HP-UX和IRIX上

可以利用U_STACK_TRACE()和trace_back_stack_and_print(),Solaris上呢?

Q: 有没有办法显示当前堆栈中的数据(GNU/Linux系统)?我希望自己的异常处理程序

在进程结束前dump整个栈区(stack),以便观察到栈顶是什么函数。对于调试意想

不到的运行时错误而言,这很重要。

A: Bjorn Reese <breese@mail1.stofanet.dk>

用/usr/proc/bin/pstack [-F] <pid ...>

参看这个例子代码,http://home1.stofanet.dk/breese/debug/debug.tar.gz

Q: is there a way to access call stack information at run time from within

a program? i've been maintaining my own crude stack using __FUNCTION__

and linked lists but can't help but think there's gotta be a better

way...

A: Nate Eldredge <neldredge@hmc.edu>

这依赖于你的系统,如果使用glibc 2.1或更新版本,可以使用backtrace()函数,

参看<execinfo.h>,其他系统可能有不同的技术支持。

注意,你所使用的办法可能是唯一能够保证跨平台使用的

A: Andrew Gabriel <andrew@cucumber.demon.co.uk> Consultant Software Engineer

下面是一个backtrace()的应用举例,如果你使用Solaris 2.4及其后续版本,那

么这个例子可以很好的工作。很可能无法工作在64-bit模式下,我没有尝试过,

好像Solaris 7已经提供了一个类似的演示程序。还可以增加某些功能,我没有时

间了。

/*

* Produce a stack trace for Solaris systems.

*

* Copyright (C) 1995-1998 Andrew Gabriel <andrew@cucumber.demon.co.uk>

* Parts derived from Usenet postings of Bart Smaalders and Casper Dik.

*

*/

/* ......................................................................... */

#include <setjmp.h>

#include <sys/types.h>

#include <sys/reg.h>

#include <sys/frame.h>

#include <dlfcn.h>

#include <errno.h>

#include <unistd.h>

#include <stdio.h>

#if defined(sparc) || defined(__sparc)

#define FLUSHWIN() asm("ta 3");

#define FRAME_PTR_INDEX 1

#define SKIP_FRAMES 0

#endif

#if defined(i386) || defined(__i386)

#define FLUSHWIN()

#define FRAME_PTR_INDEX 3

#define SKIP_FRAMES 1

#endif

#if defined(ppc) || defined(__ppc)

#define FLUSHWIN()

#define FRAME_PTR_INDEX 0

#define SKIP_FRAMES 2

#endif

/* ......................................................................... */

static void print_address ( void * pc )

{

Dl_info info;

if ( dladdr( pc, &info ) == 0 )

{

/* not found */

fprintf( stderr, "*** %s:0x%x\\n", "??", ( unsigned int )pc );

}

else

{

/* found */

fprintf( stderr, "*** %s:%s+0x%x\\n", info.dli_fname, info.dli_sname,

( unsigned int )pc - ( unsigned int )info.dli_saddr );

}

return;

} /* end of print_address */

/* ......................................................................... */

static int validaddr ( void * addr )

{

static long pagemask = -1;

char c;

if ( pagemask == -1 )

{

pagemask = ~( sysconf( _SC_PAGESIZE ) - 1 );

}

addr = ( void * )( ( long )addr & pagemask );

if ( mincore( ( char * )addr, 1, &c ) == -1 && errno == ENOMEM )

{

return 0; /* invalid */

}

else

{

return 1; /* valid */

}

} /* end of validaddr */

/* ......................................................................... */

/*

* this function walks up call stack, calling print_addess

* once for each stack frame, passing the pc as the argument.

*/

static void print_stack ( void )

{

struct frame * sp;

jmp_buf env;

int i;

int * iptr;

FLUSHWIN();

setjmp( env );

iptr = ( int * )env;

sp = ( struct frame * )iptr[ FRAME_PTR_INDEX ];

for ( i = 0; i < SKIP_FRAMES && sp; i++ )

{

if ( !validaddr( sp ) || !validaddr( &sp->fr_savpc ) )

{

fprintf( stderr, "***[stack pointer corrupt]\\n" );

return;

}

sp = ( struct frame * )sp->fr_savfp;

}

i = 100; /* looping check */

while ( validaddr( sp ) && validaddr( &sp->fr_savpc ) && sp->fr_savpc && --i

)

{

print_address( ( void * )sp->fr_savpc );

sp = ( struct frame * )sp->fr_savfp;

}

} /* end of print_stack */

/* ......................................................................... */

void backtrace( void )

{

fprintf( stderr, "***backtrace...\\n" );

print_stack();

fprintf( stderr, "***backtrace ends\\n" );

}

/* ......................................................................... */

2.4 如何编程获取栈底地址

Q: 虽然很多操作系统的用户进程栈底地址固定,但是我需要写一个可广泛移植C程序

获取这个栈底地址。

A: tt <warning3@nsfocus.com> 2001-06-02 19:40

假设堆栈(stack)向低地址方向增长,则所谓栈底指堆栈(stack)最高地址

x86/Linux 栈底是0xc0000000( 栈底往低地址的4个字节总是零 )

SPARC/Solaris 7/8 栈底是0xffbf0000( 栈底往低地址的4个字节总是零 )

SPARC/Solaris 2.6 栈底是0xf0000000( 栈底往低地址的4个字节总是零 )

x86/FreeBSD 栈底是0xbfc00000( 栈底往低地址的4个字节总是零 )

x86/NetBSD 1.5 栈底是0xbfbfe000

x86/OpenBSD 2.8 栈底是0xdfbfe000

D: jonah

对于NetBSD 1.5,栈底是0xbfc00000。根据源码,最高用户地址是0xbfbfe000,因为

最后4MB(2^22)的最后两页(0x2000字节,一页4096字节)保留用做U区,但是目前不再

使用这块内存。因此,0xbfbfe000才是真正的栈底。

tt在OpenBSD 2.8上测试结果,栈底是0xdfbfe000,注意和NetBSD 1.5相差很大。

A: tt <warning3@nsfocus.com>

--------------------------------------------------------------------------

/*

* gcc -Wall -O3 -o gstack gstack.c

*

* A simple example to get the current stack bottom address

* warning3 <warning3@nsfocus.com>

* 2001-06-01

*

* Modified by scz <scz@nsfocus.com>

* 2001-06-02

*/

#include <stdio.h>

#include <stdlib.h>

#include <signal.h>

#include <unistd.h>

#include <setjmp.h>

typedef void Sigfunc ( int ); /* for signal handlers */

Sigfunc * signal ( int signo, Sigfunc * func );

static Sigfunc * Signal ( int signo, Sigfunc * func );

static char * get_stack_bottom ( void );

static void segfault ( int signo );

static sigjmp_buf jmpbuf;

static volatile sig_atomic_t canjump = 0;

static Sigfunc *seg_handler;

static Sigfunc *bus_handler; /* for xxxBSD */

Sigfunc * signal ( int signo, Sigfunc * func )

{

struct sigaction act, oact;

act.sa_handler = func;

sigemptyset( &act.sa_mask );

act.sa_flags = 0;

if ( sigaction( signo, &act, &oact ) < 0 )

{

return( SIG_ERR );

}

return( oact.sa_handler );

} /* end of signal */

static Sigfunc * Signal ( int signo, Sigfunc * func ) /* for our signal() funct

ion */

{

Sigfunc * sigfunc;

if ( ( sigfunc = signal( signo, func ) ) == SIG_ERR )

{

exit( EXIT_FAILURE );

}

return( sigfunc );

} /* end of Signal */

static char * get_stack_bottom ( void )

{

volatile char *c; /* for autovar, must be volatile */

seg_handler = Signal( SIGSEGV, segfault );

bus_handler = Signal( SIGBUS, segfault );

c = ( char * )&c;

if ( sigsetjmp( jmpbuf, 1 ) != 0 )

{

Signal( SIGSEGV, seg_handler );

Signal( SIGBUS, bus_handler );

return( ( char * )c );

}

canjump = 1; /* now sigsetjump() is OK */

while ( 1 )

{

*c = *c;

c++;

}

return( NULL );

} /* end of get_stack_bottom */

static void segfault ( int signo )

{

if ( canjump == 0 )

{

return; /* unexpected signal, ignore */

}

canjump = 0;

siglongjmp( jmpbuf, signo ); /* jump back to main, don't return */

} /* end of segfault */

int main ( int argc, char * argv[] )

{

fprintf( stderr, "Current stack bottom is at 0x%p\\n", get_stack_bottom() );

return( EXIT_SUCCESS );

} /* end of main */

--------------------------------------------------------------------------

D: scz <scz@nsfocus.com> 2001-06-03 00:38

W. Richard Stevens在<<Advanced Programming in the UNIX Environment>>中详细

介绍了setjmp/longjmp以及sigsetjmp/siglongjmp函数。

这个程序的原理很简单,不断向栈底方向取值,越过栈底的地址访问会导致SIGSEGV

信号,然后利用长跳转回到主流程报告当前c值,自然对应栈底。

tt测试表明,在x86/FreeBSD中导致SIGBUS信号。据jonah报告,不仅仅是FreeBSD,

NetBSD 以及 OpenBSD 系统中上述程序越界访问也导致SIGBUS信号,而不是SIGSEGV

信号。

非局部转移,比如函数间转移的时候考虑使用setjmp/longjmp。但是如果涉及到信号

句柄与主流程之间的转移,就不能使用longjmp了。当捕捉到信号进入信号句柄,此

时当前信号被自动加入进程的信号屏蔽字中,阻止后来产生的这种信号干扰此信号句

柄。如果用longjmp跳出信号句柄,此时进程的信号屏蔽字状态未知,有些系统做了

保存恢复,有些系统没有做。根据POSIX.1,此时应该使用sigsetjmp/siglongjmp函

数。下面来自SPARC/Solaris 7的setjmp(3C)

--------------------------------------------------------------------------

#include <setjmp.h>

int setjmp ( jmp_buf env );

int sigsetjmp ( sigjmp_buf env, int savemask );

void longjmp ( jmp_buf env, int val );

void siglongjmp ( sigjmp_buf env, int val );

--------------------------------------------------------------------------

如果savemask非0,sigsetjmp在env中保存进程当前信号屏蔽字,相应siglongjmp回

来的时候从env中恢复信号屏蔽字。

数据类型sig_atomic_t由ANSI C定义,在写时不会被中断。它意味着这种变量在具有

虚存的系统上不会跨越页边界,可以用一条机器指令对其存取。这种类型的变量总是

与ANSI类型修饰符volatile一并出现,防止编译器优化带来的不确定状态。

在longjmp/siglongjmp中,全局、静态变量保持不变,声明为volatile的自动变量也

保持不变。

无论是否使用了编译优化开关,为了保证广泛兼容性,都应该在get_stack_bottom()

中声明c为volatile变量。

注意这里,必须使用长跳转,而不能从信号句柄中直接返回。因为导致信号SIGSEGV、

SIGBUS分发的语句始终存在,直接从信号句柄中返回主流程,将回到引发信号的原指

令处,而不是下一条指令(把这种情况理解成异常,而不是中断),于是立即导致下一

次信号分发,出现广义上的死循环,所谓程序僵住。可以简单修改上述程序,不利用

长跳转,简单对一个全局变量做判断决定是否继续循环递增c,程序最终僵住;如果

在信号句柄中输出调试信息,很容易发现这个广义上的无限循环。

D: scz <scz@nsfocus.com> 2001-06-03 00:40

在x86/Linux系统中用如下命令可以确定栈区所在

# cat /proc/1/maps <-- 观察1号进程init

... ...

bfffe000-c0000000 rwxp fffff000 00:00 0

#

在SPARC/Solaris 7中用/usr/proc/bin/pmap命令确定栈区所在

# /usr/proc/bin/pmap 1 <-- 观察1号进程init

... ...

FFBEC000 16K read/write/exec [ stack ]

#

16KB == 0x4000,0xFFBEC000 + 0x4000 == 0xFFBF0000

与前面tt介绍的

SPARC/Solaris 7/8 栈底是0xffbf0000( 栈底往低地址的4个字节总是零 )

相符合。

此外,在SPARC/Solaris 7下,可以这样验证之

# /usr/ccs/bin/nm -nx /dev/ksyms | grep "|_userlimit"

[7015] |0x0000100546f8|0x000000000008|OBJT |GLOB |0 |ABS |_userlimit

[8051] |0x000010054700|0x000000000008|OBJT |GLOB |0 |ABS |_userlimit32

# echo "_userlimit /J" | adb -k /dev/ksyms /dev/mem

physmem 3b72

_userlimit:

_userlimit: ffffffff80000000

# skd64 0x000010054700 8

byteArray [ 8 bytes ] ---->

0000000000000000 00 00 00 00 FF BF 00 00

# ~~~~~~~~~~~ 对于32-bit应用程序来说,这是用户

空间上限

如果编译64-bit应用程序,用户空间上限是_userlimit,也就是0xffffffff80000000

# /opt/SUNWspro/SC5.0/bin/cc -xarch=v9 -O -o gstack gstack.c

# ./gstack

Current stack bottom is at 0xffffffff80000000

#

对于SPARC/Solaris 2.6 32-bit kernel mode

# echo "_userlimit /X" | adb -k /dev/ksyms /dev/mem

physmem 3d24

_userlimit:

_userlimit: f0000000

#

2.5 如何得到一个运行中进程的内存映像

A: Sun Microsystems 1998-03-30

有些时候必须得到一个运行中进程的内存映像而不能停止该进程,Solaris系统了这

样的工具,gcore为运行中进程创建一个core文件。假设我的bash进程号是5347

# gcore 5347

gcore: core.5347 dumped

# file core.5347

core.5347: ELF 32-位 MSB core文件 SPARC 版本 1,来自'bash'

#

注意,只能获取属主是你自己的进程的内存映像,除非你是root。

2.6 调试器如何工作的

Q: 我想在一个自己编写的程序中单步运行另外一个程序,换句话说,那是一个调试

器,该如何做?

A: Erik de Castro Lopo <nospam@mega-nerd.com>

这是一个操作系统相关的问题。最一般的回答是使用ptrace()系统调用,尽管我

不确认究竟这有多么普遍。Linux man手册上说SVr4、SVID EXT、AT&T、X/OPEN

和BSD 4.3都支持它。

为了使用ptrace(),你的程序应该调用fork(),然后在子进程中做如下调用:

ptrace( PTRACE_TRACEME, 0, 0, 0 );

接下来调用exec()家族的函数执行你最终企图跟踪的程序。

为了单步进入子进程,在父进程中调用:

ptrace( PTRACE_SINGLESTEP, 0, 0, 0 );

还有一些其他函数做恢复/设置寄存器、内存变量一类的工作。

GDB的源代码足以回答这个问题。

2.7 x86/Linux上如何处理SIGFPE信号

Q: 参看如下程序

--------------------------------------------------------------------------

/*

* gcc -Wall -pipe -O3 -o sigfpe_test_0 sigfpe_test_0.c

*

* 注意与下面的编译效果进行对比,去掉优化开关-O3

*

* gcc -Wall -pipe -o sigfpe_test_0 sigfpe_test_0.c

*/

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <signal.h>

#include <unistd.h>

#include <setjmp.h>

/*

* for signal handlers

*/

typedef void Sigfunc ( int );

Sigfunc * signal ( int signo, Sigfunc *func );

static Sigfunc * Signal ( int signo, Sigfunc *func );

static void on_fpe ( int signo );

Sigfunc * signal ( int signo, Sigfunc *func )

{

struct sigaction act, oact;

act.sa_handler = func;

sigemptyset( &act.sa_mask );

act.sa_flags = 0;

if ( signo == SIGALRM )

{

#ifdef SA_INTERRUPT

act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */

#endif

}

else

{

#ifdef SA_RESTART

act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */

#endif

}

if ( sigaction( signo, &act, &oact ) < 0 )

{

return( SIG_ERR );

}

return( oact.sa_handler );

} /* end of signal */

static Sigfunc * Signal ( int signo, Sigfunc *func )

{

Sigfunc *sigfunc;

if ( ( sigfunc = signal( signo, func ) ) == SIG_ERR )

{

perror( "signal" );

exit( EXIT_FAILURE );

}

return( sigfunc );

} /* end of Signal */

static void on_fpe ( int signo )

{

fprintf( stderr, "here is on_fpe\\n" );

return;

} /* end of on_fpe */

int main ( int argc, char * argv[] )

{

unsigned int i;

Signal( SIGFPE, on_fpe );

i = 51211314 / 0;

/*

* 另外,增加这行后,再次对比有-O3和无-O3的效果

*

* fprintf( stderr, "i = %#X\\n", i );

*/

return( EXIT_SUCCESS );

} /* end of main */

--------------------------------------------------------------------------

有-O3、无-O3,以及有无最后那条fprintf()语句,效果上有差别,自行对比。如果

输出"here is on_fpe",则会发现永不停止。

D: 小四 <scz@nsfocus.com> 2001-12-14 18:25

为了便于讨论,约定两个名词,中断和异常。这里中断指最常规的中断,比如int指

令带来的软中断。异常的典型代表有除0错。区别在于,发生异常时,x86架构上CPU

将当前EIP(指向引发异常的指令)压栈,发生中断时,x86架构上CPU将当前EIP的后一

个地址(指向引发中断的指令的后一条指令)压栈。在异常处理代码中,如果认为能够

从灾难中恢复,可以不修改被压栈的EIP,从而返回到引发异常的指令处。更多细节

请查看Intel手册。

这些是从前DOS下残留的汇编知识,不过也快忘光了,刚才又找元宝宝确认了一下。

在上述代码中,on_fpe()直接返回了,导致再次触发异常,所以无休止输出。事实上

在所有的计算器处理程序中,都会对SIGFPE信号做相应处理,前些日子看yacc/lex的

时候又碰上过。正确的做法是,利用远跳转转移,让开引发异常的指令。

代码修改如下

--------------------------------------------------------------------------

/*

* gcc -Wall -pipe -O3 -o sigfpe_test_1 sigfpe_test_1.c

*

* 注意与下面的编译效果进行对比,去掉优化开关-O3

*

* gcc -Wall -pipe -o sigfpe_test_1 sigfpe_test_1.c

*/

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <signal.h>

#include <unistd.h>

#include <setjmp.h>

/*

* for signal handlers

*/

typedef void Sigfunc ( int );

Sigfunc * signal ( int signo, Sigfunc *func );

static Sigfunc * Signal ( int signo, Sigfunc *func );

static void on_fpe ( int signo );

static sigjmp_buf jmpbuf;

static volatile sig_atomic_t canjump = 0;

Sigfunc * signal ( int signo, Sigfunc *func )

{

struct sigaction act, oact;

act.sa_handler = func;

sigemptyset( &act.sa_mask );

act.sa_flags = 0;

if ( signo == SIGALRM )

{

#ifdef SA_INTERRUPT

act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */

#endif

}

else

{

#ifdef SA_RESTART

act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */

#endif

}

if ( sigaction( signo, &act, &oact ) < 0 )

{

return( SIG_ERR );

}

return( oact.sa_handler );

} /* end of signal */

static Sigfunc * Signal ( int signo, Sigfunc *func )

{

Sigfunc *sigfunc;

if ( ( sigfunc = signal( signo, func ) ) == SIG_ERR )

{

perror( "signal" );

exit( EXIT_FAILURE );

}

return( sigfunc );

} /* end of Signal */

static void on_fpe ( int signo )

{

if ( canjump == 0 )

{

return; /* unexpected signal, ignore */

}

canjump = 0;

fprintf( stderr, "here is on_fpe\\n" );

siglongjmp( jmpbuf, signo ); /* jump back to main, don't return */

return;

} /* end of on_fpe */

int main ( int argc, char * argv[] )

{

unsigned int i;

if ( sigsetjmp( jmpbuf, 1 ) != 0 )

{

fprintf( stderr, "c u later\\n" );

return( EXIT_SUCCESS );

}

/*

* now sigsetjump() is OK

*/

canjump = 1;

Signal( SIGFPE, on_fpe );

i = 51211314 / 0;

/*

* 另外,增加这行后,再次对比有-O3和无-O3的效果

*

* fprintf( stderr, "i = %#X\\n", i );

*/

return( EXIT_SUCCESS );

} /* end of main */

--------------------------------------------------------------------------

关于-O3的讨论,对gcc编译器熟悉的朋友请继续,呵,我对Linux下的这此东西,实

在缺乏兴趣

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
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- 王朝網路 版權所有