X86汇编语言学习_第1页
X86汇编语言学习_第2页
X86汇编语言学习_第3页
X86汇编语言学习_第4页
已阅读5页,还剩12页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、X86 汇编语言学习手记X86 汇编语言学习手记 (1)1. 编译环境OS: Solaris 9 X86Compiler: gcc 3.3.2Linker: Solaris Link Editors 5.xDebug Tool: mdbEditor: vi注:关于编译环境的安装和设置,可以参考文章:Solaris上的开发环境安装及设置。mdb 是 Solaris提供的 kerneldebug 工具,这里用它做反汇编和汇编语言调试工具。如果在 Linux 平台可以用gdb 进行反汇编和调试。2. 最简 C代码分析为简化问题,来分析一下最简的c 代码生成的汇编代码:# vi test1.cint

2、main()return 0;编译该程序,产生二进制文件:# gcc test1.c -o test1# file test1test1: ELF 32-bit LSB executable 80386 Version 1, dynamically linked, notstrippedtest1是一个 ELF 格式 32 位小端 (Little Endian)的可执行文件,动态链接并且符号表没有去除。这正是 Unix/Linux平台典型的可执行文件格式。用 mdb反汇编可以观察生成的汇编代码:# mdb test1Loading modules: libc.so.1 > main:di

3、s;反汇编main函数, mdb的命令一般格式为< 地址 >:dismain:pushl%ebp; ebp寄存器内容压栈,即保存main函数的上级调用函数的栈基地址main+1:movl%esp,%ebp; esp值赋给ebp,设置main函数的栈基址main+3:main+6:main+9:main+0xe:main+0x10:main+0x15:movlleavesublandlmovlsubl$8,%esp$0xf0,%esp$0,%eax%eax,%esp$0,%eax;设置函数返回值0;将 ebp 值赋给esp,pop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址ma

4、in+0x16:ret; main函数返回,回到上级调用>注:这里得到的汇编语言语法格式与 Intel 的手册有很大不同, Unix/Linux 采用 AT&T 汇编格式作为汇编语言的语法格式如果想了解AT&T汇编可以参考文章:Linux AT&T汇编语言开发指南问题:谁调用了main函数?在 C 语言的层面来看,main 函数是一个程序的起始入口点,而实际上,行文件的入口点并不是main 而是 _start。mdb 也可以反汇编_start:ELF可执> _start:dis; 从 _start的地址开始反汇编_start:pushl$0_start+2:

5、_start+4:_start+6:_start+7:pushlmovlpushlmovl$0%esp,%ebp%edx$0x80504b0,%eax_start+0xc:testl%eax,%eax_start+0xe:je+0xf<_start+0x1d>_start+0x10:pushl$0x80504b0_start+0x15:call-0x75<atexit>_start+0x1a:addl$4,%esp_start+0x1d:movl$0x8060710,%eax_start+0x22:testl%eax,%eax_start+0x24:je+7<_s

6、tart+0x2b>_start+0x26:call-0x86<atexit>_start+0x2b:pushl$0x80506cd_start+0x30:call-0x90<atexit>_start+0x35:movl+8(%ebp),%eax_start+0x38:leal+0x10(%ebp,%eax,4),%edx_start+0x3c:movl%edx,0x8060804_start+0x42:andl$0xf0,%esp_start+0x45:subl$4,%esp_start+0x48:pushl%edx_start+0x49:leal+0xc(%

7、ebp),%edx_start+0x4c:pushl%edx_start+0x4d:pushl%eax_start+0x4e:call+0x152<_init>_start+0x53:call-0xa3<_fpstart>_start+0x58:call+0xfb<main> 在这里调用了main 函数_start+0x5d:addl$0xc,%esp_start+0x60:pushl%eax_start+0x61:call-0xa1<exit>_start+0x66:pushl$0_start+0x68:movl$1,%eax_start+0x

8、6d:lcall$7,$0_start+0x74:hlt>问题:为什么用EAX寄存器保存函数返回值?实际上 IA32 并没有规定用哪个寄存器来保存返回值。但如果反汇编的二进制文件,就会发现,都用EAX保存函数返回值。这不是偶然现象,是操作系统的ABI(Application Binary Interface)Solaris/Linux操作系统的ABI 就是 Sytem V ABI 。Solaris/Linux来决定的。概念: SFP (Stack Frame Pointer)栈框架指针正确理解SFP必须了解:IA32的栈的概念CPU 中 32 位寄存器ESP/EBP的作用PUSH/POP

9、 指令是如何影响栈的CALL/RET/LEAVE 等指令是如何影响栈的如我们所知:1)IA32的栈是用来存放临时数据,而且是 LIFO,即后进先出的。 栈的增长方向是从高地址向低地址增长,按字节为单位编址。2) EBP 是栈基址的指针,永远指向栈底(高地址),ESP是栈指针,永远指向栈顶(低地址)。3) PUSH一个 long 型数据时,以字节为单位将数据压入栈,从高到低按字节依次将数据存入 ESP-1、 ESP-2、 ESP-3、 ESP-4 的地址单元。4) POP一个 long 型数据,过程与 PUSH相反,依次将 ESP-4、ESP-3、ESP-2、 ESP-1从栈内弹出,放入一个32

10、 位寄存器。5) CALL 指令用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复执行下条指令。6) RET 指令用来从一个函数或过程返回,之前CALL保存的下条指令地址会从栈内弹出到 EIP 寄存器中,程序转到CALL之前下条指令处执行7) ENTER 是建立当前函数的栈框架,即相当于以下两条指令:pushl%ebpmovl%esp,%ebp8) LEAVE 是释放当前函数或者过程的栈框架,即相当于以下两条指令: movl ebp esppoplebp如果反汇编一个函数,很多时候会在函数进入和返回处,发现有类似如下形式的汇编语句:pushl%ebp;ebp 寄存器内容

11、压栈,即保存main函数的上级调用函数的栈基地址movl%esp,%ebp; esp值赋给ebp,设置main函数的栈基址.;以上两条指令相当于enter0,0.leave;将 ebp 值赋给esp,pop先前栈内的上级函数栈的基地址给ebp,恢复原栈基址ret;main 函数返回,回到上级调用这些语句就是用来创建和释放一个函数或者过程的栈框架的。原来编译器会自动在函数入口和出口处插入创建和释放栈框架的语句。函数被调用时:1) EIP/EBP 成为新函数栈的边界函数被调用时,返回时的EIP 首先被压入堆栈;创建栈框架时,上级函数栈的EBP被压入堆栈,与EIP 一道行成新函数栈框架的边界2) E

12、BP 成为栈框架指针 SFP,用来指示新函数栈的边界栈框架建立后,EBP指向的栈的内容就是上一级函数栈的EBP,可以想象,通过EBP就可以把层层调用函数的栈都回朔遍历一遍,调试器就是利用这个特性实现 backtrace 功能的3) ESP 总是作为栈指针指向栈顶,用来分配栈空间栈分配空间给函数局部变量时的语句通常就是给一个整型数据就是ESP-44)函数的参数传递和局部变量访问可以通过ESP减去一个常数值,例如,分配SFP即 EBP来实现由于栈框架指针永远指向当前函数的栈基地址,参数和局部变量访问通常为如下形式:+8+xx(%ebp)-xx(%ebp);函数入口参数的的访问;函数局部变量访问假如

13、函数A 调用函数B,函数 B 调用函数C ,则函数栈框架及调用关系如下图所示:b:771101bbb0下图有点乱, 因此删去部分内容, 要看原图可参考我的blog/b:771101bbb0+-+->高地址|EIP(上级函数返回地址 )|+-+|EBP(上级函数的 EBP)|+-+| Local Variables| .|+-+| Arg n(函数 B 的第 n 个参数 ) |+-+| Arg .(函数 B的第.个参数 )|+-+| Arg 1(函数 B的第 1个参数 )|+-+| Arg 0(函数 B的第 0个参数 )|+-+EIP (A 函数的返回地址 )|+-+| EBP (A函数的

14、 EBP)|+-+| Local Variables| .|+-+| Arg n(函数 C 的第 n 个参数 ) |+-+| Arg .(函数 C的第 . 个参数 )|+-+| Arg 1(函数 C的第 1个参数 )|+-+| Arg 0(函数 C的第 0个参数 )|+-+| EIP (B函数的返回地址 )|+-+| EBP (B函数的 EBP)+-+| Local Variables| .|+-+->低地址图1-1再分析 test1反汇编结果中剩余部分语句的含义:# mdb test1Loading modules: libc.so.1 > main:dis;反汇编main函数m

15、ain:pushl%ebpmain+1:movl%esp,%ebp;创建StackFrame( 栈框架 )main+3:subl$8,%esp;通过ESP-8 来分配8字节堆栈空间main+6:main+9:andlmovl$0xf0,%esp$0,%eax;使栈地址;无意义16 字节对齐main+0xe:subl%eax,%esp;无意义main+0x10:movl$0,%eax;设置main函数返回值main+0x15:leave;撤销Stack Frame(栈框架)main+0x16:ret; main函数返回>以下两句似乎是没有意义的,果真是这样吗?movl$0,%eaxsubl

16、%eax,%esp用 gcc 的 O2级优化来重新编译 test1.c:# gcc -O2 test1.c -o test1# mdb test1> main:dismain:pushl%ebpmain+1:movl%esp,%ebpmain+3:subl$8,%espmain+6:andl$0xf0,%espmain+9:xorl%eax,%eax; 设置 main 返回值,使用 xorl 异或指令来使 eax 为 0main+0xb:leavemain+0xc:ret>新的反汇编结果比最初的结果要简洁一些, 果然之前被认为无用的语句被优化掉了,进一步验证了之前的猜测。提示:编译

17、器产生的某些语句可能在程序实际语义上没有用处,可以用优化选项去掉这些语句。问题:为什么用xorl来设置 eax 的值?注意到优化后的代码中,eax 返回值的设置由xorl %eax,%eax,这是因为IA32 指令中, xorl比movl $0,%eax变为movl 有更高的运行速度。概念: Stack aligned栈对齐那么,以下语句到底是和作用呢?subl$8,%espandl$0xf0,%esp;通过andl使低4 位为0,保证栈地址16字节对齐表面来看,这条语句最直接的后果是使ESP的地址后4 位为 0,即 16 字节对齐,那么为什么这么做呢?原来, IA32 系列 CPU的一些指令

18、分别在4、8、16 字节对齐时会有更快的运行速度,因此 gcc 编译器为提高生成代码在IA32 上的运行速度, 默认对产生的代码进行16 字节对齐andl $0xf0,%esp的意义很明显,那么subl $8,%esp呢,是必须的吗?这里假设在进入main 函数之前,栈是16 字节对齐的话,那么,进入main 函数后,EIP 和 EBP被压入堆栈后,栈地址最末4 位二进制位必定是1000 , esp -8则恰好使后4 位地址二进制位为0000。看来,这也是为保证栈16 字节对齐的。如果查一下gcc 的手册,就会发现关于栈对齐的参数设置:-mpreferred-stack-boundary=n;

19、希望栈按照2 的n 次的字节边界对齐, n的取值范围是2-12IA32默认情况下, n 是等于大多数指令的要求。4 的,也就是说,默认情况下,gcc是16 字节对齐,以适应让我们利用-mpreferred-stack-boundary=2来去除栈对齐指令:# gcc -mpreferred-stack-boundary=2 test1.c -o test1> main:dismain:pushl%ebpmain+1:movl%esp,%ebpmain+3:movl$0,%eaxmain+8:leavemain+9:ret>可以看到,栈对齐指令没有了,因为,IA32 的栈本身就是4

20、字节对齐的,不需要用额外指令进行对齐。那么,栈框架指针SFP是不是必须的呢?# gcc -mpreferred-stack-boundary=2 -fomit-frame-pointer test1.c -o test > main:dismain:movl$0,%eaxmain+5:ret>由此可知, -fomit-frame-pointer可以去除SFP。问题:去除SFP后有什么缺点呢?1) 增加调式难度由于 SFP 在调试器 backtrace的指令中被使用到,因此没有SFP该调试指令就无法使用。2) 降低汇编代码可读性函数参数和局部变量的访问,在没有的方式访问,而很难区分两

21、种方式,降低了程序的可读性。ebp 的情况下,都只能通过+xx(esp)问题:去除SFP有什么优点呢?1) 节省栈空间2) 减少建立和撤销栈框架的指令后,简化了代码3) 使 ebp 空闲出来,使之作为通用寄存器使用,增加通用寄存器的数量4) 以上 3 点使得程序运行速度更快概念:CallingConvention调用约定和ABI (ApplicationBinaryInterface)应用程序二进制接口函数如何找到它的参数?函数如何返回结果?函数在哪里存放局部变量?那一个硬件寄存器是起始空间?那一个硬件寄存器必须预先保留?Calling Convention调用约定对以上问题作出了规定。Cal

22、ling Convention也是 ABI 的一部分。因此,遵守相同ABI 规范的操作系统,使其相互间实现二进制代码的互操作成为了可能。行Linux例如:由于Solaris二进制程序的功能。、Linux都遵守System V的 ABI, Solaris 10就提供了直接运详见文章:关注:Solaris 10的 10 大新变化3. 小结本文通过最简的C 程序,引入以下概念:SFP 栈框架指针Stack aligned栈对齐CallingConvention调用约定和 ABI (ApplicationBinaryInterface)应用程序二进制接口今后,将通过进一步的实验,来深入了解这些概念。通

23、过掌握这些概念,使在汇编级调试程序产生的 core dump 、掌握 C语言高级调试技巧成为了可能。X86 汇编语言学习手记 (2)这是作者在学习X86 汇编过程中的学习笔记,难免有错误和疏漏之处,欢迎指正。 作者将随时修改错误并将新的版本发布在自己的 Blog 站点上。严格说来,本篇文档更侧重于 C 语言和 C 编译器方面的知识,如果涉及到基本的汇编语言的内容,可以参考相关文档。自 X86 汇编语言学习手记 (1) 在作者的 Blog 上发布以来,得到了很多网友的肯定和鼓励,并且还有热心网友指出了其中的错误, b:bea66ddae0 作者已经将文档中已发现的错误修正后更新在 Blog 上。

24、 /b:bea66ddae0上一篇文章通过分析一个最简的C 程序,引出了以下概念:Stack FrameStack aligned栈框架和 SFP 栈框架指针栈对齐CallingConvention调用约定和 ABI (ApplicationBinaryInterface)应用程序二进制接口本章中,将通过进一步的实验,来深入了解这些概念。如果还不了解这些概念,可以参考 X86 汇编语言学习手记 (1) 。1. 局部变量的栈分配上篇文章已经分析过一个最简的C 程序,下面我们分析一下C 编译器如何处理局部变量的分配,为此先给出如下程序:#vi test2.cint main()int i;int

25、j=2;i=3;i=+i;return i+j;编译该程序,产生二进制文件,并利用mdb来观察程序运行中的stack 的状态:#gcc test2.c -o test2#mdb test2Loading modules: libc.so.1 > main:dismain:pushl%ebpmain+1:movl%esp,%ebp; main 至main+1,创建 Stack Framemain+3:subl$8,%esp; 为局部变量 i,j 分配栈空间,并保证栈16 字节对齐main+6:andl$0xf0,%espmain+9:movl$0,%eaxmain+0xe:subl%eax

26、,%esp; main+6 至main+0xe ,再次保证栈 16字节对齐main+0x10:movl$2,-8(%ebp);初始化局部变量 j 的值为 2main+0x17:movl$3,-4(%ebp);给局部变量 i赋值为 3main+0x1e:leal-4(%ebp),%eax;将局部变量 i 的地址装入到 EAX寄存器中main+0x21:incl(%eax); i+main+0x23:movl-8(%ebp),%eax;将 j 的值装入EAXmain+0x26:addl-4(%ebp),%eax; i+j并将结果存入 EAX,作为返回值main+0x29:leave; 撤销Stac

27、k Framemain+0x2a:ret;main 函数返回>> main+0x10:b;在地址 main+0x10处设置断点> main+0x1e:b;在 main+0x1e 设置断点> main+0x29:b;在 main+0x1e 设置断点> main+0x2a:b;在 main+0x1e 设置断点下面的 mdb的 4 个命令在一行输入,中间用分号间隔开,命令的含义在注释中给出:> :r;<esp,10/nap;<ebp=X;<eax=X;运行程序 (:r命令 )mdb: stopat main+0x10;以 ESP寄存器为起始地址,

28、指定格式输出16 字节的栈内容(<esp,10/nap命令 )mdb: target stopped at:;在最后输出EBP和EAX寄存器的值(<ebp=X命令 和 <eax=X命令 )main+0x10:movl$2,-8(%ebp);程序运行后在main+0x10 处指令执行前中断,此时栈分配后还未初始化0x8047db0:0x8047db0:0xddbebca0;这是变量 j , 4字节,未初始化,此处为栈顶,ESP的值就是 0x8047db00x8047db4:0xddbe137f;这是变量 i , 4字节,未初始化0x8047db8:0x8047dd8;这是 _s

29、tart的SFP(_start 的 EBP),4 字节,由main 的 SFP指向它0x8047dbc:_start+0x5d;这是 _start调用main 之前压栈的下条指令地址,main 返回后将恢复给EIP0x8047dc0:10x8047dc4:0x8047de40x8047dc8:0x8047dec0x8047dcc:_start+0x350x8047dd0:_fini0x8047dd4:ld.so.1atexit_fini0x8047dd8:0;_start 的 SFP指向的内容为0,证明 _start 是程序的入口0x8047ddc:00x8047de0:10x8047de4:

30、0x8047eb40x8047de8:00x8047dec:0x8047eba8047db8;这是main 当前 EBP寄存器的值,即main 的 SFP0;EAX的值,当前为0> :c;<esp,10/nap;<ebp=X;<eax=X;继续运行程序 (:c命令 ) ,其余 3 命令同上,打印16 字节栈和EBP,EAX内容mdb: stop at main+0x1emdb: target stopped at:main+0x1e:leal-4(%ebp),%eax;程序运行到断点main+0x1e 处停止,此时局部变量i,j赋值已完成0x8047db0:0x8047

31、db0:2;这是变量 j , 4 字节,值为2,此处为栈顶, ESP的值就是 0x8047db00x8047db4:3;这是变量 i , 4 字节,值为 30x8047db8:0x8047dd8;这是 _start 的SFP, 4 字节0x8047dbc:_start+0x5d;这是返回 _start后的 EIP0x8047dc0:10x8047dc4:0x8047de40x8047dc8:0x8047dec0x8047dcc:_start+0x350x8047dd0:_fini0x8047dd4:ld.so.1atexit_fini0x8047dd8:00x8047ddc:00x8047de

32、0:10x8047de4:0x8047eb40x8047de8:00x8047dec:0x8047eba8047db8;这是main 当前 EBP寄存器的值,即main 的 SFP0;EAX的值,当前为 0> :c;<esp,10/nap;<ebp=X;<eax=X; 继续运行程序,打印16 字节栈和EBP,EAX内容mdb: stop at main+0x29mdb: target stopped at:main+0x29:leave; 运行到断点 main+0x29 处停止,计算已经完成,即将撤销Stack Frame0x8047db0:0x8047db0:2;这是

33、变量 j , 4 字节,值为 2,此处为栈顶, ESP的值就是 0x8047db00x8047db4:4;这是i+ 以后的变量 i , 4 字节,值为 30x8047db8:0x8047dd8; 这是 _start 的SFP, 4 字节0x8047dbc:_start+0x5d;这是返回 _start后的 EIP0x8047dc0:10x8047dc4:0x8047de40x8047dc8:0x8047dec0x8047dcc:_start+0x350x8047dd0:_fini0x8047dd4:ld.so.1atexit_fini0x8047dd8:00x8047ddc:00x8047de

34、0:10x8047de4:0x8047eb40x8047de8:00x8047dec:0x8047eba8047db8;这是main 当前 EBP寄存器的值,即main 的 SFP6;EAX的值,即函数的返回值,当前为6> :c;<esp,10/nap;<ebp=X;<eax=X;继续运行程序,打印16 字节栈和EBP,EAX内容mdb: stop at main+0x2amdb: target stopped at:main+0x2a:ret; 运行到断点main+0x2a 处停止, Stack Frame 已被撤销, main 即将返回0x8047dbc:0x804

35、7dbc:_start+0x5d; Stack Frame 已经被撤销,栈顶是返回_start后的 EIP ,main 的栈已被释放0x8047dc0:10x8047dc4:0x8047de40x8047dc8:0x8047dec0x8047dcc:_start+0x350x8047dd0:_fini0x8047dd4:ld.so.1atexit_fini0x8047dd8:00x8047ddc:00x8047de0:10x8047de4:0x8047eb40x8047de8:00x8047dec:0x8047eba0x8047df0:0x8047ed60x8047df4:0x8047edd0

36、x8047df8:0x8047ee48047dd8; _start 的SFP,之前存储在地址0x8047db8 处, main 的 Stack Frame撤销时恢复6; EAX的值,即函数的返回值,当前为6> :s;<esp,10/nap;<ebp=X;<eax=X; 单步执行下条指令 (:s命令 ) ,打印 16字节栈和 EBP,EAX内容mdb: target stopped at:_start+0x5d:addl$0xc,%esp; 此时 main 已经返回,_start+0x5d 曾经存储在地址0x8047dbc 处0x8047dc0:0x8047dc0:1;

37、main已经返回, _start +0x5d已经被弹出0x8047dc4:0x8047de40x8047dc8:0x8047dec0x8047dcc:_start+0x350x8047dd0:_fini0x8047dd4:ld.so.1atexit_fini0x8047dd8:0;_start 的 SFP指向的内容为0,证明 _start是程序的入口0x8047ddc:00x8047de0:10x8047de4:0x8047eb40x8047de8:00x8047dec:0x8047eba0x8047df0:0x8047ed60x8047df4:0x8047edd0x8047df8:0x804

38、7ee40x8047dfc:0x8047ef38047dd8; _start 的SFP,之前存储在地址0x8047db8 处, main 的 Stack Frame 撤销时恢复6; EAX的值为 6,还是 main 函数的返回值>通过 mdb对程序运行时的寄存器和栈的观察和分析,可以得出局部变量在栈中的访问和分配及释放方式:1. 局部变量的分配,可以通过esp 减去所需字节数subl$8,%esp2. 局部变量的释放,可以通过leave 指令leave3. 局部变量的访问,可以通过ebp 减去偏移量movl-8(%ebp),%eaxaddl-4(%ebp),%eax问题:当存在2 个以上

39、的局部变量时,如何进行栈对齐?在上篇文章中,提到subl $8,%esp 语句除了分配栈空间外,还有一个作用就是栈对齐。那么本例中,由于 i 和 j 正好是 8 字节,那么如果存在 2 个以上的局部变量时,如何同时满足空间分配和栈对齐呢?2. 两个以上的局部变量的栈分配在之前的C程序中,增加局部变量定义k,程序如下:# vi test3.cint main()int i, j=2, k=4;i=3;i=+i;k=i+j+k;return k;编译该程序后,用mdb反汇编得出如下结果:# gcc test3.c -o test3# mdb test3Loading modules: libc.so.1 > main:dismain:pushl%ebpmain+1:movl%esp,%ebp; main至 main+1,创建 Stack Framemain+3:subl$0x18,%esp;为局部变量 i,j,k分配栈空间,并保证栈16 字节对齐main+6:andl$0xf0,%espmain+9:movl$0,%eaxmain+0xe:subl%eax,%esp; main+6至 main+0xe ,再次保证栈16 字节对齐main+0x10:movl$2,-8(%ebp);j=2main+0x17:movl$4,-0xc(%ebp);k=4main+0x1e:movl$

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论