免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 3591 | 回复: 3
打印 上一主题 下一主题

[BootLoader] [原创]u-boot汇编代码探险指南.(另指出源码中的错误) [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2012-06-04 09:35 |只看该作者 |倒序浏览
本帖最后由 HateMath 于 2012-06-26 16:23 编辑

u-boot汇编代码探险指南.[by HateMath]

文章早写出来了,不过话说编辑这么长的帖子真辛苦,主要是给代码部分做标记,工作量大。今天心情好,搞一搞。


u-boot是一个典型的嵌入式bootloader,很多人都想一窥它源码真面目。在这个过程中,首先遇到的就是汇编代码的原始丛林,本文试图带领大家进入这片原始丛林,一睹传说中的神秘的“stage1”启动代码。

(一)准备工作

首先把环境交代一下。
1)u-boot-1.1.6(http://sourceforge.net/projects/u-boot/files/)
2)开发板为smdk2410(cpu为samsung s3c2410,arm920t的处理器核)

然后把涉及到的文件做个简写约定,以后方便叙述。
  1. cpu/arm920t/start.S             记为 startS
  2. cpu/arm920t/interrupts.c        记为 intrptC
  3. board/smdk2410/u-boot.lds       记为 ubootLDS
  4. board/smdk2410/lowlevel_init.S  记为 llinitS
  5. lib_arm/board.c                 记为 boardC
复制代码
最后是用到的主要参考资料:
[1] Uboot中start.S源码的指令级的详尽解析.作者:green-waste(at)163.com
[2] s3c2410 datasheet

值得一提的是资料[1],写的很详细,具体解释了每一条汇编指令。如果说我的这篇文章的侧重点是带领大家在原始丛林中探路,让大家对这片丛林的猛兽、泥沼、陷阱有个明确的认识,那么资料[1]的侧重点则是研究路上的每一株花花草草。这两篇文章是互补关系,所以严重推荐在看懂我这篇后再看一下资料[1],沾惹一下各种花花草草。

(二)代码分析
先说一下u-boot启动的大致流程吧,俯瞰一下这片原始丛林,然后就钻进去探路了。
一般来说,bootloader这种东西,其启动分为两个阶段,分别叫stage1和stage2。startS的汇编代码,基本上做的是stage1的事情,这是最底层、最基础也是最重要的事情。它指定了各种中断处理程序、初始化处理器、内存到合适的工作状态,并将自己拷贝到内存中,进行stage2的启动。
好,下面开始探路。友情提醒:跟紧我,别掉队,指南针在程序的世界无效。

1)第一站就挺险恶的,陷阱遍布。u-boot是从startS中的“_start: b reset”语句开始运行的,这可以从ubootLDS文件中的“ENTRY(_start)”这句看出来。而“_start: b reset”这句话和它下面的一堆代码其实是一个整体,它们是干什么的呢?

2)学过单片机的都知道,芯片上电的时候,大多从地址0开始取指令执行,而地址0附近往往有好多中断入口。ARM处理器也差不多,除了地址0处的复位中断外,还有未定义指令中断、软件中断、预取指令中断(有人管这些叫异常)等等,总之是各种倒霉的情况的处理集中营。程序员要告诉处理器出了这些倒霉情况时该怎么做。这就我们看到的:
  1. _start: b reset
  2. ldr pc, _undefined_instruction
  3. ldr pc, _software_interrupt
  4. ldr pc, _prefetch_abort
  5. ldr pc, _data_abort
  6. ldr pc, _not_used
  7. ldr pc, _irq
  8. ldr pc, _fiq
复制代码
每一句的意思都是,如果出现某个中断,处理器就会将相应的处理程序的起始地址装入pc寄存器,执行相应的处理程序。至于处理器怎么知道哪个中断对应哪个处理程序,学过单片机的都知道,我就不唐僧了。我想起了早年的BASIC编程中,类似“出什么问题到预先设定的地方去解决”这种理念,就被称为陷阱技术,区别在于那是软件陷阱,我们正在谈的这个是正宗的硬件陷阱。

举例分析一下,比如出现了未定义指令中断,处理器就会执行“ldr pc, _undefined_instruction”这句,而_undefined_instruction代表哪个地址呢,在当前文件中已有定义:
  1. _undefined_instruction: .word undefined_instruction
  2. _software_interrupt: .word software_interrupt
  3. _prefetch_abort: .word prefetch_abort
  4. _data_abort: .word data_abort
  5. _not_used: .word not_used
  6. _irq: .word irq
  7. _fiq: .word fiq
复制代码
哦,原来是undefined_instruction这个东东的地址。在当前文件中搜索undefined_instruction:
  1. undefined_instruction:
  2. get_bad_stack
  3. bad_save_user_regs
  4. bl do_undefined_instruction
复制代码
原来这个东东是个子程序的标号。这个子程序有三句话,第一句“get_bad_stack”是个宏定义,当前文件中:
  1. .macro get_bad_stack
  2. ldr r13, _armboot_start @ setup our mode stack
  3. sub r13, r13, #(CONFIG_STACKSIZE+CFG_MALLOC_LEN)
  4. sub r13, r13, #(CFG_GBL_DATA_SIZE+8) @ reserved a couple spots in abort stack

  5. str lr, [r13] @ save caller lr / spsr
  6. mrs lr, spsr
  7. str lr, [r13, #4]

  8. mov r13, #MODE_SVC @ prepare SVC-Mode
  9. @ msr spsr_c, r13
  10. msr spsr, r13
  11. mov lr, pc
  12. movs pc, lr
  13. .endm
复制代码
第二句“bad_save_user_regs”也是个宏定义,当前文件中:
  1. .macro bad_save_user_regs
  2. sub sp, sp, #S_FRAME_SIZE
  3. stmia sp, {r0 - r12} @ Calling r0-r12
  4. ldr r2, _armboot_start
  5. sub r2, r2, #(CONFIG_STACKSIZE+CFG_MALLOC_LEN)
  6. sub r2, r2, #(CFG_GBL_DATA_SIZE+8) @ set base 2 words into abort stack
  7. ldmia r2, {r2 - r3} @ get pc, cpsr
  8. add r0, sp, #S_FRAME_SIZE @ restore sp_SVC
  9. add r5, sp, #S_SP
  10. mov r1, lr
  11. stmia r5, {r0 - r3} @ save sp_SVC, lr_SVC, pc, cpsr
  12. mov r0, sp
  13. .endm
复制代码
第三句“do_undefined_instruction”,在intrptC文件中:
  1. void do_undefined_instruction (struct pt_regs *pt_regs)
  2. {
  3. printf ("undefined instruction\n");
  4. show_regs (pt_regs);
  5. bad_mode ();
  6. }
  7. void bad_mode (void)
  8. {
  9. panic ("Resetting CPU ...\n");
  10. reset_cpu (0);
  11. }
复制代码
上述流程就是大概意思就是:处理器在执行程序时出问题了,赶紧给现场拍照并通知程序员,最后试图重启。


3)按照上述分析,处理器上电复位时,会走到“b reset”这句。找”reset”:
  1. reset:
  2. /*
  3. * set the cpu to SVC32 mode
  4. */
  5. mrs r0,cpsr
  6. bic r0,r0,#0x1f
  7. orr r0,r0,#0xd3
  8. msr cpsr,r0
复制代码
有人说:哎,这段代码前面还有一大段代码你没分析呢。 别想太多,Follow me,你要是迷路了,只有一种可能,就是没有紧跟我。
这段代码的功能是设置处理器运行在SVC32模式。对照s3c2410的datasheet就知道那些乱七八糟的值是怎么得来的了,很简单的。至于为什么要设置为SVC32模式,文献[1]有详细解释,简要的说:1)SVC32模式有各种访问资源的特权,方便干活 ;2)如果要启动linux系统,linux要求CPU必须处于SVC32模式。

好,继续往下走:

4)关闭看门狗
  1. /* turn off the watchdog */
  2. #if defined(CONFIG_S3C2400)
  3. # define pWTCON 0x15300000
  4. # define INTMSK 0x14400008 /* Interupt-Controller base addresses */
  5. # define CLKDIVN 0x14800014 /* clock divisor register */
  6. #elif defined(CONFIG_S3C2410)
  7. # define pWTCON 0x53000000
  8. # define INTMSK 0x4A000008 /* Interupt-Controller base addresses */
  9. # define INTSUBMSK 0x4A00001C
  10. # define CLKDIVN 0x4C000014 /* clock divisor register */
  11. #endif

  12. #if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410)
  13. ldr r0, =pWTCON
  14. mov r1, #0x0
  15. str r1, [r0]
复制代码
分析:这段代码的功能是关闭看门狗。看门狗是处理器内部的一个硬件模块,是个好东西,但是这里用不着,要让丫闭嘴,不然会捣乱,比如咬人什么的。一开始那一堆密集的预编译,用来指定看门狗狗窝的住址,因为不同的家庭(处理器),狗窝的位置不一定相同,要分别指定一下,避免访问狗窝时误入了猪圈。将看门狗赋值为0(见datasheet),它就不叫唤了。

看,原始丛林也没想象中那么险恶嘛。继续:

5)设置中断和时钟
  1. /*
  2. * mask all IRQs by setting all bits in the INTMR - default
  3. */
  4. mov r1, #0xffffffff
  5. ldr r0, =INTMSK
  6. str r1, [r0]
  7. # if defined(CONFIG_S3C2410)
  8. ldr r1, =0x3ff
  9. ldr r0, =INTSUBMSK
  10. str r1, [r0]
  11. # endif
  12. /* FCLK:HCLK:PCLK = 1:2:4 */
  13. /* default FCLK is 120 MHz ! */
  14. ldr r0, =CLKDIVN
  15. mov r1, #3
  16. str r1, [r0]
  17. #endif /* CONFIG_S3C2400 || CONFIG_S3C2410 */
复制代码
分析:这段代码的功能是禁止所有中断。跟关闭看门狗类似,都是按照datasheet的说明给特定寄存器赋特定的值罢了。
但是,这个里面我想说说自己的看法。我觉得”ldr r1, =0x3ff”这句话不妥,因为INTSUBMSK的bit10是INT_ADC,既然要禁止所有中断,为什么要单单留这个INT_ADC?难道开发者和它有一腿 ? 我认为正确写法应该是”ldr r1, =0x7ff”。
6)处理器杂项初始化、板载内存初始化
  1. /*
  2. * we do sys-critical inits only at reboot,
  3. * not when booting from ram!
  4. */
  5. #ifndef CONFIG_SKIP_LOWLEVEL_INIT
  6. bl cpu_init_crit
  7. #endif
复制代码
分析:这段的注释文字,我当初阅读源码的时候,曾花了一段时间去揣摩,主要是我一开始接受不了u-boot作为一个bootloader,居然还会被别的程序load。这段话的言下之意是,u-boot不总是从flash、usb、sd卡之类的设备中启动,有时候,u-boot会被别的程序加载到ram,从ram中开始运行(奇怪吧,自己是bootloader,却要别人去加载。但这种情况是存在的,README文件中有句话提到这种情况),这个时候,内存刷新参数之类的工作,肯定是别人已经做好了,u-boot就不要多此一举,而且最好别多此一举。
对于大部分情况,cpu_init_crit还是要被执行的。这个子程序在就在startS文件中,且看其内容:
  1. #ifndef CONFIG_SKIP_LOWLEVEL_INIT
  2. cpu_init_crit:
  3. /*
  4. * flush v4 I/D caches
  5. */
  6. mov r0, #0
  7. mcr p15, 0, r0, c7, c7, 0 /* flush v3/v4 cache */
  8. mcr p15, 0, r0, c8, c7, 0 /* flush v4 TLB */

  9. /*
  10. * disable MMU stuff and caches
  11. */
  12. mrc p15, 0, r0, c1, c0, 0
  13. bic r0, r0, #0x00002300 @ clear bits 13, 9:8 (--V- --RS)
  14. bic r0, r0, #0x00000087 @ clear bits 7, 2:0 (B--- -CAM)
  15. orr r0, r0, #0x00000002 @ set bit 2 (A) Align
  16. orr r0, r0, #0x00001000 @ set bit 12 (I) I-Cache
  17. mcr p15, 0, r0, c1, c0, 0

  18. /*
  19. * before relocating, we have to setup RAM timing
  20. * because memory timing is board-dependend, you will
  21. * find a lowlevel_init.S in your board directory.
  22. */
  23. mov ip, lr
  24. bl lowlevel_init
  25. mov lr, ip
  26. mov pc, lr
  27. #endif /* CONFIG_SKIP_LOWLEVEL_INIT */
复制代码
上面的代码可以分为三个部分,第一部分flush(我想不出确切意思的中文翻译) 指令和数据缓存;第二部分是禁用内存管理单元MMU;第三部分设置内存参数,其中调用了文件llinitS 中的lowlevel_init子程序。内存设置完全是和板子相关的,放在 board/smdk2410/ 目录里正合适。

7)走到现在还能跟上吧?快结束了,不过,关键的地方到了。下面这段代码可谓承上启下、承前启后,很重要。
  1. #ifndef CONFIG_SKIP_RELOCATE_UBOOT
  2. relocate: /* relocate U-Boot to RAM */
  3. adr r0, _start /* r0 - current position of code */
  4. ldr r1, _TEXT_BASE /* test if we run from flash or RAM */
  5. cmp r0, r1 /* don't reloc during debug */
  6. beq stack_setup

  7. ldr r2, _armboot_start
  8. ldr r3, _bss_start
  9. sub r2, r3, r2 /* r2 - size of armboot */
  10. add r2, r0, r2 /* r2 - source end address */

  11. copy_loop:
  12. ldmia r0!, {r3-r10} /* copy from source address [r0] */
  13. stmia r1!, {r3-r10} /* copy to target address [r1] */
  14. cmp r0, r2 /* until source end addreee [r2] */
  15. ble copy_loop
  16. #endif /* CONFIG_SKIP_RELOCATE_UBOOT */
复制代码
上面的代码中有不少没有见过的符号,还记得开始的时候我们跳过的那段代码吗?现在说明一下,那是(全局)变量的定义。
  1. _TEXT_BASE:
  2. .word TEXT_BASE /* _TEXT_BASE的值被定义为TEXT_BASE,而TEXT_BASE的定义在ubootLDS文件中,其默认值为0x33f80000 */

  3. .globl _armboot_start
  4. _armboot_start:
  5. .word _start /* 你可以把_armboot_start和_start理解为一回事 */

  6. /*
  7. * These are defined in the board-specific linker script.
  8. */
  9. .globl _bss_start
  10. _bss_start:
  11. .word __bss_start

  12. .globl _bss_end
  13. _bss_end:
  14. .word _end
复制代码
bss段放的是未初始化的全局变量。正如英文注释所说的,__bss_start和_end是链接的时候才确定的值,咱就甭操心了。只需要知道它们分别是bss段的起始和结束地址即可。
  1. #ifdef CONFIG_USE_IRQ
  2. /* IRQ stack memory (calculated at run-time) */
  3. .globl IRQ_STACK_START
  4. IRQ_STACK_START:
  5. .word 0x0badc0de

  6. /* IRQ stack memory (calculated at run-time) */
  7. .globl FIQ_STACK_START
  8. FIQ_STACK_START:
  9. .word 0x0badc0de
  10. #endif
复制代码
正如英文注释所说的,IRQ_STACK_START和FIQ_STACK_START是运行时确定的值,咱也甭操心了。0x0badc0de这个数字是哪块地址?别揣摩了,那只是一个单词……
CONFIG_SKIP_RELOCATE_UBOOT这个宏,在我们这种情况下,是没有被定义的,也就是说,宏里面的代码会被执行。啥时候会被定义呢,比如u-boot已经被人加载到内存的时候,这段代码就不需要了。嗯,反应快的人已经猜到了,这段代码是将u-boot拷贝到内存的。

这段代码因为涉及到编译链接时的地址重定位问题,分析源码比较绕,让我们突破迷雾,直击本质,看看最后生成的u-boot文件中的汇编代码吧。命令:
  1. [u-boot-1.1.6 #]arm-linux-objdump -d u-boot | head -n 100
复制代码
找到 relocate这段:
  1. 33f80094 relocate:
  2. 33f80094: e24f009c sub r0, pc, #156 ; 0x9c
  3. 33f80098: e51f1060 ldr r1, [pc, #-96] ; 33f80040 _TEXT_BASE
  4. 33f8009c: e1500001 cmp r0, r1
  5. 33f800a0: 0a000007 beq 33f800c4 stack_setup
  6. 33f800a4: e51f2068 ldr r2, [pc, #-104] ; 33f80044 _armboot_start
  7. 33f800a8: e51f3068 ldr r3, [pc, #-104] ; 33f80048 _bss_start
  8. 33f800ac: e0432002 sub r2, r3, r2
  9. 33f800b0: e0802002 add r2, r0, r2

  10. 33f800b4  copy_loop:
  11. 33f800b4: e8b007f8 ldm r0!, {r3, r4, r5, r6, r7, r8, r9, sl}
  12. 33f800b8: e8a107f8 stmia r1!, {r3, r4, r5, r6, r7, r8, r9, sl}
  13. 33f800bc: e1500002 cmp r0, r2
  14. 33f800c0: dafffffb ble 33f800b4 copy_loop
复制代码
第一句,r0=pc-#156=(0×94+8)-#156=0;
第二句,r1=[pc, #-96]=[(0x98+8)-#96]=[0x40],而地址[40]处定义的值为0x33f80000(arm-linux-objdump -d cpu/arm920t/start.o可知).即r1=0x33f80000.

没看懂?觉得pc的值是33f800xx才对?我来解释一下。
首先要说明,链接器是尽职尽责的,程序员在configMK中定义了TEXT_BASE = 0x33F80000,即指定了代码段基址从0x33F80000开始,连接器认为该程序会被加载到0x33F80000处执行,于是很认真的对指令进行了地址重定位,所以u-boot文件中的指令地址都是大于0x33F80000的(有兴趣的arm-linux-objdump一下编译生成的”u-boot”文件),但,这不意味着pc寄存器就一定是0x33F8xxxx,这取决于程序实际从哪里启动。
按照预期,既然开发人员定义了TEXT_BASE,按理说应该加载到定义的地址后去运行,但是开发人员可能不按套路出牌,实际加载到哪个地址运行随具体情况而定。在我们的分析中,很不幸的,程序被烧写(加载)到norflash中并从这里开始运行,而norflash的起始地址为0×0,所以,程序刚开始是从0×0地址开始运行的。因此,运行到我们分析的这片代码时,pc的实际值是个很小的数,就像我分析的那样。

第三句,比较r0和r1是否相同,相同的话说明u-boot已经被加载到TEXT_BASE指定的地址了,否则,就把u-boot加载(拷贝)到TEXT_BASE指定的地址。

开始拷贝?不急。拷贝前还有些准备工作要做。拷贝有3要素:源地址、目标地址、要拷贝的数据长度。源地址r2=[pc, #-104]=[0xa4+8-#104]=[0xac-#104]=[0x44],而地址[40]处定义的值为0×0.即r2=0×0.目标地址r1=0x33f80000,刚才分析过的。拷贝哪些东西呢?text段和bss段之间的内容,所以数据长度=BSS段起始地址-CS段起始地址=0x33f96938-0x33f80000=0×16938=大概90KB的样子。

现在可以开始了。从 copy_loop起的那段就是拷贝的代码,一个循环搬运的过程,没什么好说的。

拷贝完成后,痛苦的汇编代码即将结束,因为程序要飞到c语言编写的代码去啦,以后就好分析多了。

8)收尾工作往往令人厌倦,比如吃完饭后洗碗、打完球后刷鞋之类的。但收尾工作还是要做的,因为它也是下一阶段工作的开始。首尾工作分为两块,一个是为c语言中函数调用设置栈空间;另一个是清零bss段。
  1. /* Set up the stack */
  2. stack_setup:
  3. ldr r0, _TEXT_BASE /* upper 128 KiB: relocated u-boot */
  4. sub r0, r0, #CFG_MALLOC_LEN /* malloc area */
  5. sub r0, r0, #CFG_GBL_DATA_SIZE /* bdinfo */
  6. #ifdef CONFIG_USE_IRQ
  7. sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
  8. #endif
  9. sub sp, r0, #12 /* leave 3 words for abort-stack */
复制代码
说起来也简单,就是告诉处理器哪块内存可以当栈用而已。通过计算把栈的起始位置定下来,然后把这个地址赋值给sp寄存器。
首先r0=0x33f80000,然后去掉堆占用的空间,再去掉开发板所用数据结构占用的空间,如果使用IRQ,还要把IRQ用得空间也去掉。最后赋值给sp寄存器。显然,这个栈是向下增长的。
那么,CFG_MALLOC_LEN、CFG_GBL_DATA_SIZE、CONFIG_STACKSIZE、CONFIG_STACKSIZE_FIQ都是哪里定义的?答案尽在smdk2410H中。

有不少人思考问题很严谨,于是有人要问了,刚才的代码说是为c语言中函数调用设置栈空间,那么本篇开始时,有个c函数do_undefined_instruction的调用为什么不需要设置栈空间?
问得好。我们看看objdump后的uboot汇编代码:
  1. 33f80020 _undefined_instruction:
  2. 33f80020: 33f80140 .word 0x33f80140
  3. ...
  4. 33f80140 undefined_instruction:
  5. 33f80140: e51fd104 ldr sp, [pc, #-260] ; 33f80044 _armboot_start
  6. 33f80144: e24dd805 sub sp, sp, #327680 ; 0x50000
  7. 33f80148: e24dd088 sub sp, sp, #136 ; 0x88
  8. ...
复制代码
栈的本质作用是为了保护现场,把待会要用到的数据放在一块内存里–当然,这块内存不能存在有用的数据,否则就被抹掉了,会发生不可预料的后果。可见,上面的代码里其实也是指定了栈的位置的,确切的说,是随便指定了一个位置,位置在[33f80044]-0x33f80000-0×50000-0×88。考虑到uboot启动时,系统处于混沌状态,这样的临时设置,破坏有用数据的可能性基本上没有,除非u-boot是被别的程序加载到内存中的,而那个加载u-boot的程序恰好用了那块内存,那真是躺着也中枪了…
因为bss段放的是未初始化的全局变量,所以,把这块内存初始化为0, 其实就是循环赋值0。
  1. clear_bss:
  2. ldr r0, _bss_start /* find start of bss segment */
  3. ldr r1, _bss_end /* stop here */
  4. mov r2, #0x00000000 /* clear */

  5. clbss_l:str r2, [r0] /* clear loop... */
  6. add r0, r0, #4
  7. cmp r0, r1
  8. ble clbss_l
复制代码
9)看到丛林的出口啦!
  1. ldr pc, _start_armboot
复制代码
程序跳转到_start_armboot。查找_start_armboot,在当前文件中发现其定义:_start_armboot: .word start_armboot,搜索start_armboot,在boardC文件中:void start_armboot (void)。C语言的函数!一阵感动哇。。。从汇编的暗黑世界里挣扎出来,发现c语言太可爱了。

至此,汇编语言部分(也就是传说中的stage1)分析完毕,c语言的大门就在眼前,我脑中突然出现了游戏《暗黑2》联网时的过场:一道充满光芒的门徐徐打开……

(全文完)
  1. 作者:HateMath
  2. 原创首发于博客 “a HateMath Driver”
  3. 转载请注明出处:(http://hatemath.sinaapp.com/)
复制代码

论坛徽章:
0
2 [报告]
发表于 2012-06-26 16:22 |只看该作者
终于把格式编辑完了。文章有点长,不知有没有人能看完。

论坛徽章:
0
3 [报告]
发表于 2012-07-01 08:42 |只看该作者
昨天才把这个程序看两遍,也看了些分析资料,问一下对于具体的板子lowlevel_init做了些什么工作

论坛徽章:
0
4 [报告]
发表于 2012-08-31 00:04 |只看该作者
好文章,楼主辛苦了。呵呵。恭喜暗黑2通关。
我们还在不停砍怪,积累EXP中,感谢给出秘籍。
吃BOSS的方法已经给出,不过总是操作不好,容易被秒杀。

希望楼主有时间继续跟进暗黑3的通关方法。谢谢了!

哈哈 只能说嵌入式很神奇,要说抽大烟的上瘾,还真就别说,没有嵌入式这“海洛因”厉害!后劲儿大啊!!
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP