[ 永遠的UNIX::UNIX技術資料的寶庫 ]   GB | BIG5

首頁 > 編程技術 > C/C++ > 正文
Unix編程/應用問答中文版 ---2.堆棧相關問題
本文出自:http://www.nsfocus.com 維護:小四 (2002-10-18 06:02:00)
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下的這此東西,實 
在缺乏興趣。 (http://www.fanqiang.com)
    進入【UNIX論壇

相關文章
Unix編程/應用問答中文版 ---1.系統管理配置問題 (2002-10-17 06:02:00)
Unix編程/應用問答中文版 ---0.簡介 Unix/C傳奇問題 (2002-10-16 06:02:01)
 

★  樊強制作 歡迎分享  ★