[系统安13全]反汇编-循环语句do-while、while、for
0x1 循环语句
C语言的循环主要分为for、while与do-while 3种。
1.1 do-while循环
C源代码:
0x41是字母A的ASCII码,变量nNum的初始值是26,因此0x41+(26-nNum)配合着每次的nNum--,是一个从字母A到Z的打印过程。
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
int nNum = 26;
printf("Mom! I can sing my ABC!\r\n");
do // 听!小Baby开始唱ABC了……
{
printf("%c ", 0x41+(26-nNum) );
nNum--;
} while (nNum>0);
return 0;
}
Debug反汇编代码:
00D37710 push ebp
...省略代码
00D3772E mov [local.2],0x1A ; 1A等于十进制的26,赋值到nNum变量中
00D37735 push 00DC8E50 ; Mom! I can sing my ABC!\r\n
00D3773A call 00D33D14 ; 调用printf函数
00D3773F add esp,0x4 ; 调用者进行堆栈平衡
00D37742 mov eax,0x1A ; 1A等于十进制的26
00D37747 sub eax,[local.2] ; eax减去nNum变量当前的值,26-nNum
00D3774A add eax,0x41 ; 41是十进制的A,与A相加
00D3774D push eax
00D3774E push 00DC8E70 ; %c
00D37753 call 00D33D14 ; 调用printf函数
00D37758 add esp,0x8
00D3775B mov eax,[local.2] ; 把nNum变量赋值到eax
00D3775E sub eax,0x1 ; nNum变量减去1
00D37761 mov [local.2],eax ; eax的值重新赋值回nNum变量,因为汇编是不可以直接对数值进行操作的,必须经过寄存器进行操作
00D37764 cmp [local.2],0x0 ; 对比nNum变量的值是否为0
00D37768 jg short 00D37742 ; 不满足条件跳转回原先循环的地址00D37742处重复执行
...省略代码
00D3776E pop ebx
Release反汇编代码:
00D37710 push ebp
...省略代码
00941261 push 009564D8 ; Mom! I can sing my ABC!\r\n
00941266 call printf ; 调用printf函数
0094126B add esp,0x4 ; 调用者进行堆栈平衡
0094126E mov esi,0x41 ; 1A等于十进制的26,赋值到esi寄存器中
00941273 push esi ; <%c> ;注意下这里
00941274 push 009564F4 ; %c
00941279 call printf ; 调用printf函数
0094127E inc esi ; 直接将esi加1
0094127F add esp,0x8 ; 平衡堆栈操作
00941282 cmp esi,0x5B ; 直接与0x5B相对比(Z的十六进制码是0x5A)
00941285 jl short 00941273; 如果小于此值则跳转回00941273地址处再次执行
...省略代码
00D3776E pop ebx
通过以上代码,可以看出编译器已经把代码进行了优化:
int main()
{
int nNum = 0x41;
do{
printf("%c",nNum++)
}while(nNum<'[') //while(nNum < 0x5B)
// z的十六进制是5A
}
编译器把稍显复杂的代码变得非常简单,真实意图是将我们的减法运算转换为加法运算。原因是CPU本质上只会做加法运算,因此加法运算对于CPU来说是执行速度最快的计算。
特征:
DO_TAG:
XXXXX
代码进行操作; do..while循环执行的内容
XXXXX
CMP XXX,XXX ; 对比条件
JXX DO_TAG ; 不满足跳转到原先执行的位置
1.2 while循环
C源代码:
int _tmain(int argc, _TCHAR* argv[])
{
int nNum = 26;
printf("Mom! I can sing my ABC!\r\n");
while(nNum>0) // 听!小Baby开始唱ABC了……
{
printf("%c ",0x41+(26-nNum) );
nNum--;
}
return 0;
}
Debug反汇编代码:
Debug版本的反汇编代码多了两条用于判断是否跳出循环的指令。因为do..while是先执行代码,后进行判断。while则是先进行判断,后执行代码。
00BC7710 push ebp
..省略代码
00BC772C rep stos dword ptr es:[edi]
00BC772E mov [local.2],0x1A ; 把26赋值到nNum变量中
00BC7735 push 00C58E50 ; Mom! I can sing my ABC!\r\n
00BC773A call 00BC3D14
00BC773F add esp,0x4
00BC7742 cmp [local.2],0x0
00BC7746 jle short 00BC776C ; 多了个判断,如果其小于等于0,则跳出循环
00BC7748 mov eax,0x1A ; 把26赋值到eax
00BC774D sub eax,[local.2] ; 用存着26的eax寄存器减去nNum变量
00BC7750 add eax,0x41 ; 跟0x41(A)相加
00BC7753 push eax
00BC7754 push 00C58E70 ; %c
00BC7759 call 00BC3D14 ; 调用printf函数
00BC775E add esp,0x8 ; 平衡堆栈
00BC7761 mov eax,[local.2]
00BC7764 sub eax,0x1 ; 前后两行代码都是nNum变量减去1
00BC7767 mov [local.2],eax
00BC776A jmp short 00BC7742 ; 无条件跳转回判断00BC7742 处
00BC776C xor eax,eax
..省略代码
00BC7781 retn
Release反汇编代码:
do..while与while生成的Release版代码是相同的。编译器探测出了我们的循环判断用的是一个常量,因此不存在首次执行条件不匹配的情况。
002B1260 push esi ; 9-16.__argc
002B1261 push 002C64D8 ; Mom! I can sing my ABC!\r\n
002B1266 call printf ; 调用printf
002B126B add esp,0x4 ; 平衡堆栈
002B126E mov esi,0x41 ; 把0x41赋值到esi
002B1273 push esi ; <%c>
002B1274 push 002C64F4 ; %c
002B1279 call printf ; 调用printf
002B127E inc esi ; 加1指令
002B127F add esp,0x8 ; 平衡堆栈
002B1282 cmp esi,0x5B ; 对比0x5B
002B1285 jl short 002B1273; 小于0x5B则跳转
002B1287 xor eax,eax
002B1289 pop esi
002B128A retn
将代码中的常量改成变量试试,那么汇编代码就会更改成另外一种形式了。C源代码如下:
int _tmain(int argc, _TCHAR* argv[])
{
printf("Mom! I can sing my ABC!\r\n");
while(argc>0) // 听!小Baby开始唱ABC了……
{
printf("%c ",0x41+(26-argc) ); //输出Z
argc--;
}
return 0;
}
Release版的反汇编代码:
00041260 push ebp
...省略代码
00041264 push 000564D8 ; Mom! I can sing my ABC!\r\n
00041269 call printf ; printf
0004126E mov esi,[arg.1] ; 将参数赋值到esi寄存器中
00041271 add esp,0x4 ; 平衡堆栈
00041274 test esi,esi ; 测试参数是否为0
00041276 jle short 00041295; 如果为0则跳出循环
00041278 push edi
00041279 mov edi,0x5B ; 0x5B = 0x41+26
0004127E sub edi,esi ; 用0x5B减去参数
00041280 push edi ; <%c> 此时的值由5B变成了5A,也就是'Z'
00041281 push 000564F4 ; %c
00041286 call printf ; printf
0004128B dec esi ; 参数减1
0004128C add esp,0x8 ; 平衡堆栈
0004128F inc edi ; EDI加1,5A又变回5B
00041290 test esi,esi ; 按位的'与'运算,指令对两个操作数 的内容均不进行修改,仅是在逻辑与操作后,对标志位重新置位
00041292 jg short 00041280 ; 如果参数大于ESI时则继续循环,否则就向下执行
...省略代码
00041299 retn
用变量做判断条件很明显与常量不一样,编译器将0x41+(26-argc)优化成了0x5B-argc。如下:
int _tmain(int argc, _TCHAR* argv[])
{
printf("Mom! I can sing my ABC!\r\n");
//while(argc>0) // 听!小Baby开始唱ABC了……
//{
// printf("%c ",0x41+(26-argc) ); //输出Z
// argc--;
//}
while (argc>0)
{
printf("%c", 0x5B - argc); //直接就是0x5B-参数
argc--;
0x5B + 1;
}
return 0;
}
while循环反汇编特征:
先对比在执行代码,不满足条件再次循环
CMP XXX,XXX ;注:CMP可以替换为TEST
JXX WHILE_END_TAG
WHILE_TAG:
.......
.......
CMP XXX,XXX
JXX WHILE_TAG
WHILE_END_TAG:
1.3 for循环
for循环与while循环本质上是一样的,唯一的不同在于for循环在循环体内多了一个步长部分。
C源代码:
int _tmain(int argc, _TCHAR* argv[])
{
printf("Mom! I can sing my ABC!\r\n");
// 听!小Baby开始唱ABC了……
for (int nNum = 26; nNum>0; nNum--)
printf("%c ",0x41+(26-nNum) );
return 0;
}
Debug反汇编代码:
0003C540 push ebp
...省略代码
0003C55C rep stos dword ptr es:[edi]
0003C55E push 9-19.00090C70 ; Mom! I can sing my ABC!\r\n
0003C563 call 9-19.0003B0B3
0003C568 add esp,0x4 ; 平衡堆栈
0003C56B mov [local.2],0x1A ; 1A等于十六进制26,赋值给nNum变量中
0003C572 jmp X9-19.0003C57D
0003C574 mov eax,[local.2] ; nNum被赋值到eax寄存器
0003C577 sub eax,0x1 ; nNum--;步长的位置
0003C57A mov [local.2],eax
0003C57D cmp [local.2],0x0 ; 判断是否大于0
0003C581 jle X9-19.0003C59E ; 如果小于0就结束循环
0003C583 mov eax,0x1A
0003C588 sub eax,[local.2] ; 26-nNum变量
0003C58B add eax,0x41 ; 跟0x41(A)相加
0003C58E push eax
0003C58F push 9-19.00090C6C ; %c
0003C594 call 9-19.0003B0B3 ; 调用Printf()函数
0003C599 add esp,0x8
0003C59C jmp X9-19.0003C574
0003C59E xor eax,eax
...省略代码
0003C5B2 pop ebp
0003C5B3 retn
- 显然从循环语句这个集合是从do-while先诞生的,而后是while,最后才是for。
- 从执行效率上看,代码最短且判断最少的就是do-while循环了。
Release版反汇编:
00301000 push esi
00301001 push 9-19.00316448; Mom! I can sing my ABC!\r\n
00301006 call 9-19.printf ; printf
0030100B add esp,0x4
0030100E mov esi,0x41 ; 将0x41 赋值给 esi
00301013 push esi ; <%c>
00301014 push 9-19.00316464; %c
00301019 call 9-19.printf ; printf
0030101E inc esi ; esi自增1
0030101F add esp,0x8
00301022 cmp esi,0x5B ; 对比是否小于0x5B,Z等于0x5A
00301025 jl X9-19.00301013
00301027 xor eax,eax
00301029 pop esi
0030102A retn
由于本例子中使用了常量,基本逻辑没有改变,所以这段代码与do-while、while一模一样。
印证了while与for都是以do-while为基础框架的,只不过是在里面加了一些小判断。
变量版for循环的反汇编例子:
int _tmain(int argc, _TCHAR* argv[])
{
printf("Mom! I can sing my ABC!\r\n");
// 听!小Baby开始唱ABC了……
for ( ; argc >0; argc--)
printf("%c ",0x41+(26-argc) );
return 0;
}
Release版反汇编代码:
以变量为判断条件的for循环与while循环所生成的代码是完全相同的。
00391000 push edi
00391001 push 9-20.0039B384 ; /Mom! I can sing my ABC!\r\n
00391006 call 9-20.printf ; \printf
0039100B mov edi,dword ptr ss:[esp+0xC]; 取得参数,argc
0039100F add esp,0x4
00391012 test edi,edi
00391014 jle X9-20.00391035 ; 如果参数为空就跳出循环
00391016 push esi
00391017 mov esi,0x5B ; 将0x5B赋值给esi,Z的值是0x5A
0039101C sub esi,edi ; 0x5B的值减去参数值argc
0039101E mov edi,edi
00391020 push esi ; /<%c>
00391021 push 9-20.0039B3A0 ; |%c
00391026 call 9-20.printf ; \printf
0039102B dec edi ; edi减1
0039102C add esp,0x8 ; 平衡堆栈
0039102F inc esi ; esi自增1
00391030 test edi,edi
00391032 jg X9-20.00391020
00391034 pop esi ; 9-20.<ModuleEntryPoint>
00391035 xor eax,eax
00391037 pop edi
00391038 retn
Debug版本for循环的一个反汇编特点的总结:
首先对初始化变量进行赋值,判断条件是否符合。不符合则不跳转继续执行代码块内容。然后无条件跳转回前面的地址,执行增加步长部分的指令。伪代码如下:
初始化块:
jmp CMP_TAG
STEP_TAG:
步长块执行
CMP_TAG:
for中的条件判断,判断是否结束循环
JXX FOR_END_TAG
.....
执行for代码块中的内容
.....
jmp STEP_TAG ;无条件跳转回步长操作部分
.....
FOR_END_TAG:
总结:
debug版3种循环各不相同,Release版可总结如下:
- 当循环采用常量为判断条件时,相同逻辑的3种循环生成的代码完全相同。
- 当循环采用变量为判断条件时,相同逻辑的while与for生成的代码完全相同,而do-while则自成一格。