LINUX內核調試相關--oops信息的定位2

Ø  實驗目的與意義

1 、掌握printk 的使用、設置及實現原理,理解分級別進行打印log 信息的實現方法

2 、掌握如何分析oops 的方法

3 、掌握strace 工具的移植和使用方法

 

Ø  基本原理和方法

1 、請回顧棧的工作原理,尤其是棧幀的作用

2 、請對照printk 的源代碼來進行printk 相關實驗,並在實驗中進一步理解源代碼

 

Ø  實驗內容及步驟 

一. Printk實驗

1 、在內核中編寫自己的printk 代碼,可利用上次系統調用實驗中已有的代碼,也可利用之前驅動實驗中的模塊。

2 、在根文件系統中增加/proc 目錄,用來掛載proc 文件系統

3 、重新燒錄uImage (如果有所改動的話)和根文件系統,進入控制檯之後,輸入命令掛載proc 文件系統:mount – t proc none /proc 。

如果掛載成功, /proc 目錄應該可以看到文件,比如下面的結果:

# ls proc

1              74             devices        kpagecount     slabinfo

100            765             diskstats      kpageflags     stat

101            773            driver         loadavg        swaps

102            783            execdomains    locks          sys

103            784            fb             meminfo        sysrq-trigger

2               786            filesystems    misc           sysvipc

3              790            fs             modules        timer_list

4              804            ide            mounts         tty

5              806            interrupts     mtd            uptime

6              buddyinfo      iomem          net            version

60             bus            ioports        pagetypeinfo   vmstat

65             cmdline        irq            partitions     yaffs

71             cpu            kallsyms       sched_debug    zoneinfo

737            cpuinfo        kmsg           self

4 、檢查並修改printk 的log 級別,比如下面的命令:

# cat /proc/sys/kernel/printk
7       4       1       7
# echo 1 >/proc/sys/kernel/printk
# cat /proc/sys/kernel/printk
1       4       1       7

修改之後,默認的printk 打印(級別爲4 )不會顯示到串口終端,但仍可以通過“ cat /proc/kmsg ”看到打印結果。

5 、通過代碼和 /proc/sys/kernel/printk 分別修改log 級別,並對應printk 的源代碼來分析結果。

 

二.C 語言可變參數實驗

1 、在內核代碼kernel/printk.c 中的printk 函數用到了C 語言中的可變參數的用法。請參考下面的代碼來學習如何使用可變參數。以下代碼可直接在x86 環境測試:

 

#include <stdio.h>

 

typedef char *va_list;

 

/*

  *  * Storage alignment properties

  *   */

#define  _AUPBND                 (sizeof (signed int) - 1)

#define  _ADNBND                (sizeof (signed int) - 1)

 

/*

  *  * Variable argument list macro definitions

  *   */

#define _bnd(X, bnd)            (((sizeof (X)) + (bnd)) & (~(bnd)))

#define va_arg(ap, T)           (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))

#define va_end(ap)              (void) 0

#define va_start(ap, A)         (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

 

 

int max ( int num, ... )

{

    int m = -0x7FFFFFFF;

    int i;

     va_list ap;

    va_start ( ap, num );

    for ( i= 0; i< num; i++ )

    {

        int t = va_arg (ap, int);

        if ( t > m )

        {

            m = t;

        }

    }

    va_end (ap);

    return m;

}

 

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

{   

    int n = max ( 5, 2, 6 ,3 ,8 ,1);

    printf("n=%d./n", n);

    return 0;

}

 

2 、自己動手 分析上面代碼可變參數的用法及實現方式。提示:va_start ( ap, num ) 是爲了取得可變參數在棧中的位置,該宏展開執行後,ap 將指向第一個可變參數。可利用GDB 和彙編代碼協助分析。

  

3 、ARM 架構中通常使用寄存器而不是棧來傳遞參數,那麼,上述可變參數的方式能夠用於ARM 架構中嗎?請想辦法找到證據來支持你的猜測。

 

三. Oops實驗

1 、在上次系統調用實驗的代碼中,人爲的製造產生oops 的條件,比如下面的改動:

 

asmlinkage long sys_mytest()

{

printk("pid: %d:/tThis is my call./n",current->pid);

*(int *)0 = 0;

    return 0;

}

 

2 、從串口終端得到所產生的oops 消息,並進行初步分析

3 、在內核代碼中,利用arm-linux-objdump 得到kernel/sys.o 的彙編代碼,對照彙編代碼進行分析

4 、arm-linux-addr2line 工具可用來尋找代碼地址所對應的c 代碼(),可嘗試:

arm-linux-addr2line e sys.o 0xnnnn(你出錯的代碼地址)

 

(1) 在kernel/sys.c 文件裏

#arm-linux-gcc c sys.o sys.c(生成彙編文件)

#vim sys.o

尋找:sys_mytest

找到地址是:000000b0

而通過下面Oops 信息知道:PC is at sys_mytest+0x28/0x34

則0xnnnn( 代碼出錯的地址)=000000b0+0x28=0xd8

(2) # arm-linux-addr2line e sys.o 0xd8

結果輸出是137

則查出出錯的地方在sys.c 中的137 行。

 

5 、下面是我實驗中遇到的oops ,請嘗試做一些初步分析:

Unable to handle kernel NULL pointer dereference at virtual address 00000000

pgd = c3eb0000

[00000000] *pgd=33ddd031, *pte=00000000, *ppte=00000000

Internal error: Oops: 817 [#1]

Modules linked in:

CPU: 0    Not tainted  (2.6.25.8 #7)

PC is at sys_mytest+0x28/0x34 (表示在sys_mytest+0x28 至sys_mytest+0x34 之間,以4 個字節爲一個單位)

LR is at 0xc031781c

pc : [<c00561ec>]    lr : [<c031781c>]    psr: 60000013

sp : c3ec3f98  ip : c031781c  fp : c3ec3fa4

r10: 00008528  r9 : c3ec2000  r8 : c002cb84

r7 : 00000161  r6 : beeced1c  r5 : 00000000  r4 : 0000000a

r3 : 80000013  r2 : 00000001  r1 : 00000001  r0 : 00000000

Flags: nZCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment user

Control: 0000717f  Table: 33eb0000  DAC: 00000015

Process syscall_test (pid: 777, stack limit = 0xc3ec2260)

Stack: (0xc3ec3f98 to 0xc3ec4000)

3f80:                                                       00000000 c3ec3fa8

3fa0: c002c9e0 c00561d4 00000000 beeced1c 00000161 beecef14 beecef1c 0000000a

3fc0: 0000000a 00000000 beeced1c 00000001 beecef14 000081c4 00008528 beeced18

3fe0: beeced08 beececfc 000081e8 0000f8e0 60000010 00000161 7bf2fafd eff7fbbd

Backtrace:

[<c00561c4>] (sys_mytest+0x0/0x34) from [<c002c9e0>] (ret_fast_syscall+0x0/0x2c)

Code: e59f0010 e59310d8 ebffcf47 e3a00000 (e5800000)

---[ end trace e5388d99d2481600 ]---

 

 

四. Strace實驗

1 、從www.sourceforge.net 上下載strace 的源代碼

2 、配置並編譯strace :

./configure --host=arm-linux

make

3 .將strace 工具加入到你的根文件系統中,並測試使用它

 

第一篇定位Oops的具體代碼行 作者: albcamus (百無一用書生)
(
來自Linus Torvalds的討論:
[url]https://groups.google.com/group/linux.kernel/browse_thread/thread/b70bffe9015a8c41/ed9c0a0cfcd31111[/url]
又,[url]http://kerneltrap.org/Linux/Further_Oops_Insights[/url]
)
     
        例如這樣的一個Oops:
                Oops: 0000 [#1] PREEMPT SMP  
                Modules linked in: capidrv kernelcapi isdn slhc ipv6 loop dm_multipath snd_ens1371 gameport snd_rawmidi snd_ac97_codec ac97_bus snd_seq_dummy snd_seq_oss snd_seq_midi_event snd_seq snd_seq_device snd_pcm_oss snd_mixer_oss snd_pcm snd_timer snd parport_pc floppy parport pcnet32 soundcore mii pcspkr snd_page_alloc ac i2c_piix4 i2c_core button power_supply sr_mod sg cdrom ata_piix libata dm_snapshot dm_zero dm_mirror dm_mod BusLogic sd_mod scsi_mod ext3 jbd mbcache uhci_hcd ohci_hcd ehci_hcd

                Pid: 1726, comm: kstopmachine Not tainted (2.6.24-rc3-module #2)
                EIP: 0060:[<c04e53d6>] EFLAGS: 00010092 CPU: 0
                EIP is at list_del+0xa/0x61
                EAX: e0c3cc04 EBX: 00000020 ECX: 0000000e EDX: dec62000
                ESI: df6e8f08 EDI: 000006bf EBP: dec62fb4 ESP: dec62fa4
                 DS: 007b ES: 007b FS: 00d8 GS: 0000 SS: 0068
                Process kstopmachine (pid: 1726, ti=dec62000 task=df8d2d40 task.ti=dec62000)
                Stack: 000006bf dec62fb4 c04276c7 00000020 dec62fbc c044ab4c dec62fd0 c045336c
                       df6e8f08 c04532b4 00000000 dec62fe0 c043deb0 c043de75 00000000 00000000
                       c0405cdf df6e8eb4 00000000 00000000 00000000 00000000 00000000
                Call Trace:
                 [<c0406081>] show_trace_log_lvl+0x1a/0x2f
                 [<c0406131>] show_stack_log_lvl+0x9b/0xa3
                 [<c04061dc>] show_registers+0xa3/0x1df
                 [<c0406437>] die+0x11f/0x200
                 [<c0613cba>] do_page_fault+0x533/0x61a
                 [<c06123ea>] error_code+0x72/0x78
                 [<c044ab4c>] __unlink_module+0xb/0xf
                 [<c045336c>] do_stop+0xb8/0x108
                 [<c043deb0>] kthread+0x3b/0x63
                 [<c0405cdf>] kernel_thread_helper+0x7/0x10
                 =======================
                Code: 6b c0 e8 2e 7e f6 ff e8 d1 16 f2 ff b8 01 00 00 00 e8 aa 1c f4 ff 89 d8 83 c4 10 5b 5d c3 90 90 90 55 89 e5 53 83 ec 0c 8b 48 04 <8b> 11 39 c2 74 18 89 54 24 08 89 44 24 04 c7 04 24 be 32 6b c0  
                EIP: [<c04e53d6>] list_del+0xa/0x61 SS:ESP 0068:dec62fa4
                note: kstopmachine[1726] exited with preempt_count 1
     
        1, 有自己編譯的vmlinux: 使用gdb
     
           編譯時打開complie with debug info選項。

           注意這行:
     
                EIP is at list_del+0xa/0x61
     
           這告訴我們,list_del函數有0x61這麼大,而Oops發生在0xa處。 那麼我們先看一下list_del從哪裏開始:

                # grep list_del /boot/System.map-2.6.24-rc3-module
                c10e5234 T plist_del
                c10e53cc T list_del
                c120feb6 T klist_del
                c12d6d34 r __ksymtab_list_del
                c12dadfc r __ksymtab_klist_del
                c12e1abd r __kstrtab_list_del
                c12e9d03 r __kstrtab_klist_del

           於是我們知道,發生Oops時的EIP值是:

                c10e53cc + 0xa  == c10e53d6

           然後用gdb查看:

                # gdb /home/arc/build/linux-2.6/vmlinux
                (gdb) b *0xc10e53d6
                Breakpoint 1 at 0xc10e53d6: file /usr/src/linux-2.6.24-rc3/lib/list_debug.c, line 64.

           看,gdb直接就告訴你在哪個文件、哪一行了。

           gdb中還可以這樣:

                # gdb Sources/linux-2.6.24/vmlinux
                (gdb) l *do_fork+0x1f
                0xc102b7ac is in do_fork (kernel/fork.c:1385).
                1380
                1381    static int fork_traceflag(unsigned clone_flags)
                1382    {
                1383            if (clone_flags & CLONE_UNTRACED)
                1384                    return 0;
                1385            else if (clone_flags & CLONE_VFORK) {
                1386                    if (current->ptrace & PT_TRACE_VFORK)
                1387                            return PTRACE_EVENT_VFORK;
                1388            } else if ((clone_flags & CSIGNAL) != SIGCHLD) {
                1389                    if (current->ptrace & PT_TRACE_CLONE)
                (gdb)

            也可以直接知道line number。

            或者:

                (gdb) l *(0xffffffff8023eaf0 + 0xff)  /* 出錯函數的地址加上偏移 */



        2, 沒有自己編譯的vmlinux: TIPS

           如果在lkml或bugzilla上看到一個Oops,而自己不能重現,那就只能反彙編以"Code:"開始的行。 這樣可以嘗試定位到
           源代碼中。

           注意,Oops中的Code:行,會把導致Oops的第一條指令,也就是EIP的值的第一個字節, 用尖括號<>括起來。 但是,有些
           體系結構(例如常見的x86)指令是不等長的(不一樣的指令可能有不一樣的長度),所以要不斷的嘗試(trial-and-error)。

           Linus通常使用一個小程序,類似這樣:

                const char array[] = "/xnn/xnn/xnn...";
                int main(int argc, char *argv[])
                {
                        printf("%p/n", array);
                        *(int *)0 = 0;
                }

e.g. /*{{{*/ /* 注意, array一共有從array[0]到array[64]這65個元素, 其中出錯的那個操作碼<8b> == arry[43] */
#include <stdio.h>
#include <stdlib.h>


const char array[] ="/x6b/xc0/xe8/x2e/x7e/xf6/xff/xe8/xd1/x16/xf2/xff/xb8/x01/x00/x00/x00/xe8/xaa/x1c/xf4/xff/x89/xd8/x83/xc4/x10/x5b/x5d/xc3/x90/x90/x90/x55/x89/xe5/x53/x83/xec/x0c/x8b/x48/x04/x8b/x11/x39/xc2/x74/x18/x89/x54/x24/x08/x89/x44/x24/x04/xc7/x04/x24/xbe/x32/x6b/xc0";
int main(int argc, char *argv[])
{
        printf("%p/n", array);
        *(int *)0 = 0;
}
/*}}}*/



           用gcc -g編譯,在gdb裏運行它:

                [arc@dhcp-cbjs05-218-251 ~]$ gdb hello
                GNU gdb Fedora (6.8-1.fc9)
                Copyright (C) 2008 Free Software Foundation, Inc.
                License GPLv3+: GNU GPL version 3 or later <[url]http://gnu.org/licenses/gpl.html[/url]>
                This is free software: you are free to change and redistribute it.
                There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
                and "show warranty" for details.
                This GDB was configured as "x86_64-redhat-linux-gnu"...
                (no debugging symbols found)
                (gdb) r
                Starting program: /home/arc/hello
                0x80484e0

                Program received signal SIGSEGV, Segmentation fault.

           注意,這時候就可以反彙編0x80484e0這個地址:

                (gdb) disassemble 0x80484e0
                Dump of assembler code for function array:
                0x080484e0 <array+0>:   imul   $0xffffffe8,%eax,%eax
                0x080484e3 <array+3>:   jle,pn 0x80484dc <__dso_handle+20>
                0x080484e6 <array+6>:   ljmp   *<internal disassembler error>
                0x080484e8 <array+8>:   rcll   (%esi)
                0x080484ea <array+10>:  repnz (bad)
                0x080484ec <array+12>:  mov    $0x1,%eax
                0x080484f1 <array+17>:  call   0x7f8a1a0
                0x080484f6 <array+22>:  mov    %ebx,%eax
                0x080484f8 <array+24>:  add    $0x10,%esp
                0x080484fb <array+27>:  pop    %ebx
                0x080484fc <array+28>:  pop    %ebp
                0x080484fd <array+29>:  ret
                0x080484fe <array+30>:  nop
                0x080484ff <array+31>:  nop
                0x08048500 <array+32>:  nop
                0x08048501 <array+33>:  push   %ebp
                0x08048502 <array+34>:  mov    %esp,%ebp
                0x08048504 <array+36>:  push   %ebx
                0x08048505 <array+37>:  sub    $0xc,%esp
                0x08048508 <array+40>:  mov    0x4(%eax),%ecx
                0x0804850b <array+43>:  mov    (%ecx),%edx
                0x0804850d <array+45>:  cmp    %eax,%edx
                0x0804850f <array+47>:  je     0x8048529
                0x08048511 <array+49>:  mov    %edx,0x8(%esp)
                0x08048515 <array+53>:  mov    %eax,0x4(%esp)
                0x08048519 <array+57>:  movl   $0xc06b32be,(%esp)
                0x08048520 <array+64>:  add    %ah,0xa70
                End of assembler dump.
                (gdb)

          OK, 現在你知道出錯的那條指令是array[43],也就是mov    (%ecx),%edx,也就是說,(%ecx)指向了一個錯誤內存地址。

補充:

爲了使彙編代碼和C代碼更好的對應起來, Linux內核的Kbuild子系統提供了這樣一個功能: 任何一個C文件都可以單獨編譯成彙編文件,例如:

make path/to/the/sourcefile.s

例如我想把kernel/sched.c編譯成彙編,那麼:

make kernel/sched.s V=1

或者:

make kernel/sched.lst V=1

         編譯出*.s文件
          
           有時侯需要對*.s文件進行分析,以確定BUG所在的位置。 對任何一個內核build目錄下的*.c文件,都可以
           直接編譯出*.s文件。

                   # make drivers/net/e100.s V=1
          
           而對於自己寫的module,就需要在Makefile中有一個靈活的target寫法:
                  
                # cat Makefile
                obj-m := usb-skel.o
                KDIR := /lib/modules/`uname -r`/build
                traget := modules

                default:
                        make -C $(KDIR) M=$(shell pwd) $(target)
                clean:
                        rm -f *.o *.ko .*.cmd *.symvers *.mod.c
                        rm -rf .tmp_versions


                # make target=usb-skel.s V=1
          
           這樣,kbuild系統才知道你要make的目標不是modules,而是usb-skel.s。




另外, 內核源代碼目錄的./scripts/decodecode文件是用來解碼Oops的:

./scripts/decodecode < Oops.txt
 
第二篇: 定位可動態加載的內核模塊的OOPS代碼行作者:Godbach (To be 千里馬!)
最近又仔細學習了albcamus版主提供的《定位Oops的具體代碼行》(鏈接:http://linux.chinaunix.net/bbs/viewthread.php?tid=1008573),並且進行了實踐。因此這裏簡單總結一下,並且以實例的方式給出定位可動態加載模塊Oops信息的方法。

本文歡迎自由轉載,但請標明出處,並保證本文的完整性。
Godbach
Apr 19, 2009

1. 從vmlinux獲取具體的代碼行

文章中albcamus版主也提到了,需要有自己編譯的vmlinux,而且編譯時打開compile with debug info. 這個選項打開之後會使vmlinux文件比不加調試信息大一些。我這裏代調試信息的是49M。建議如果學習的時候,想使用gdb的方式獲取出錯代碼行的話,就加上這個編譯條件。
然後就可以按照具體的方法去操作,可以定位到具體的C 代碼行。

2. 從自己編譯的內核模塊出錯信息中獲取代碼行
以ldd3中提供的misc-modules/faulty.c爲例。主要以faulty_write函數作分析。
(1)由於作者提供的函數代碼就一樣,過於簡單,我這裏簡單加上一些代碼(也就是判斷和賦值),如下:

ssize_t faulty_write ( struct file* filp,const char __user* buf, size_tcount ,
                loff_t * pos)
{
        /* make a simple fault by dereferencing a NULL pointer */
        if (count> 0x100)
                count = 0x100;
        * (int* ) 0 = 0;
        return count;
}

(2)編譯該模塊,並且mknod /dev/faulty
(3)向該模塊寫入數據:echo 1 > /dev/faulty, 內核OOPS,信息如下:

 

< 1> BUG: unable to handle kernelNULL pointer dereference atvirtual address 00000000
printing eip:
f8e8000e
* pde = 00000000
Oops: 0002 [# 3]
SMP
Modules linked in: faulty autofs4 hidp rfcomm l2cap.. . . ..//此處省略若干字符

CPU: 1
EIP: 0060: [ < f8e8000e>]Not tainted VLI
EFLAGS: 00010283 ( 2. 6. 18. 3# 2)
EIP is at faulty_write+ 0xe/ 0x19[ faulty]
eax: 00000001 ebx: f4f6ca40 ecx: 00000001 edx: b7c2d000
esi: f8e80000 edi: b7c2d000 ebp: 00000001 esp: f4dc5f84
ds: 007b es: 007b ss: 0068
Process bash ( pid: 6084, ti= f4dc5000 task= f7c8d4d0 task. ti= f4dc5000)
Stack : c1065914 f4dc5fa4 f4f6ca40 fffffff7 b7c2d000 f4dc5000 c1065f06 f4dc5fa4
       00000000 00000000 00000000 00000001 00000001 c1003d0b 00000001 b7c2d000
       00000001 00000001 b7c2d000 bfd40aa8 ffffffda 0000007b c100007b 00000004
Call Trace:
[ < c1065914>] vfs_write+ 0xa1/ 0x143
[ < c1065f06>] sys_write+ 0x3c/ 0x63
[ < c1003d0b>] syscall_call+ 0x7/ 0xb
Code: Bad EIP value.
EIP: [ < f8e8000e> ] faulty_write+ 0xe/ 0x19[ faulty] SS: ESP 0068: f4dc5f84

其中,我們應該關注的信息是第一行紅色標出部分:告訴我們操作了NULL指針。其次,就是第二行紅色部分:EIP is at faulty_write+0xe/0x19。這個出錯信息告訴我們EIP指針出現問題的地方時faulty_write函數,而且指出了是faulty 模塊。
同時,faulty_write+0xe/0x19的後半部分0xe/0x19,說明該函數的大小時0x019,出錯位置是在0x0e。這兩個值應該值得都是彙編代碼的值。
(4)將faulty模塊反彙編出彙編代碼:
          objdump -d faulty.ko > faulty.s
        或
          objdump -d faulty.o > faulty.s
然後,我們打開faulty.s文件。由於我們需要關注的部分正好在文件的前面,因此我這裏只貼出文件的前面一部分內容:

faulty. o:file format elf32- i386

Disassembly of section . text:

00000000 < faulty_write>:
   0: 81 f9 00 01 00 00 cmp $0x100,% ecx
   6: b8 00 01 00 00 mov $0x100,% eax
   b: 0f 46 c1 cmovbe % ecx, % eax
   e: c7 05 00 00 00 00 00 movl $0x0, 0x0
  15: 00 00 00
  18: c3 ret

00000019 < cleanup_module>:
  19: a1 00 00 00 00 mov 0x0,% eax
  1e: ba 00 00 00 00 mov $0x0,% edx
  23: e9 fc ff ff ff jmp 24 < cleanup_module+ 0xb>

00000028 < faulty_init>:
  28: a1 00 00 00 00 mov 0x0,% eax
  2d: b9 00 00 00 00 mov $0x0,% ecx
  32: ba 00 00 00 00 mov $0x0,% edx
  37: e8 fc ff ff ff call 38 < faulty_init+ 0x10>
  3c: 85 c0 test% eax,% eax
  3e: 78 13 js 53 < faulty_init+ 0x2b>
  40: 83 3d 00 00 00 00 00 cmpl $0x0, 0x0
  47: 74 03 je 4c < faulty_init+ 0x24>
  49: 31 c0 xor% eax,% eax
  4b: c3 ret
  4c: a3 00 00 00 00 mov % eax, 0x0
  51: 31 c0 xor% eax,% eax
  53: c3 ret

由以上彙編代碼可以看出,faulty_write函數的大小確實是0x18 -0x00 +1 = 0x19. 那麼EIP指針出問題的地方是0x0e處,代碼爲:

e: c7 05 00 00 00 00 00 movl $0x0, 0x0

這行彙編代碼就是將0值保存到0地址的位置。那麼很顯然是非法的。這一行對應的C 代碼應該就是:
*(int *)0 = 0;

(5)以上是對模塊出錯信息的分析。不過也有一定的侷限。
   首先就是EIP出錯的位置正好在本模塊內部,這樣可以在本模塊定位問題;
   其次,要求一定的彙編基礎,特別是當一個函數的代碼比較多時,對應的彙編代碼也比較大,如何準確定位到C代碼行需要一定的經驗和時間。

    實際運用中,可以將內核代碼行的定位和可動態加載的內核模塊代碼行的定位結合起來使用,應該可以較快的定位問題。

    分析中有紕漏或者不妥的地方希望大家指出,也希望有網友分享更有效的方法。
出自: http://linux.chinaunix.net/bbs/thread-1097586-1-1.html
發佈了6 篇原創文章 · 獲贊 13 · 訪問量 41萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章