您的位置:首页 > 其它

函数调用约定收藏

2008-11-02 21:01 344 查看

 函数调用约定收藏

新一篇: 函数名修饰约定规则 | 旧一篇: 构建自己的操作系统 1

function StorePage(){d=document;t=d.selection?(d.selection.type!='None'?d.selection.createRange().text:''):(d.getSelection?d.getSelection():'');void(keyit=window.open('http://www.365key.com/storeit.aspx?t='+escape(d.title)+'&u='+escape(d.location.href)+'&c='+escape(t),'keyit','scrollbars=no,width=475,height=575,left=75,top=20,status=no,resizable=yes'));keyit.focus();}



[b]一:函数调用约定:[/b]
函数调用约定是函数调用者和被调用的函数体之间关于参数传递、返回值传递、堆栈清除、寄存器使用的一种约定; 它是需要二进制级别兼容的强约定,函数调用者和函数体如果使用不同的调用约定,将可能造成程序执行错误,必须把它看作是函数声明的一部分;

参考文章:

 http://liue.spaces.live.com/blog/cns!d126ff4c28b17ad1!237.entry 

 http://dev.csdn.net/article/52/52485.shtm

二:常见的调用约定:

VC6中的函数调用约定;

调用约定   堆栈清除    参数传递
__cdecl     调用者       从右到左,通过堆栈传递
__stdcall    函数体      从右到左,通过堆栈传递
__fastcall   函数体      从右到左,优先使用寄存器(ECX,EDX) , 然后使用堆栈
thiscall       函数体       this指针默认通过 ECX 传递, 其它参数从右到左入栈

__cdecl 是 C/C++ 的默认调用约定; VC 的调用约定中并没有 thiscall 这个关键字,它是类成员函数默认调用约定;
C/C++中的main(或wmain)函数的调用约定必须是 __cdecl , 不允许更改;

默认调用约定一般能够通过编译器设置进行更改,如果你的代码依赖于调用约定,请明确指出需要使用的调用约定;

Delphi6中的函数调用约定;

调用约定  堆栈清除   参数传递
register     函数体        从左到右, 优先使用寄存器( EAX , EDX , ECX ),然后使用堆栈
pascal      函数体        从左到右, 通过堆栈传递
cdecl        调用者        从右到左, 通过堆栈传递(与C/C++默认调用约定兼容)
stdcall      函数体        从右到左, 通过堆栈传递(与VC中的__stdcall兼容)
safecall     函数体       从右到左, 通过堆栈传递(同stdcall)

Delphi中的默认调用约定是 register , 它也是我认为最有效率的一种调用方式, 而cdecl是我认为综合效率最差的一种调用方式;
VC中的__fastcall调用约定一般比register效率稍差一些;

C++Builder6中的函数调用约定;

调用约定 堆栈清除 参数传递
__fastcall     函数体       从左到右,优先使用寄存器(EAX,EDX,ECX), 然后使用堆栈 (兼容Delphi的register)
                                    (register与__fastcall等同)
__pascal      函数体       从左到右,通过堆栈传递
__cdecl        调用者       从右到左,通过堆栈传递(与C/C++默认调用约定兼容)
__stdcall      函数体       从右到左,通过堆栈传递(与VC中的__stdcall兼容)
__msfastcall 函数体       从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈(兼容VC的__fastcall)

VB一般使用的是stdcall调用约定;(ps:有更强的保证吗)  

A:  测试代码:

int x;
int __cdecl add(int a,int b) { return a+b; }//使用__cdecl调用约定
int main(int argc, char* argv[])
{
      x=add(1,2);
    return 0;
}

; Debug模式编译后得到的汇编代码

PUBLIC ?x@@3HA ; x
_BSS SEGMENT
?x@@3HA DD 01H DUP (?) ; x 变量
_BSS ENDS
PUBLIC ?add@@YAHHH@Z ; add
PUBLIC _main
EXTRN __chkesp:NEAR
; COMDAT _main
_TEXT SEGMENT

_main PROC NEAR ; COMDAT //main函数体

push ebp          ; //保存ebp的值到堆栈,退出函数前用pop ebp恢复
mov ebp, esp   ; //ebp指向当前堆栈; 函数中可以通过ebp来进行堆栈访问
sub esp, 64      ; //在堆栈中开辟64byte局部空间

; //说明:这三条汇编指令是很多函数体开始的惯用法;
; //用ebp指向堆栈(不会改变);并通过ebp来访问参数和局部变量;  

push ebx ; //一般按照函数间调用的约定,函数中可以自由使用eax,ecx,edx;
push esi ; //其它寄存器如果需要使用则需要保存,用完时恢复;也就是寄存器的使用约定; 这也使函数调用约定的一部分;
push edi ; //即:在函数中调用了别的函数后,eax,ecx,edx很可能已经改变,
                 ; //而其它寄存器(ebx,esi,edi,ebp ) 的值可以放心继续使用( esp除外 )

lea edi, DWORD PTR [ebp-64]
mov ecx, 16 ; 00000010H
mov eax, -858993460 ; ccccccccH
rep stosd ; //前面开辟的( 16*4 ) byte 局部空间全部填充 0xCC
                  ; // 注意: 0xCC 是调试中断(__asm int 3 ) 的指令码 , 所以可以想象,当 
                  ; // 程序错误的跳转到这个区域进行执行时将产生调试中断

push 2                 ; //代码: x=add(1,2);
push 1                  ; //从右到左入栈 (__cdecl调用约定!!!)
call ?add@@YAHHH@Z     ; 调用add函数;  call指令将把下一条指令的地址(返回地址)压入堆栈
add esp, 8                ; add函数调用完以后,调用者负责清理堆栈 (__cdecl调用约定!!!)
                                   ; 两个int型参数共使用了8byte空间的堆栈
mov DWORD PTR ?x@@3HA, eax   ; 将add函数的返回值存入x变量中 , 可以看出add函数的返回值放在eax中
xor eax, eax ;            // 原代码:return 0; 执行eax清零,main函数的返回值0放在eax中

pop edi
pop esi
pop ebx ;           // 恢复edi,esi,ebx寄存器
add esp, 64 ;    // 恢复64byte局部空间
cmp ebp, esp
call __chkesp ; //到这里时应该ebp==esp, Debug版进行确认,如果不等,抛出异常等
mov esp, ebp
pop ebp ;          //恢复ebp寄存器
ret 0
_main ENDP
_TEXT ENDS

;//下面是add函数的代码,就不用解释的像上面那么详细了

; COMDAT ?add@@YAHHH@Z
_TEXT SEGMENT
_a$ = 8          ;// 参数a相对于堆栈偏移8
_b$ = 12       ;// 参数b相对于堆栈偏移12

?add@@YAHHH@Z PROC NEAR ; add, COMDAT //add函数体

push ebp
mov ebp, esp
sub esp, 64 ; 00000040H
push ebx
push esi
push edi
lea edi, DWORD PTR [ebp-64]
mov ecx, 16 ; 00000010H
mov eax, -858993460 ; ccccccccH
rep stosd

mov eax, DWORD PTR _a$[ebp] ;将参数a的值移动到eax
add eax, DWORD PTR _b$[ebp] ;将参数b的值累加到eax; 可以看出返回值通过eax返回

pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0 ; 函数体不管堆栈的参数清理 (__cdecl调用约定!!!)
; ret指令将取出call指令压入的返回地址,并跳转过去继续执行

?add@@YAHHH@Z ENDP ; add
_TEXT ENDS
END

; 再来看一下Release模式编译后得到的汇编代码
; 可以看出,这比Debug模式少了很多的汇编指令,速度当然可能更快了;不再做详细说明了,请对照上面的解释

PUBLIC ?x@@3HA ; x
_BSS SEGMENT
?x@@3HA DD 01H DUP (?) ; x
_BSS ENDS
PUBLIC ?add@@YAHHH@Z ; add
PUBLIC _main
; COMDAT _main
_TEXT SEGMENT

_main PROC NEAR ; COMDAT //main函数体

push 2
push 1 ; //从右到左入栈 (__cdecl调用约定!!!)
call ?add@@YAHHH@Z ; //调用add函数;
mov DWORD PTR ?x@@3HA, eax ; x
add esp, 8 ; //调用者负责清理堆栈 (__cdecl调用约定!!!)

xor eax, eax
ret 0
_main ENDP
_TEXT ENDS

; COMDAT ?add@@YAHHH@Z
_TEXT SEGMENT
_a$ = 8
_b$ = 12

?add@@YAHHH@Z PROC NEAR ; add, COMDAT //add函数体

mov eax, DWORD PTR _b$[esp-4] ;将参数b的值移动到eax
mov ecx, DWORD PTR _a$[esp-4] ;将参数a的值移动到ecx
add eax, ecx ;将ecx的值累加到eax; 返回值通过eax传递
ret 0 ;函数体不管堆栈的参数清理 (__cdecl调用约定!!!)
?add@@YAHHH@Z ENDP ; add
_TEXT ENDS
END

 

下面的分析中将只给出Release模式编译后的汇编代码

B:   声明add函数为__stdcall调用约定

int x;
int __stdcall add(int a,int b) { return a+b; }
int main(int argc, char* argv[])
{
x=add(1,2);
return 0;
}

;来看产生的汇编代码:

; //main函数体
push 2
push 1 ; //从右到左入栈
call ?add@@YGHHH@Z ; add
mov DWORD PTR ?x@@3HA, eax ; x
xor eax, eax
ret 0

; //add函数体
mov eax, DWORD PTR _b$[esp-4]
mov ecx, DWORD PTR _a$[esp-4]
add eax, ecx
ret 8 ; //函数体负责清栈 ;两个int型参数共使用了8byte空间的堆栈

C:  声明add函数为__fastcall调用约定

int x;
int __fastcall add(int a,int b) { return a+b; }
int main(int argc, char* argv[])
{
x=add(1,2);
return 0;
}

;来看产生的汇编代码:

; //main函数体
mov edx, 2                   ; b通过寄存器edx传递
mov ecx, 1                   ; a通过寄存器ecx传递
call ?add@@YIHHH@Z      ; add
mov DWORD PTR ?x@@3HA, eax       ; x
xor eax, eax
ret 0

; //add函数体
lea eax, DWORD PTR [ecx+edx] ; //a,b参数值已经在ecx,edx中,该句将这两个值的和放到eax作为返回值;
ret 0                 ; //这里应该函数体负责清栈 ;但因为两个参数已经通过寄存器传递  ; //了,没有使用堆栈,所以ret 0;

; //T::add函数体
mov eax, DWORD PTR [ecx]            ; //通过this指针(保存在ecx)将start0的值移动到eax
mov ecx, DWORD PTR _a$[esp-4] ; //把a的值移动到ecx; this的值将丢失,但函数体中已经不需要了
add eax, ecx           ; //将a的值累加到eax
mov ecx, DWORD PTR _b$[esp-4]  ; //把b的值移动到ecx;
add eax, ecx         ; //将b的值累加到eax
ret 8                       ; //函数体负责清栈 ; 

五: 其他

1.在VC中实现一个函数体时可以使用__declspec(naked)声明,它告诉编译器,不要为函数体自动产生开始和结束码;
2.在VC6中,想得到汇编代码清单,设置方法为:

引用:[Project]->[Setting...]->[C++]->[Category:]->[Listing Files]->[Listing file type:]->[Assembily ,...]

3. VC6中嵌入汇编代码的方式为:

__asm { <汇编语句s> }  或  __asm <一条汇编语句> 

4.VC6中重新设定函数使用的默认调用约定的方法是:
   引用:
   在 [ Project ]->[ Setting...]->[ C++ ]->[ Project Options: ]中增加编译设置
   比如:/Gd 代表__cdecl;   /Gr 代表__fastcall;  /Gz 代表__stdcall 

总结:

 1._stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。

    2、C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。

    _cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。是MFC缺省调用约定。

   
3、__fastcall调用约定是“人”如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字
(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。

    _fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上"@"前缀,在函数名后加上"@"和参数的字节数。   

    4、thiscall仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。

    5、naked
call采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些
寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。

    关键字
__stdcall、__cdecl和__fastcall可以直接加在要输出的函数前,也可以在编译环境的Setting.../C/C++
/Code
Generation项选择。当加在输出函数前的关键字与编译环境中的选择不同时,直接加在输出函数前的关键字有效。它们对应的命令行参数分别为/Gz、
/Gd和/Gr。缺省状态为/Gd,即__cdecl。

   
要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI
宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己
的APIs。

常见的函数调用约定中,只有 cdecl 约定需要调用者来清除堆栈;
C/C++中的函数支持参数数目不定的参数列表, 比如printf函数;由于函数体不知道调用者在堆栈中压入了多少参数,所以函数体不能方便的知道应该怎样清除堆栈,那么最好的办法就是把清除堆栈的责任交给调用者;
这应该就是cdecl调用约定存在的原因吧;
Windows的API中,一般使用的是 stdcall 约定; 建议在不同语言间的调用中(如DLL)最好采用stdcall调用约定,因为它在语言间兼容性支持最好;

 


    

 

 

 

 
发表于 @ 2007年08月29日 11:39:00|评论(0AddFeedbackCountStack("1763471"))|编辑

新一篇: 函数名修饰约定规则 | 旧一篇: 构建自己的操作系统 1


2007年08月



 函数名修饰和调用规则2

函数的名字修饰(Decorated
Name)就是编译器在编译期间创建的一个字符串,用来指明函数的定义或原型。LINK程序或其他工具有时需要指定函数的名字修饰来定位函数的正确位置。
多数情况下程序员并不需要知道函数的名字修饰,LINK程序或其他工具会自动区分他们。当然,在某些情况下需要指定函数的名字修饰,例如在C++程序中,
为了让LINK程序或其他工具能够匹配到正确的函数名
阅读全文>

发表于 @ 2007年08月30日 14:55:00|评论(0AddFeedbackCountStack("1765425"))|编辑



 函数名修饰约定规则


数名字修饰(Decorated Name)方式 函数的名字修饰(Decorated
Name)就是编译器在编译期间创建的一个字符串,用来指明函数的定义或原型。LINK程序或其他工具有时需要指定函数的名字修饰来定位函数的正确位置。
多数情况下程序员并不需要知道函数的名字修饰,LINK程序或其他工具会自动区分他们。当然,在某些情况下需要指定函数的名字修饰,例如在C++程序中,
为了让LINK程序或其他工具能够匹配到正确的函数名字,就必须为重载函数和一些特殊的函数(如构造函数和析构函数)指定名字装饰。另一种需要指定函数的
名字修饰的情况是在汇编程序中调用C或C++的函数。如果函数名字,
阅读全文>

发表于 @ 2007年08月30日 14:50:00|评论(1AddFeedbackCountStack("1765411"))|编辑



 函数调用约定

1._stdcall是Pascal程序的缺省调用方式,通常用于Win32
Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。
2、C调用约定(即用__cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如
此,实现可变参数的函数只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。
阅读全文>

发表于 @ 2007年08月29日 11:39:00|评论(0AddFeedbackCountStack("1763471"))|编辑



 构建自己的操作系统 1

如果你了解操作系统的工作原理, 这对你的编程会大有裨益,特别是对那些开发设备驱动的系统程序员。即使是非系统程序员也会从中获得很多知识。我相信:每个搞程序开发的人都想有一个自己制作的操作系统。通过阅读本篇文章你也会学到怎样从磁盘驱动器读写原始扇区。
阅读全文>

发表于 @ 2007年08月28日 15:09:00|评论(1AddFeedbackCountStack("1762172"))|编辑



 从Ollydbg说起-----WinDbg用户态调试教程(2)

2.一般调试流程
2.1 调试目标

CTRL+E 相当于Ollydbg的菜单“文件=>打开”,WinDbg除了能设启动参数之外,还能设起始文件夹,还有一个调试子进程的额外选项。
阅读全文>

发表于 @ 2007年08月27日 18:09:00|评论(1AddFeedbackCountStack("1760995"))|编辑



 从Ollydbg说起-----WinDbg用户态调试教程(1)


先说明, 这篇文章是我转载的。文章不错。我现在对 windbg 比较熟,平时调试就是用它来调试内核程序。调试用户态程序就用 VC.net
自带的调试器功能也相当强大。这篇文章主要是结合 ollydbg 和 windbg 讲解的文章,可以增长点对用户态程序调试的技巧。
阅读全文>

发表于 @ 2007年08月27日 18:07:00|评论(1AddFeedbackCountStack("1760991"))|编辑



 页目录和页表结构

页目录和页表结构
阅读全文>

发表于 @ 2007年08月27日 16:31:00|评论(0AddFeedbackCountStack("1760815"))|编辑



 利用VMWare和WinDbg调试驱动程序

用VMWare和WinDbg调试驱动程序
阅读全文>

发表于 @ 2007年08月27日 13:29:00|评论(8AddFeedbackCountStack("1760549"))|编辑



 More on debugging with SOS.DLL - enter Visual Studio

To
follow up briefly on the postings that I put up about WinDbg and the
SOS.DLL I wanted to add some information about Visual Studio because I
think I goofed really with the last couple of postings on that topic
阅读全文>

发表于 @ 2007年08月27日 13:01:00|评论(0AddFeedbackCountStack("1760522"))|编辑



 A word for WinDbg

不错的入门文章,介绍了WinDbg的安装、Symbols、常用命令和一个调试示例。
阅读全文>

发表于 @ 2007年08月27日 12:56:00|评论(0AddFeedbackCountStack("1760515"))|编辑



 RARP:反向地址转换协议


向地址转换协议 (RARP:Reverse Address Resolution
Protocol)就是将局域网中某个主机的物理地址转换为IP地址,比如局域网中有一台主机只知道物理地址而不知道IP地址,那么可以通过RARP协议
发出征求自身IP地址的广播请求,然后由RARP服务器负责回答。
阅读全文>

发表于 @ 2007年08月27日 10:28:00|评论(0AddFeedbackCountStack("1760249"))|编辑



 winVNC 源代码分析

下面的内容是用 VC.NET 的调试器调试的整个源码而确定的执行流程. 在分析代码时尽量不要静态的分析代码,这样速度很慢的。利用调试器我们可以通过简单的设置断点来跟踪整个执行流程。WINVNC 调试其整体流程
阅读全文>

发表于 @ 2007年08月25日 12:52:00|评论(0AddFeedbackCountStack("1758541"))|编辑



 实时屏幕传输

实时屏幕传输
阅读全文>

发表于 @ 2007年08月24日 09:15:00|评论(1AddFeedbackCountStack("1756961"))|编辑



 调试技术的五大原则

1. 理解代码 2. 适当休息 3. 渐增测试 4. 务求简单 5. 不要舍不得代码 6. 跟踪整体流程
阅读全文>

发表于 @ 2007年08月24日 09:05:00|评论(0AddFeedbackCountStack("1756937"))|编辑



 Recognizer & FS & Filter

文件系统过滤驱动的一般处理流程(参照sfilter):

DriverEntry
||
||初始化dispatch表中的IRP_MJ_FILE_SYSTEM_CONTROL
||routine
||
DriverObject->MajorFunction[IRP_MJ_FILE
阅读全文>

发表于 @ 2007年08月23日 20:28:00|评论(0AddFeedbackCountStack("1756603"))|编辑



 PE加载器的实现

对于加壳软件的开发者,掌握PE Loader的实现是最基本的技术;因为壳运行结束后,你要仿照PE加载器去Load映象体,我曾看过UPX的开源代码,全手工打造PE,实现的也是极其复杂,是学习加壳,脱壳和开发加壳软件的上乘资料.
阅读全文>

发表于 @ 2007年08月23日 20:26:00|评论(0AddFeedbackCountStack("1756601"))|编辑



 Smss.exe 进程分析--NT 源码--当机方法


说中的会话管理服务器进程,它是windows操作系统启动时引导的最重要的系统进程,它负责启动csrss.exe和winlogon.exe进程,并
对它们进行监控,如果发现其中一个挂掉,它马上叫你当机,所以要想结束csrss.exe/winlogon.exe,先结束Smss.exe,源码前一
目了然(摘自windows nt 4.0代码)
阅读全文>

发表于 @ 2007年08月23日 20:15:00|评论(0AddFeedbackCountStack("1756589"))|编辑



 如何写windows系统已保护的内存区域

如何写windows系统已保护的内存区域
阅读全文>

发表于 @ 2007年08月23日 20:09:00|评论(0AddFeedbackCountStack("1756584"))|编辑



 NDIS and TDI Hooking, Part II

This is the second and last article on how to hook into the NDIS and TDI
阅读全文>

发表于 @ 2007年08月23日 20:07:00|评论(0AddFeedbackCountStack("1756578"))|编辑



 文件透明加密--新思路

文件透明加密--新思路
阅读全文>

发表于 @ 2007年08月23日 19:53:00|评论(0AddFeedbackCountStack("1756567"))|编辑



 穿透还原卡和还原软件的代码

穿透还原卡和还原软件的代码
阅读全文>

发表于 @ 2007年08月23日 19:51:00|评论(0AddFeedbackCountStack("1756565"))|编辑



 NMFilter.sys(4.3.2.2485) 逆向源代码

#ifdef ExAllocatePool
#undef ExAllocatePool
#endif
#define ExAllocatePool(a,b) ExAllocatePoolWithTag(a,b,'tliF')

///////////////////////////////////////////////////////////////////////////////////////////////////

#define NT_DEV_NAME L"//Device//NMFILTER"
#define PORT_NAME L"COMPORT"
#define KEYBOARD_CONTROLLER L"I8042"
#define CLASS_GUID L"ClassGUID"
#define KEYBOARD_GUID L"{4D36E96B-E325-11CE-BFC1-08002BE10318}"
#define GUID_STR_LEN
阅读全文>

发表于 @ 2007年08月23日 19:43:00|评论(0AddFeedbackCountStack("1756561"))|编辑



 利用 cmd 安装驱动 inf 文件

通常情况下我们不能从 cmd 中安装驱动程序,但是下面的方法可以正确安装驱动程序文件

xx.inf 应该和驱动程序在同一个文件夹下:

C:/> rundll32 syssetup,SetupInfObjectInstallAction DefaultInstall 128 C:/xxx.inf
阅读全文>

发表于 @ 2007年08月22日 16:00:00|评论(0AddFeedbackCountStack("1754610"))|编辑



 wchar[] 与 char[] 转换


网上找了 将近一个小时没有找到合适的传换类型代码。自己 10 分钟就写好了,有点没自信了。懒啦!! 现在帖出来和大家分享吧, 节省大家的时间。
W2C(WCHAR *pwstr, CHAR * cstr) C2W( CHAR * cstr, WCHAR *wstr)
阅读全文>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: