先打个广告欢迎加入编程<->逆向群: 90569473 ,本群无任何不良目的,欢迎加入!
《剑走偏峰来自一个中国人的C++教程》之三
C++的运算符与基本控制结构
最近学校课程比较多,抽空还得去游戏里体验人生,所以更新的速度慢了些许。还如前一章所说的一样,本系列的核心思想在第一章,如果你懂了第一章中的思想,可以将这种思想推而广之,用则无穷,不用则如粪土!
正如第一部分所说的,C++中提供了三种处理数据的方法运算符、基本控制结构、编程思想,本章讨论其中的两部分运算符与基本控制结构!
在玩游戏的时候,我发现人生的确如游戏,当你在玩游戏的时候,不妨想象一样你自己,也许你只是地球游戏中被外星人创造的一个角色而已。这种想象原于对太极图的思考,在太极图中,阴中有阳圈,阳中有阴圈,个人觉得它的意思是在说,阴中有太极,阳中有太极,太极中有阴阳,即大太极套小太极,小太极套小小太极,依次无穷无尽,这个过程被老子称为天下大事必做于细,天下难事必做易!
class老子的话,object C++,现在让我们用这种套子思想来看待C++中的一些特性!
问题:有9个袋子,只有一个袋子存放9枚硬币,其它袋子都存放10枚硬币,给你一个天平,问最少称几次可以找出只有9枚硬币的袋子?
相信聪明的你,已经想到答案是2次,如你所想,我们首先将9个袋子平均分成三份,将每一份放入一个大袋子,然后任意取两个大袋子进行称量,如果相等则9枚硬币的袋子在没有称的大袋子中,相反则9枚硬币的袋子在较轻的大袋子中,然后从存放9枚硬币的大袋子中随便取出两个小袋子,同样的方法找到9枚硬币的小袋子!
这个例子,我特意将分成3份的小袋子放入到了一个大袋子中,只是为了让人们理解天下大事必做于细,天下难事必做易这句话,用套子思想,返回去思考上面的问题,不难发现3个袋子(即为太极),大袋子(大太极),每个大袋子中的3个袋子(小太极)!
这个抽象的过程是在说明天下所有的事物应该是有一些细小的基本元素组成的,是故我们称天下大事必做于细,天下难事必做于易,相信看到这里也许你会不得不承认我们祖先的伟大之处,这与某些现代面试中所说的细节决定成败如出一辙!
继续以这种想法来讨论C++,以if分支结构为例,结合现实中的现象再举一个例子,在《走西口》这部电视剧中,主人公在从田家大院走到包头的过程与if分支结构基本相似!当主人公在路上发现饭店时,他可以根据需要进去吃或不吃(if结构,即可以执行也可以不执行),当主人公走到双叉路口时,他必须选择一条路走才能走到包头(if……else……结构,即必须执行一个分支),当主人公走到三叉路口时,他先选择一条路看能否继续走下去(if结构,可以走也可以不走),如果不能走,它需要分析剩下的两条路(if……else……结构必须执行一个分支,即必须选择一条路),可见,主人公面对三叉路口时为(if……if ……else结构),当主人公走到多个叉路口时,它需要一条条的判断能否走下去,即(if……else if……else if结构)!
正如上面所说的过程,当你每天走在作任何事的路上时,不妨思考一下双叉路口、三叉路口,多叉路口,这时候你就会发现天下书本是用来查的,而决不是用来读的!
正如我的教育观点一样,教育的作用是树人,而就小处来说人都做不好,岂能做大事,所以孔子提出了格物、致知、修身、齐家、治国、平天下的道理,这个道理也是遵守上面的天下大事必做于细道理的!
这里写了这么多,还是在想完成我的目标让中国人认识自己的国人思想,并善于学习及利用!
一:位运算
//阴阳学说与二进制(第一章中的例子)
#include "head.h"
class yinyang
{
int yin;
int yang;
public:
yinyang(){yin=0;yang=1;}
void and();
void or();
void not();
void xor();
};
void yinyang::and()
{
cout<<"阴阳相克,即0克1"<<endl;
cout<<(yin&yang)<<endl;
}
void yinyang::or()
{
cout<<"阴阳相克,即1克0"<<endl;
cout<<(yin||yang)<<endl;
}
void yinyang::not()
{
cout<<"阴阳相生,即0生1,1生0"<<endl;
cout<<~yin<<" "<<~yang<<endl;
}
void yinyang::xor()
{
cout<<"阴阳互制,即有一个1得1,有两个1得0"<<endl;
cout<<(yin^yang)<<endl;
}
int main()
yinyang oneandzero;
oneandzero.and();
oneandzero.or();
oneandzero.xor();
oneandzero.not();
return 0;
}
上面的程序是在系列一中讨论阴阳学说与二进制的一个例子,在这个例子中就用到了位运算!
1.1:位运算及其机器编码
| & ~ ^ >> <<
机器编码:0x7c 0x26 0x7e 0x5e 0x3e 0x3c
1.2:汇编指令
位运算是与汇编指令相对应的
and reg/mem,reg/mem/imm
or reg/mem,reg/mem/imm
not reg/mem
xor reg/mem,reg/mem/imm
SAL/SAR Reg/Mem, CL/Imm
SHL/SHR Reg/Mem, CL/Imm
and操作是模为2的操作数的乘法操作,它的特点是遇0得0,该指令常用于mask操作,在前面提到过
or操作的特点是遇到1得1
not操作是求反,它与neg指令的关系可以理解为neg=not+1
xor操作是模为2的加法操作,它的特点是双1得0,正是因为模为2的加法操作,所以当1+1=2的时候,就相当于结果溢出了,于是取值为0,即1xor 1=0!
Sal/SAR是算术移位指令
SHL/SHR是逻辑移位指令!
1.3:C++中的位运算
C++中的位运算特别需要注意>> <<,我们在输入输出时,常用到cout<<及cin>>这样的表达式,这是有运算符重载实现的,下面我们通过一个例子将C++中的cout<<修改为cout>>,以领会这种运算符重载的本质套子思想!
//示例程序
#define FAST
#include "head.h"
class test
{
int a,b;
public:
test();
test(int a,int b);
void operator>>(test &);
};
test::test()
{
a=b=1;
}
test::test(int a,int b)
{
this->a=a;
this->b=b;
}
void test::operator >>(test &p)
{
cout<<p.a<<" "<<p.b<<endl;
}
int main()
{
test cout(1,2),b;
cout>>b;
return 0;
}
我们建立了cout对象然后通过cout对象调用operator>>函数,这个函数的作用是输出b对象的内容!
这个例子说明了C++运算符的一个特性重载,因为作为编程语言来说需要的是数据类型,面象对象解决了C++数据类型的问题,而与数据关系最为密切的是运算符,在面向对象后,运算符必须与数据相适应,所以C++提供了运算符的重载作用!
二:逻辑及关系运算符
2.1逻辑关系运算符及机器编码
&&
||
!
< > <= >= == !=
机器编码:0x3c 0x3d 0x3e 关系 <=>
0x7c 0x26 0x21 | & !
2.2:逻辑及关系运算符与汇编
既然逻辑与关系与运算符的主要作用是为了描述控制结构中的条件,所以我们需要讨论一下IA32 CPU的状态寄存器与跳转指令:
标志寄存器
31 … 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
… … VM RF NT IOPL OF DF IF TF SF ZF AF PF CF
运算结果标志位:
CF位主要用来反映运算是否产生进位或借位,如果运算结果的最高位产生了一个进位或借位,那么其值为1,否则其值为0,即有高位借位或者位则为真标识为1
PF用于反映运算结果中1的个数的奇偶性,如果1的个数为偶数则PF=1,否则为PF=0,即运算结果中1的个数为偶数则为真标识为1
AF位用在如下情况时:
在字操作时,发生低位字节向高位字节进位或借位时
在字节操作时,发生低4位向高4位进位或借位时
ZF位用来反映运算结果不否为0,如果为0则其值为1,否则为0,即运算结果是否为0,如果是则条件标识为真ZF=1
SF位用来反映运算结果的符号位,它与运算结果的最高位相同,运算结果为正数,则标识为0,否则为1,这与整数的符号编码是一样的,用0 1表示正负号,这是一种约定而已!
OF位用于反映运算结果是否溢出,如果溢出则OF=1,否则OF=0,即,运算是否溢出,如果溢出则条件为真,标志为1
状态标志位:
TF位,当TF=1时,CPU进入单步执行方式,即每执行一条指令,产生一个单步中断请求!这种方式主要用于程序的调试,指令系统中没有专门的指令来改变标志位TF的值,但可以通过pushfd和popfd来曲线设置
IF位,用来决定CPU是否响应CPU外部的可屏蔽中断发送的中断请求,具体规定如下:
IF=1,CPU可以CPU外部的可屏蔽中断发出的中断请求
IF=0,CPU不响应CPU外部的可屏蔽中断发出的中断请求
CPU指令系统中有专门的指令来改变标志位的IF值!
DF位,用来决定串操作指令时有关指针寄存器发生调整的方向!
32位标志寄存器增加的标志位
IOPL用两位2进制位来表示,该字段指定了要执行I/O指令的特权级,如果当前的特权级在数值上小于等于IOPL的值,那么,该I/O指令可执行,否则将发生一个保护异常!
NT位,用来控制中断返回指令IRET的执行,具体规定如下:
NT=0,用堆栈中保存的值恢复EFLAGS CS EIP,执行常规的中断返回操作
NT=1,通过任务转换实现中断返回
RF位,重启动RF用来控制是否接受高度故障,规定:RF=0时,表示接受调试故障,否则拒绝之,在成功执行完一条指令后,处理机把RF置为0,当接受到一个非调试故障时,处理机就把它置为1!
VM=1表示CPU在8086方式下工作,VM=1表示处理机在保护方式下工作,在Windows开机时,实际上就是通过这个标志位实现CPU实模式到保护模式的切换的
跳转指令:
jmp
无符号跳转
JE/JZ ZF=1 Jump Equal or Jump Zero
JNE/JNZ ZF=0 Jump Not Equal or Jump Not Zero
JA/JNBE CF=0 and ZF=0 Jump Above or Jump Not Below or Equal
JAE/JNB CF=0 Jump Above or Equal or Jump Not Below
JB/JNAE CF=1 Jump Below or Jump Not Above or Equal
JBE/JNA CF=1 or AF=1 Jump Below or Equal or Jump Not Above
有符号跳转
JE/JZ ZF=1 Jump Equal or Jump Zero
JNE/JNZ ZF=0 Jump Not Equal or Jump Not Zero
JG/JNLE ZF=0 and SF=OF Jump Greater or Jump Not Less or Equal
JGE/JNL SF=OF Jump Greater or Equal or Jump Not Less
JL/JNGE SF≠OF Jump Less or Jump Not Greater or Equal
JLE/JNG ZF=1 or SF≠OF Jump Less or Equal or Jump Not Greater
特殊跳转指令
JC CF=1 Jump Carry
JNC CF=0 Jump Not Carry
JO OF=1 Jump Overflow
JNO OF=0 Jump Not Overflow
JP/JPE PF=1 Jump Parity or Jump Parity Even
JNP/JPO PF=0 Jump Not Parity or Jump Parity Odd
JS SF=1 Jump Sign (negative)
JNS SF=0 Jump No Sign (positive)
2.3:C++中的逻辑与关系与运算符
在C++中运算符+操作数称为表达式,对于关系与逻辑运算表达式,都有一个bool值的结果,因此我们能连用关系及逻辑运算符,比如下面的语句:
int a,b,c;
if(a>b>c)...........;
if(a<b<c)............;
a>b>c,根据结合性先计算a>b的值,如果是真则为1,如果是假则为0,所以如果a=4,b=3,c=2的话,实际上不是在比较4>3>2而是在比较1>2,正确的描述为
a> b&&b>c
//示例程序
#define FAST
#include "head.h"
int main()
{
int a=1,b=0;
cout<<(a&&b)<<endl; //位关系位逻辑,<<符号是C++进行重载后的,在操作符重载后其优先级不变,故需要加括号改变&& ||运算符在表达式中的优先级
cout<<(a||b)<<endl;
cout<<!a<<endl;
return 0;
}
//a&&b主要的反汇编代码
00401296 837D FC 00 cmp dword ptr ss:[ebp-4],0 ; 设置标志位
0040129A 74 0F je short api.004012AB
0040129C 837D F8 00 cmp dword ptr ss:[ebp-8],0
004012A0 74 09 je short api.004012AB
004012A2 C745 F4 01000000 mov dword ptr ss:[ebp-C],1
004012A9 EB 07 jmp short api.004012B2
004012AB C745 F4 00000000 mov dword ptr ss:[ebp-C],0
在上面的代码不难看出在a&&b表达式的运算结果保存到了[ebp-C]中,这也验证了我们开头说的话,关系与逻辑表达式都会有一个BOOL值的结果!正是因为逻辑与运算的作用是判断操作中是否有0,所以a&&b表达式的反汇编指令是cmp与je指令配合,通过与0相减,如果结果为0则操作数为0,ZF=1通过je指令跳转最为合适!
//a||b主要的反汇编代码
004012E2 837D FC 00 cmp dword ptr ss:[ebp-4],0
004012E6 75 0F jnz short api.004012F7
004012E8 837D F8 00 cmp dword ptr ss:[ebp-8],0
004012EC 75 09 jnz short api.004012F7
004012EE C745 F0 00000000 mov dword ptr ss:[ebp-10],0
004012F5 EB 07 jmp short api.004012FE
004012F7 C745 F0 01000000 mov dword ptr ss:[ebp-10],1
与a&&b相同a||b表达式也有一个值,这个bool值保存到了[ebp-10]中,正因为逻辑或运算的作用是判断操作数中是否有1,所以a||b表达式的反汇编指令是cmp与jne指令配合,通过与0相减,如果结果不为0,则操作数中一定有一个不为0,ZF=0,通过jne指令最为合适!
//!a反汇编
0040133A 837D FC 00 cmp dword ptr ss:[ebp-4],0
0040133E 0F94C2 sete dl
与上面与或逻辑表达式一样,!操作也会保存运算结果,这个值保存到了EDX中,所以才会有下面的反汇编代码:
0040133E 0F94C2 sete dl
00401341 8BFC mov edi,esp
00401343 52 push edx
00401344 8B0D DC504100 mov ecx,dword ptr ds:[<&MSVCP60D.std::cou>; MSVCP60D.std::cout
sete指令是设置非EFLAGS寄存器状态标志位的一个指令,它的作用是如果ZF=1则dl=0,如果zf=0则dl=1!
//示例关系与swtich结构
#define FAST
#include "head.h"
int main()
{
int a,b;
cin>>a>>b;
int c;
a>b?c=1:c=2;
switch(c)
{
case 1:
cout<<"big!"<<endl;
break;
case 2:
cout<<"little!"<<endl;
break;
default:
cout<<"equal!"<<endl;
break;
}
return 0;
}
//程序反汇编详解
00401270 > 55 push ebp ;保存前一个栈帧的栈底
00401271 8BEC mov ebp,esp ;开辟一个新的栈帧
00401273 83EC 54 sub esp,54 ;分配栈空间
00401276 53 push ebx ;保存ebx
00401277 56 push esi ;保存esi
00401278 57 push edi ;保存edi 实际上就是context结构的一部分,在面向对象中push ecx总是最后,用于指示对象
00401279 8D7D AC lea edi,dword ptr ss:[ebp-54] ;将栈空间的首地末地址保存到edi中
0040127C B9 15000000 mov ecx,15 ;设置栈空间的初始化次数 0x54=84/4=21=0x15,这里的次数与分配的栈空间是一致的
00401281 B8 CCCCCCCC mov eax,CCCCCCCC ;栈空间初始化的值0xccccccccc,实际上就是int3,这个int3与int 3不一样,后者为0xcc03
00401286 F3:AB rep stos dword ptr es:[edi] ;重复串操作将eax的值保存到开辟的栈空间中,实现栈空间初始化
00401288 8BF4 mov esi,esp ;esi=esp
0040128A 8D45 F8 lea eax,dword ptr ss:[ebp-8] ; 得到变量b的地址
0040128D 50 push eax ; 进栈准备做参数
0040128E 8BFC mov edi,esp
00401290 8D4D FC lea ecx,dword ptr ss:[ebp-4] ; 得到变量a的地址
00401293 51 push ecx ; 入栈准备作参数
00401294 8B0D F4504100 mov ecx,dword ptr ds:[<&MSVCP60D.std::cin>; 标准输入到变量a中
0040129A FF15 F0504100 call dword ptr ds:[<&MSVCP60D.std::basic_>; 调用操作符重载函数
004012A0 3BFC cmp edi,esp
004012A2 E8 7B010000 call api._chkesp ; 检测栈平衡
004012A7 8BC8 mov ecx,eax
004012A9 FF15 F0504100 call dword ptr ds:[<&MSVCP60D.std::basic_>; MSVCP60D.std::basic_istream<char,std::char_traits<char> >::operator>>
004012AF 3BF4 cmp esi,esp
004012B1 E8 6C010000 call api._chkesp ; jmp 到 MSVCRT._chkesp
004012B6 8B55 FC mov edx,dword ptr ss:[ebp-4] ; a的值入edx
004012B9 3B55 F8 cmp edx,dword ptr ss:[ebp-8] ; 比较a与b的值
004012BC 7E 0F jle short api.004012CD ; 小于则跳转即大于时不跳
004012BE C745 F4 01000000 mov dword ptr ss:[ebp-C],1 ; 大于时则c=1
004012C5 8B45 F4 mov eax,dword ptr ss:[ebp-C] ; c的值保存到EAX中
004012C8 8945 F0 mov dword ptr ss:[ebp-10],eax ; 保存a>b表达式的值到ebp-0x10中
004012CB EB 0D jmp short api.004012DA ; 跳入switch结构
004012CD C745 F4 02000000 mov dword ptr ss:[ebp-C],2 ; a<b时c=2
004012D4 8B4D F4 mov ecx,dword ptr ss:[ebp-C] ; c的值保存到ecx中
004012D7 894D F0 mov dword ptr ss:[ebp-10],ecx ; 保存a>b表达式的值
004012DA 8B55 F4 mov edx,dword ptr ss:[ebp-C] ; switch结构开始处
004012DD 8955 EC mov dword ptr ss:[ebp-14],edx
004012E0 837D EC 01 cmp dword ptr ss:[ebp-14],1 ; case 1:
004012E4 74 08 je short api.004012EE ; 进入case 1:分支
004012E6 837D EC 02 cmp dword ptr ss:[ebp-14],2 ; case 2:
004012EA 74 39 je short api.00401325 ; 进入case 2:分支
004012EC EB 6E jmp short api.0040135C ; 跳转到default分支中
004012EE 8BF4 mov esi,esp ; case 1:分支处的指令开始处
004012F0 A1 EC504100 mov eax,dword ptr ds:[<&MSVCP60D.std::end>
004012F5 50 push eax
004012F6 8BFC mov edi,esp
004012F8 68 30304100 push api.00413030 ; ASCII "big!"
004012FD 8B0D E8504100 mov ecx,dword ptr ds:[<&MSVCP60D.std::cou>; MSVCP60D.std::cout
00401303 51 push ecx
00401304 FF15 E4504100 call dword ptr ds:[<&MSVCP60D.std::operat>; MSVCP60D.std::operator<<
0040130A 83C4 08 add esp,8
0040130D 3BFC cmp edi,esp
0040130F E8 0E010000 call api._chkesp ; jmp 到 MSVCRT._chkesp
00401314 8BC8 mov ecx,eax
00401316 FF15 E0504100 call dword ptr ds:[<&MSVCP60D.std::basic_>; MSVCP60D.std::basic_ostream<char,std::char_traits<char> >::operator<<
0040131C 3BF4 cmp esi,esp
0040131E E8 FF000000 call api._chkesp ; jmp 到 MSVCRT._chkesp
00401323 EB 6D jmp short api.00401392 ; case 1分支结束处,跳转到程序结束
00401325 8BF4 mov esi,esp ; case 2:分支的指令开始处
00401327 8B15 EC504100 mov edx,dword ptr ds:[<&MSVCP60D.std::end>; MSVCP60D.std::endl
0040132D 52 push edx
0040132E 8BFC mov edi,esp
00401330 68 24304100 push api.00413024 ; ASCII "little!"
00401335 A1 E8504100 mov eax,dword ptr ds:[<&MSVCP60D.std::cou>
0040133A 50 push eax
0040133B FF15 E4504100 call dword ptr ds:[<&MSVCP60D.std::operat>; MSVCP60D.std::operator<<
00401341 83C4 08 add esp,8
00401344 3BFC cmp edi,esp
00401346 E8 D7000000 call api._chkesp ; jmp 到 MSVCRT._chkesp
0040134B 8BC8 mov ecx,eax
0040134D FF15 E0504100 call dword ptr ds:[<&MSVCP60D.std::basic_>; MSVCP60D.std::basic_ostream<char,std::char_traits<char> >::operator<<
00401353 3BF4 cmp esi,esp
00401355 E8 C8000000 call api._chkesp ; jmp 到 MSVCRT._chkesp
0040135A EB 36 jmp short api.00401392 ; case 2:分支指令结束处,跳转到程序结束
0040135C 8BF4 mov esi,esp ; default语句开始处
0040135E 8B0D EC504100 mov ecx,dword ptr ds:[<&MSVCP60D.std::end>; MSVCP60D.std::endl
00401364 51 push ecx
00401365 8BFC mov edi,esp
00401367 68 1C304100 push api.0041301C ; ASCII "equal!"
0040136C 8B15 E8504100 mov edx,dword ptr ds:[<&MSVCP60D.std::cou>; MSVCP60D.std::cout
00401372 52 push edx
00401373 FF15 E4504100 call dword ptr ds:[<&MSVCP60D.std::operat>; MSVCP60D.std::operator<<
00401379 83C4 08 add esp,8
0040137C 3BFC cmp edi,esp
0040137E E8 9F000000 call api._chkesp ; jmp 到 MSVCRT._chkesp
00401383 8BC8 mov ecx,eax
00401385 FF15 E0504100 call dword ptr ds:[<&MSVCP60D.std::basic_>; MSVCP60D.std::basic_ostream<char,std::char_traits<char> >::operator<<
0040138B 3BF4 cmp esi,esp
0040138D E8 90000000 call api._chkesp ; jmp 到 MSVCRT._chkesp
00401392 33C0 xor eax,eax ; return 0;
通过反汇编这个程序,我们得到如下结论:
A:再次证明关系表达式是要保存结果的,在上面的反汇编代码中,我们可以看到每个case 分支附近都有表达式值的保存!
B:熟悉了switch结构的反汇编,在逆向C++的时候,很难分清循环与简单分支结构的不同,实际上,循环结构的反汇编标志就是回跳,而简单分支结构的特点是向下的跳转,这并不是一个难把握的东西!
三:算术运算符
算术运算符可简单的分为如下几类:
1:+ - 正负号运算
2:+ - * / %
3: 增量运算++ --
4:=赋值运算
1.1:算术运算符的机器编码
+ - * / % =
0x2b 0x2d 0x2a 0x2f 0x25 0x3d
1.2:算术运算符与汇编指令
add reg/mem,reg/mem/imm 源操作数+目的操作数,结果保存到目的操作数中
adc reg/mem,reg/mem/imm 源操作数+目的操作+CF,结果保存到目的操作数中
inc reg/mem 操作数+1
xadd reg/mem,reg 目的操作数与源操作数切换位置,然后再相加,使用该指令必须保证有一个操作数位于寄存器中,这牵涉到寻址与CPU周期的问题,作为CPU来说,无法直接实现内存到内存的数据流动!
//示例
mov dword ptr ss:[ebp-4],2
mov eax,2
add eax,dword ptr ss:[ebp-4] ;eax=4
adc eax,dword ptr ss:[ebp-4] ;cf=1 eax=7 cf=0 eax=6
inc eax ;eax=7 eax=8
xadd dword ptr ss:[ebp-4],eax ;eax=2 ss:[ebp-4]=9 or A
sub reg/mem,reg/mem/imm 从目的操作数减去源操作数
sbb reg/mem,reg/mem/imm 从目的操作数减去CF值后再减去源操作数
dec reg/mem 操作数-1
neg reg/mem 求补,即求相反数,在计算机中的表示需要按照一定的规则
neg指令对应于负号运算符
mul reg/mem 将操作数与EAX的值相乘并将其值保存到EDX和EAX中
//示例
mov dword ptr ss:[ebp-4],5
mov eax,2
mul dword ptr ss:[ebp-4]
些时EDX的值为0,EAX的值为A
imul reg1,reg2/mem 386+ reg1*reg2->reg1 或 reg1*mem->reg1
//示例
imul eax,dword ptr ss:[ebp-4] ;eax=0x32
div reg/mem 将操作数与EDX-EAX相除,商保存到eax,余数保存到edx中
idiv reg/mem 与div操作是一样的
div dword ptr ss:[ebp-4] ;eax=0xA
idiv dword ptr ss:[ebp-4] ;eax=0x2
idiv dword ptr ss:[ebp-4] ;eax=0 edx=2
1.3:c++中的算术运算符
//示例程序
#define FAST
#include "head.h"
int main()
{
int a=3;
while(--a)
{
cout<<a<<endl;
}
cout<<"restarting......."<<endl;
a=3;
while(a--)
{
cout<<a<<endl;
}
return 0;
}
00401288 C745 FC 03000000 mov dword ptr ss:[ebp-4],3 ; int a=3;
0040128F 8B45 FC mov eax,dword ptr ss:[ebp-4] ; eax=3
00401292 83E8 01 sub eax,1 ; eax=2
00401295 8945 FC mov dword ptr ss:[ebp-4],eax ; a=2
00401298 837D FC 00 cmp dword ptr ss:[ebp-4],0 ; 2与0进行比较
0040129C 74 33 je short api.004012D1
.........................
00401306 > C745 FC 03000000 mov dword ptr ss:[ebp-4],3 ;a=3
0040130D 8B55 FC mov edx,dword ptr ss:[ebp-4] ;edx=3
00401310 8B45 FC mov eax,dword ptr ss:[ebp-4] ;eax=3
00401313 83E8 01 sub eax,1 ;eax=2
00401316 8945 FC mov dword ptr ss:[ebp-4],eax ;a=2
00401319 85D2 test edx,edx ;3与3进行比较
0040131B 74 33 je short api.$E30 ;分支
.............................
00401328 8B55 FC mov edx,dword ptr ss:[ebp-4] ;edx=2
0040132B 52 push edx ;edx入栈
0040132C 8B0D E4504100 mov ecx,dword ptr ds:[<&MSVCP60D.std::cou>; MSVCP60D.std::cout ;运算符重载输出edx的值
不难看出前算术运算,是先减1,然后用减去的结果去进行操作,而后算术运算是将操作数保存到两个寄存器中,然后将其中之一的寄存器减1,而后再用另一个寄存器去作为条件进行判断,然后再将条件判断的寄存器内容进行修改,通过这个过程来判断while(--a)比while(a--)少一次条件判断!
所以上面程序的结果是2 1 2 1 0
结论:前增量运算要比后运算的指令少好几条,所以在编程时最好使用前增量!同时后增量的值会紧接着在条件判断后对数据进行增量操作!
//理解赋值运算符
#define FAST
#include "head.h"
int main()
{
int a,b,c,d;
a=1;
b=2;
c=3;
d=4;
/*
00401288 C745 FC 01000000 mov dword ptr ss:[ebp-4],1
0040128F C745 F8 02000000 mov dword ptr ss:[ebp-8],2
00401296 C745 F4 03000000 mov dword ptr ss:[ebp-C],3
0040129D C745 F0 04000000 mov dword ptr ss:[ebp-10],4
*/
cout<<"restarting.......!"<<endl;
a=b=c=d=1;
/*
004012D9 C745 F0 01000000 mov dword ptr ss:[ebp-10],1
004012E0 8B55 F0 mov edx,dword ptr ss:[ebp-10]
004012E3 8955 F4 mov dword ptr ss:[ebp-C],edx
004012E6 8B45 F4 mov eax,dword ptr ss:[ebp-C]
004012E9 8945 F8 mov dword ptr ss:[ebp-8],eax
004012EC 8B4D F8 mov ecx,dword ptr ss:[ebp-8]
004012EF 894D FC mov dword ptr ss:[ebp-4],ec
*/
return 0;
}
通过上面的代码不难看出,右结合性连续赋值在执行上并不效率,只不过在编写源码时我们方便了一些而已,实际上在2个变量的连续赋值中是可行的,而越多则越不效率,越不如单句赋值效率!赋值运算关键记住左右二字,左必须为左值,右为右结合性!
//区分=与==
#define FAST
#include "head.h"
int main()
{
int i;
if(i=42)
{
cout<<"此处检测i值是否为0"<<endl;
}
if(i==42)
{
cout<<"此处检测i值是否为42"<<endl;
}
return 0;
}
//反汇编代码 if(i=42)
00401288 C745 FC 2A000000 mov dword ptr ss:[ebp-4],2A
0040128F 837D FC 00 cmp dword ptr ss:[ebp-4],0
00401293 74 35 je short api.004012CA
.....................
//反汇编if(i==42)
004012CA 837D FC 2A cmp dword ptr ss:[ebp-4],2A
004012CE 75 35 jnz short api.00401305
第一个if结构的作用是先将i赋值为42,然后检测i的值是否为0,不为0则条件为真,而后一个是检测i的值是否等于42,如果是则为真!
四:特殊运算符
逗号运算符,是从左到右计算机表达式的值,将最后一个表达式的值作为整个逗号表达式的值!
//,运算符示例
#define FAST
#include "head.h"
int main()
{
int a,b;
if(a=0,b=1)
{
cout<<"1"<<endl;
}
if(b=1,a=0)
{
cout<<"0"<<endl;
}
return 0;
}
//关键反汇编
00401288 C745 FC 00000000 mov dword ptr ss:[ebp-4],0 ; esi=esp
0040128F C745 F8 01000000 mov dword ptr ss:[ebp-8],1
00401296 837D F8 00 cmp dword ptr ss:[ebp-8],0
0040129A 74 35 je short api.004012D1
........................
004012D1 C745 F8 01000000 mov dword ptr ss:[ebp-8],1
004012D8 > C745 FC 00000000 mov dword ptr ss:[ebp-4],0
004012DF 837D FC 00 cmp dword ptr ss:[ebp-4],0
004012E3 74 35 je short api.0040131A
->运算符,这是一个方便对象指针的运算符
//示例
#define FAST
#include "head.h"
class test
{
int a;
public:
void show(){cout<<a<<endl;}
test(){a=1;}
};
int main()
{
test a,*p;
p=&a;
p->show();
(*p).show();
a.show();
return 0;
}
五:太极图与C++运算符、基本控制结构
1:太极图中的套子思想C++运算符的重载特性与if结构
2:太极图中的S曲线程序执行流程!
在太极图中阴阳分界的是S曲线,这条线为什么是S曲线而不是像象棋棋谱中那样为直线简洁而明显呢?
纵观世间万物,只有相对的暂时笔直,决无永远的笔直,比如你看到的路、山川、河流无一笔直的事物,再比如地球,在空中看为圆,地球上看为方,程序的执行流程也一样,表面上看是顺序执行的,但是实际上是有许多条件分支组成的,将S曲线求极限(limS曲线),S曲线的组成部分越接近直线,这与程序执行流程也基本类似,我们的程序写的越小则分支越少,越接近于纯顺序执行!
3:正如人生一样,道路之曲折处,恰是生死福祸之变化处,程序也一样,分支执行处,恰时破解产生之时!
4:思考题
A:为什么本章从hex编码、汇编指令去讨论C++的运算符?
B:为什么位与运算是&而逻辑与运算为&&?
- 标 题:剑走偏峰来自一个中国人的C++教程(系列三)
- 作 者:reduta
- 时 间:2010-03-23 19:11:36
- 链 接:http://bbs.pediy.com/showthread.php?t=109455