CALL是如何炼成的 之一:理论篇

遇到一个CALL应该如何写? 
 
这个是写一个内挂不可避免的问题.刚初学的朋友可能会不知道如何入手.想起刚学这方面的时候,绕过很多
弯路,现在把一些经验写出来给大家参考参考吧,不是很高深的东西,但我觉得对某些人很有帮助. 
 
CALL是什么? 
 
CALL是汇编中的一个指令,CPU执行这条指令会执行2个动作 一:压入EIP入栈 二:跳转到后面的地
址.  跟RETN指令配合就实现了汇编中子程序的作用,通常我们常说的写CALL就是 调用游戏中已经存在
的功能子程序. 
 
比如说 
 
viod myadd (int a, int b) 

int c=a+b; 

 
这是一个简单的子程序,当我们用程序语言调用那么就是  myadd(5,4); 
而在汇编里则是 
push 4 
push 5 
call myadd 
 
编译好的程序不会存在什么函数变量和子程序. 只有1和0 . 所以编译器会给myadd 分配一个地址.在反
汇编里就是  
push 4 
push 5 
call ********  
 
CALL ******* 并不单单就是子程序的调用,他还可以调用 函数 API. 
 
如函数  send 
 
send( 
socket; 
buf; 
len; 
flags; 

 
 
在汇编里调用就是 
push flags 
push len 
push buf 
push socket 
call send 
 
 
 
参数在反汇编里表现形式. 
 
在汇编或反汇编里参数都是压入堆栈来供CALL调用的.而CALL以[ESP+*]的形式读取参数. 
 
如: 
viod myadd(int a,int b) 

int c=a+b 

 
myadd(4,5); 
 
在反汇编参数 4 和参数 5 在反汇编里的实现形式是 
 
push 5 
push 4 
call ******** 
 
 
写CALL的时候有时候会给EAX赋值而有些时候却要给ECX赋值,如何知道要给某个寄存器赋值呢? 
 
一般学过外挂方面的知识的朋友大概都是 汇编指令 mov 的 实现的功能.  
 
mov 操作数1 操作数2 
 
将操作数2 的值放入到操作数1 里. 
 
如: 
mov eax,ebx 
 
将 ebx的值放入到eax里 
 
寄存器的作用大家都知道是用来存放数据供CPU调用.所以寄存器本身是空的. 
 
在调用一个CALL的时候 所有的寄存器都是空的或者是调用上一个CALL遗留下来的残留数据. 
 
当这个CALL需要一个200的值,通过ebx 储存. 
 
那么我们调用CALL而不给ebx赋值 ,调用的时候CALL还是会读取当前ebx的值,而这个时候寄存器的值
则是0或者上一个CALL调用后残留数据.而不是CALL想要的200数据. 
 
 
调用CALL之所以需要寄存器,是因为CALL通过调用相关的寄存器获取到特定的数值. 
 
而CALL调用寄存器的语句常用则有  PUSH  寄存器,    mov 寄存器,寄存器  lea 寄存器,寄存器 等等. 
 
 
这里我们要提一下寄存器环境保护,众所周知,CPU的寄存器只有一个EAX ,EBX,ECX,EDX......  如果一个
寄存器EAX里存放着一些资料供后面使用,但当前CALL却需要EAX储存一些临时的数值这个时候要怎么
办? 
 
这个时候我们则需要把寄存器EAX里的数值保存到一个地方,然后把EAX给CALL使用 用完后在把那个
值放 
回到寄存器EAX里去.  这个过程则是寄存器的环境保护. 在反汇编里保存寄存器的地方就是堆栈. 
 
当你在一个子程序头部看到一些 push eax  而又在尾部看到 pop eax 的时候 这里的eax 就是寄存器数
值保护,push eax  则是保存eax储存的数值,  pop eax则是放回去. 
 
堆栈如何平衡? 
 
如何理解堆栈的平衡呢?当push 压入了一个堆栈, esp的值就会-4 来存放数值. CALL完后就需要+4来把
esp的值恢复回去.如何知道一个CALL是否需要堆栈平衡呢?看CALL内部的尾部就可以了, 如果CALL尾
部有RETN **  或者add esp,*之类的,则是CALL自动平衡了一些堆栈,至于恢复了多少则要看指令后面带
的数字. 
 
如何写一个CALL? 
 
其实写CALL很简单,先处理堆栈和寄存器,在处理堆栈和寄存器中的数值,最后处理堆栈平衡就可以了, 
 
写CALL之前你要明白几个东西. 
 
viod myadd(int a,int b) 

int c=a+b 

 
myadd(4,5);  //程序的调用代码 
 
在反汇编里程序的调用代码为 
mov eax,5 
mov ebx,4 
push eax 
push ebx 
call 5e0000  //假设 CALL的地址为5e0000 
add esp,8 
retn 
 
你会如何写这个CALL呢? 
 
写CALL最重要的一点就是把CALL需要的寄存器参数写出来. 
 
如 这个CALL则需要2个参数. 
我们的调用代码则可以这样写 
 
push 5 
push 4 
call 5e0000 
add esp,8 
 
 
而不需要把5放入eax里 把4放入ebx里. 
 
为何?因为CALL并没有读取eax 和ebx里的数据. 
 
这里我们写的代码和反汇编里的代码有什么共同点呢?2者之间有和关联? 
 
其实共同点只有三个,那就是调用了同一个地址和提供了CALL所需要的参数,还有处理了堆栈的平衡 
 
而两者之间是没有任何关联的. 
 
有些人认为写CALL一定要按照游戏调用CALL的代码来写.其实,不是的.游戏调用CALL的代码只是用来
参考的.我们只需要写出CALL所需要的寄存器即可.  这个是很多人存在的一个误区.只要你能理解这个概
念那么CALL就会变的简单明了. 
 
所以上面我们没有按照调用代码把数值放入寄存器里而是直接压入堆栈供CALL调用. 
 
所以说直接靠一个CALL地址(并非调用代码地址),分析代码,来写出调用代码,也并不是不可能的事情,我也
可以做到一些不是很复杂的CALL代码分析,当然如果太复杂的话那就是找不自在. 
 
记住~游戏中调用CALL的代码只是一个参考的依据. 
 
寄存器中的值. 
 
我认为寄存器中的值可以分为3种, 
一是常量 也就是固定的值,比如说push 23123,push 0 等等 
二是基址 这类一般是控件或者类的基址,或者游戏的基址,人物的基址,一般由ECX储存 
三是可以自定义的值 如 坐标, 喊话频道, 喊话内容地址等等.这些都是可以自定义的. 
 
 
 
==============================2009.11.6日更新=================== 
 
寄存器eax值的跟踪! 
 
在调用约定里 所有CALL的返回值都是靠EAX返回的,如果EAX放不下就放在EDX里。如果返回的结
果是文本类型的话就会返回一个指针地址。在游戏里,一般的功能CALL返回值很少有。 
 
如 
int myadd(int a, int b) 

int c=a+b; 
return c;   

 
在反汇编的表现形式大概就是 
 
push ebp                        //寄存器环境保护 
mov  ebp,esp              //将堆栈指针的值传给EBP用于开辟临时变量 
sub esp,4                  //开辟一个临时空间 也就是我们定义的临时变量C 
mov [esp-4],0            //将 c 初始化 也就是C=0 尽管我们没有写不过编译器会自动完成 
mov eax,a                //将A放入寄存器EAX里  
add eax,b              //将EAX加上B 
mov [esp-4],eax              // 将计算结果保存到临时变量C 
 
mov eax,[esp-4]    //将保存结果的变量C传递到EAX 
 
mov esp,ebp        //将堆栈指针恢复到调用CALL之前。 
pop ebp              //将寄存器保存的值取出来。 
 
retn 
 
这个就是一个简单加法子程序的运行过程。所以很多时候我们跟踪一下自己写的程序,看看机器是如何运
行你的代码的,这个是一件很有趣的事情,也会增加你对编程和汇编的了解。这里的反汇编代码是我手动
写的可能和真正的有误差,但大概的过程是应该没有错的。 
 
所以很多人在跟踪寄存器EAX的时候往往会放过挨着的CALL指令,直接找上面的汇编代码。要看看EAX
是否是一个CALL的返回值其实很简单,看看运行CALL指令后EAX的值是否有变化就可以了。 
 
 
 
 
CALL参数的构造。 
 
CALL的有些参数需要自己定义,比如说常见的有,坐标,喊话频道,怪物ID等等。这些就不说了,还有
一些不太明显的。比如说喊话内容。 
 
游戏里的喊话内容是以文本指针地址存放的。有些朋友就把这个指针的基址找出来了。其实没有必要,因
为对于CALL来说,他需要的只是一个喊话内容而已。而不是需要游戏里的喊话指针地址。 
 
所以写一个CALL重要的是知道CALL需要什么,只要你能知道他需要什么你也就能够调用它了,就像机
器一样你需要提供给它需要的动力才能启动。 
 
 
游戏调用CALL时,值的内存传递方式。 
 
除了堆栈和寄存器以外,游戏可能会用其他的方法传递数值,最常见的就是内存。写过游戏聊天CALL的
朋友可能会遇到过频道信息放在内存里供CALL调用的。一般是在喊话CALL内部调用,用CE找到频道
信息的地址吧。 当然这里既然在CALL内部调用了频道在内存里的值,也可以通过频道信息来找到喊话
CALL的地址。  
 
 
 
 
=================================11.11日更新.光棍节快乐=============== 
 
CALL参数的传递! 
 
 
总所周知,寄存器是用来传递参数的,但本身并没有值,他只是一个存放值的空间,存放用来传递的参数.CPU
执行一个CALL的时候,我们可以把几个寄存器看成是一个空的空间.那么我们就可以理解,并不是所有的寄
存器都会拿来存放数据的.看CALL需要传递几个参数了.  
 
寄存器除了传递参数外,另外一个功能就是存放临时数据,如何判断一个CALL调用了那么寄存器的参数.你
要会判断这个寄存器是用来传递的还是用来临时存放数据的. 
 
在分析CALL之前我们应该把寄存器都看成空的 除了ESP EIP2个特殊的寄存器外. 
 
当你发现 mov eax,ebx的时候 EBX是否是CALL要调用的寄存器呢?我觉得这个要看寄存器是否有值(在
假设寄存器都为空的情况下).  如果mov eax,ebx 前面没有给EBX赋值的指令的话那么 这个ebx就是
CALL需要调用的寄存器.  也就是用来传递参数的寄存器. 那么EAX呢? 
 
这里的EAX就是一个临时存放的寄存器.用来存放ebx里的数据. 
 
如果mov eax,ebx  指令前面有给ebx赋值的话,  比如说mov ebx,123123  那么ebx也可以排除了 因为
他用来存放临时数据了 .并不是用来传递数据的. 
 
当然,在调试的情况下,调用一个CALL之前寄存器不可能为空,大部分都有之前CALL遗留下来的残留数据. 
 
还有一种是堆栈传递.这个就不相信说了,只要看下调用CALL前的PUSH指令就能很好的得到. 
 
 
 
寄存器esp,ebp值的跟踪! 
 
上面说到 堆栈参数的传递,当参数压入到堆栈CALL的内部是如何读取的呢? 
 
很多朋友都看见过这样的寻址指令 mov ebx,[esp+1c]  这个时候很多朋友都去寻找赋值 ESP 相关的指令
去了,但找了半天都没找到.其实这里直接指向了堆栈. ESP是一个特殊的寄存器,他永远指向了堆栈顶部. 
 
所以一般轻易不会去变动这个值.  当 CPU 执行 PUSH指令时 ,ESP就会-4 然后把数据存放到当前ESP
的地址里.这个时候如果读取刚刚压入的参数 就是 mov ,eax,[esp]  这样堆栈顶部的数据就会被读取. 
 
但是这个时候又压入一个数据呢?  压入的时候ESP会-4  那么 指向刚刚那个参数就是 mov ebx,[esp+4] 
 
如果压入了5个 要指向第一个参数 就是 [esp+10]  没错就是10  一个堆栈是4字节 4个就是16字节 16
在16进制里就是10 
 
这里要说明的一下是 CALL指令也会压入一个数值.所以 
 
PUSH EAX 
CALL ******** 
 
在CALL内部指向 EAX 就是 [ESP+4]  这里其实压入了2个堆栈 ,一个是PUSH EAX  还有一个是
CALL. 
 
 
临时参数的表达方式是 [ebp-*]  一般是在CALL头部把ESP的数值赋值到EBP  那么 EBP永远指向
CALL头部时的堆栈指针,对于下面新增的堆栈就是 以减法表示 因为 压入一个堆栈ESP就会-4  .故
[EBP-*]在反汇编里代表临时变量. 
 
=========================11.12号更新===================================== 
 
游戏CALL分析的四种常用方法! 
 
一般来说常见的有 
发包函数下段返回:这个是比较常见的方法,代表作是完美国际的游戏.常用发包函数有send  和 WSASend
按CTRL+F9就可以返回到调用功能代码处. 
 
功能信息跟踪:当断下发包函数返回无法找到功能CALL的话那么就需要用到这种方法了,也是比较常见的
方法.比如说,自动寻路的目标坐标点. 喊话功能的喊话内容存放地址,攻击怪物的怪物ID信息,等等的一切都
是.即使能靠下发包断点返回就能找到游戏功能CALL的游戏里 有些功能也需要用这种方法跟踪 最常见的
是自动寻路和TAB 选怪功能. 
 
发包BUF区下内存写入:我没用过这种方法,是听群里人说的,他在天龙里用过.断封包函数,然后在封包临时
存放地址下内存写入.然后返回即可. 
 
游戏结构分析:比较常见的是完美国际的和热血江湖,其他的也见过一些,首先找到一个CALL的地址,然后返
回到他的结构点,一般是游戏的检测玩家动作的函数层, 接收到后然后对比执行相关CALL这里几乎能找到
所有的动作,不过因为是最高层所以分析的东西比较多. 
 
当然了,可能还有其他的方法,比如说下鼠标点击函数等等. 
 
 
 
 
寄存器值的跟踪与分析! 
 
有时候有的参数会经常变化 所以需要找到他存放的基址才能获取每次变化的值.这个时候就要对参数进行
跟踪找到他的基址.一般是用CE 或者用OD分析代码取到. 
 
对于这类值的跟踪我建议使用CE.最后说明一下,基址的公式并不只有一种,用最短的那个公式即可 
 
如果某些需要用OD来跟踪的话,最好学下汇编的基础. 
 
 
 
寄存器值的来源! 
 
很多参数都是有来源的,比如说,喊话内容 来自于 你输入的喊话框地址,  压入的频道信息来自于你选择的
频道,买卖的数量来自你选择的买卖窗口, 坐标的值来自于你点击地图生成的反馈,等等的一切都是. 
 
看似很简单的东西,大部分人都明白.其实还有一些参数也是由一些 
 
 
写CALL时常见的错误! 
 
一般写CALL的时候常见的2种错误是,一个是无法读取一个内存地址,或者是无法写入一个内存地址,这个
代码的地址一般在CALL的内部.一般由寄存器的参数赋值错误或未对需要的寄存器赋值造成的. 
 
还有一种常见的是堆栈不平衡所造成的错误. 
 
这2种是比较常见的错误,也非常好解决,只要对照一下正确的寄存器参数就可以了 
 
 
找CALL的思路! 
 
找CALL时,思路很重要,如果一个打坐的CALL无法用bp send返回找到, 那么该如何找呢?思路就是打坐
的状态~游戏里 人物的每个动作必定有些内存值会不一样.也许这些值代表人物是在打坐状态,那么我们跟
踪这个 
 
值就很容易找到CALL了.或者说征途的买卖功能, 他的游戏是这样实现买卖的首先打开买卖窗口是不会发
包 
的,最后会弹出一个确认框,这个时候点确定便会发一个买卖封包.CALL内部压入了一个确认框的基址. 
 
如果你找到这个最后确认的CALL那么之前的窗口如何寻找呢? 我觉得既然是买卖窗口最后产生的确认框
地址,我们也可以通过这个地址来寻找到买卖的窗口. 
 
一个喊话功能寻找的话并非只有查找访问喊话地址的代码,你也可以查找频道信息,密人信息等聊天有关的.
所以依照这个思路,查找怪物ID也可以通过找攻击CALL来寻找. 
 
只要你找对了正确的思路,你便可以通往一条成功的道路. 
 
=========================11.18号更新===================================== 
 
CALL的本质! 
 
所谓的call,其实本质上来说就是一条汇编指令.只要找到了关键代码的地址,传入适当参数,就可以借用游
戏中已有功能来完成内挂的功能。 
 
写CALL其实就2个步骤,第一找到关键代码的地址,第二传入适当的参数。 
 
 
刚刚逛光海的时候,看到这样一个返汇编原型 
 
004CA355    C745 E1 0000000>MOV DWORD PTR SS:[EBP-1F],0 
004CA35C    C745 DD 0000000>MOV DWORD PTR SS:[EBP-23],0 
004CA363    C745 D9 0000000>MOV DWORD PTR SS:[EBP-27],0 
004CA36A    C745 D5 0000000>MOV DWORD PTR SS:[EBP-2B],0 
004CA371    8B4D FC        MOV ECX,DWORD PTR SS:[EBP-4]     
004CA374    894D D5        MOV DWORD PTR SS:[EBP-2B],ECX 
004CA377    6A 00          PUSH 0 
004CA379    8D55 D4        LEA EDX,DWORD PTR SS:[EBP-2C] 
004CA37C    52              PUSH EDX 
004CA37D    8B45 CC        MOV EAX,DWORD PTR SS:[EBP-34] 
004CA380    8B48 04        MOV ECX,DWORD PTR DS:[EAX+4] 
004CA383    E8 F8B90B00    CALL game.00585D80 
004CA388    33C0            XOR EAX,EAX 
004CA38A    8BE5            MOV ESP,EBP 
004CA38C    5D              POP EBP 
 
这里用到的指令不外乎那几种,汇编的初学者都可以正确的说出来。很多人能理解这个指令的功能,却不
懂这个指令所代表的含义。 
 
004CA355    C745 E1 0000000>MOV DWORD PTR SS:[EBP-1F],0 
004CA35C    C745 DD 0000000>MOV DWORD PTR SS:[EBP-23],0 
004CA363    C745 D9 0000000>MOV DWORD PTR SS:[EBP-27],0 
004CA36A    C745 D5 0000000>MOV DWORD PTR SS:[EBP-2B],0 
 
如,如果光看指令的话,谁都知道这个指令是代表 将0 赋值给[ebp-*] 这个地址。但[ebp-*]又代表什么呢?
这个其实是一个很简单的指令,相信很多人都知道[ebp-*] 代表临时变量. 
 
很多汇编指令都有一定的含义所在.所以理解这些东西你将会更清楚的从反汇编原型中看到更多的东西出
来. 
 
汇编很重要,反汇编对于逆向来说更重要. 
 
写一个CALL我们首先要处理堆栈.这里压入了2个堆栈.也就是说这个子程序有2个参数. 
 
 
004CA377    6A 00          PUSH 0                                                '第一个堆栈 
004CA379    8D55 D4        LEA EDX,DWORD PTR SS:[EBP-2C] 
004CA37C    52              PUSH EDX                                              '第二个堆栈 
004CA37D    8B45 CC        MOV EAX,DWORD PTR SS:[EBP-34] 
004CA380    8B48 04        MOV ECX,DWORD PTR DS:[EAX+4] 
004CA383    E8 F8B90B00    CALL game.00585D80 
 
第一个堆栈属于常量不会变化的那种.所以直接写上去就可以了. 
 
第二个是压入了EDX 而EDX 则是 [ebp-2C]  的地址.也就是说EDX=EBP-2C  .LEA是传址的命令. 
 
我们知道[ebp-2c] 是一个临时变量,而压入了这个地址其实是充当着一个指针的作用.当然这些因为没有看
过反汇编内部所以一切只是猜测.    假设[ebp-2c]=1c5000=200     
那么 
他压入的就是1c5000 
push 1c5000 
 
我们如何跟踪这个值呢? 
 
首先ebp的来源是从esp传过来的.而esp 则是不固定是变化的.所以我们这里每次断下来的值不一定是一
样的. 
 
我们上面提到 要写一个CALL首先要知道子程序所需要的.这里压入了一个地址,我们可以猜测子程序需要
的是一个存放着一定数值的指针地址. 
所以我们可以在写CALL的时候模拟出一个来,只要CALL需要的时候能正确指向这个数值就可以了. 
 
堆栈我们先放下一边,我们再来看看寄存器. 
 
004CA371    8B4D FC        MOV ECX,DWORD PTR SS:[EBP-4]     
004CA374    894D D5        MOV DWORD PTR SS:[EBP-2B],ECX 
 
004CA379    8D55 D4        LEA EDX,DWORD PTR SS:[EBP-2C] 
 
004CA37D    8B45 CC        MOV EAX,DWORD PTR SS:[EBP-34] 
004CA380    8B48 04        MOV ECX,DWORD PTR DS:[EAX+4] 
 
因为没有CALL内部的反汇编原型,所以下面的分析会存在一些错误. 
 
 
004CA379    8D55 D4        LEA EDX,DWORD PTR SS:[EBP-2C] 
从这些指令上来看,调用CALL的时候分别使用了 ECX EAX EDX.首先EDX可以排除,他被调用时为了压入
堆栈的时候临时存放的.当然他可以存放到EBX 或者EAX都可以. 
 
004CA371    8B4D FC        MOV ECX,DWORD PTR SS:[EBP-4]     
 
 
004CA380    8B48 04        MOV ECX,DWORD PTR DS:[EAX+4] 
 
这里2个指令都调用了 ECX, 但真正用来存放数据的只是下面那行. 因为在执行第二个指令的时候原有的
ECX值被覆盖了.调用CALL的时候只会存在最后一个指令赋值的数据. 
 
004CA371    8B4D FC        MOV ECX,DWORD PTR SS:[EBP-4]     
004CA374    894D D5        MOV DWORD PTR SS:[EBP-2B],ECX 
 
从这里可以看出,一开始的ECX只是作为一个临时存放的寄存器.赋值给[ebp-2b]的数据还是由[ebp-4]传递
过来的. 
 
这2句如果翻译成编程语言也很简单 
临时变量y; 
临时变量x; 
x=y; 
 
 
004CA37D    8B45 CC        MOV EAX,DWORD PTR SS:[EBP-34] 
004CA380    8B48 04        MOV ECX,DWORD PTR DS:[EAX+4] 
 
这里ECX的来源是EAX  也就是 [ebp-34] 指针里的值 在加上4 指针地址里的值.  很绕口,我们可以分开
来写. 
 
这里我们要看传入ECX的数据是什么样的类型,如果是数据的话 那么我们可以直接构造,如果是基址一类
的话那么就需要跟踪处理了. 
 
假设,[ebp-34]的来源如下 
 
mov ecx,[5c2000] 
mov [ebp-34],ecx 
MOV EAX,DWORD PTR SS:[EBP-34] 
MOV ECX,DWORD PTR DS:[EAX+4] 
 
这样的话我们要如何写这个CALL呢? 
 
其实很简单,[5c2000] 一直到EAX的时候也没变化所以我们这里不需要处理了,然后加4 写成公式就
是,[[5c2000]+4]  写成调用代码就是  
 
mov ebx,[5C2000] 
mov ecx,[ebx+4] 
 
就是那么简单。这里有人可能要问了,为何不用EAX? 
 
其实答案很简单,就是CALL没有调用EAX里的数据。作为临时存放的地方,我们可以调用任何一个寄存
器来作为临时存放的地点。当然一些特殊的寄存器除外。 
 
所以写成ECX也是一样的 
 
mov ecx,[5C2000] 
mov ecx,[ecx+4] 
 
反正最后调用的是ECX的值,只要ECX的值正确就可以了。 
 
所以这里CALL调用的寄存器就是 ECX了, 而ECX通常是作为一些基址传递专用的寄存器,所以用ECX
传递的数据一般都是基址类的数据,很少直接存放数据。 
 
好了我们来整理一下。 
 
 
PUSH 0 
LEA EDX,[EBP-2C]的地址 
PUSH EDX 
MOV EAX,[EBP-34]的地址 
MOV ECX,[EAX+4] 
CALL 00585D80 
 
这个就是调用代码了。 
 
对于这个CALL来说 ,他所需要的参数环境就是 2个堆栈 一个ECX。 
 
004CA371    8B4D FC        MOV ECX,DWORD PTR SS:[EBP-4]  
004CA374    894D D5        MOV DWORD PTR SS:[EBP-2B],ECX 
上面代码中给临时变量赋值为何这里不写? 
 
编过程序的人都知道,临时变量只能在子程序内部使用。其他子程序是无法调用的。所以不会用临时变量
来传递CALL所需要的参数。当然全局变量是可以的。 
 
因为只有一段小小的代码,很多东西都没有,所以在很多方面我们都只能猜测.具体的情况请自己分析反汇编
代码吧。 
 
理论写完了,下面会写一些例程来对照理论。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值