[系统安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则自成一格。
posted @ 2017-10-22 01:21  17bdw  阅读(564)  评论(0编辑  收藏  举报