C语言常见的自增\自减,判断,循环等反汇编

C语言中自增/自检运算符

自增/自减运算(后缀型):

#include <stdio.h>

int main(void)
{
	int x = 1;
	int y;

	y = x++;

	return 0;
}

反汇编如下:

     5: 	int x = 1;
00FA4398  mov         dword ptr [x],1  
     6: 	int y;
     7: 
     8: 	y = x++;
00FA439F  mov         eax,dword ptr [x]  
00FA43A2  mov         dword ptr [y],eax  
00FA43A5  mov         ecx,dword ptr [x]  
00FA43A8  add         ecx,1  
00FA43AB  mov         dword ptr [x],ecx  

dword ptr [x]指的就是变量x。dword ptr是指将目标变量的数据类型转为dword类型。

从这段代码我们可以看出x++是先赋值再+1的。

自增/自减运算(前缀型):

#include <stdio.h>

int main(void)
{
    int x = 1;
    int y;
    
    y = ++x;
    
    return 0;
}

反汇编如下

5: 	int x = 1;
00AA4398  mov         dword ptr [x],1  
     6: 	int y;
     7: 
     8: 	y = ++x;
00AA439F  mov         eax,dword ptr [x]  
00AA43A2  add         eax,1  
00AA43A5  mov         dword ptr [x],eax  
00AA43A8  mov         ecx,dword ptr [x]  
00AA43AB  mov         dword ptr [y],ecx  

这里的x是先+1后赋值


下面来看一下臭名昭著的谭浩强行为,这段谭浩强行为的代码在编译器中是怎么执行的

谭浩强行为代码

#include <stdio.h>

int main(void)
{
	int x = 1;
	int y;

	y = x+++x;

	return 0;
}

反汇编如下

     5: 	int x = 1;
00394398  mov         dword ptr [x],1  
     6: 	int y;
     7: 
     8: 	y = x+++x;
0039439F  mov         eax,dword ptr [x]  
003943A2  add         eax,dword ptr [x]  
003943A5  mov         dword ptr [y],eax  
003943A8  mov         ecx,dword ptr [x]  
003943AB  add         ecx,1  
003943AE  mov         dword ptr [x],ecx  

可以看到,x是先与x相加后再赋值给y,然后再自加+1

但是这并不是唯一的结果。这取决于编译器,编译器不同最后的结果也不同。

C语言中的判断语句

if-else

#include <stdio.h>

int main(void)
{
	int x;

	scanf("%d", &x);

	if (x <= 0)
		x++;
	else
		x--;

	return 0;
}

反汇编如下:

5: 	int x;
     6: 
     7: 	scanf("%d", &x);
00734858 8D 45 F8             lea         eax,[x]  
0073485B 50                   push        eax  
0073485C 68 CC 7B 73 00       push        offset string "%d" (0737BCCh)  
00734861 E8 C3 CB FF FF       call        __vfscanf_l (0731429h)  
00734866 83 C4 08             add         esp,8  
     8: 
     9: 	if (x <= 0)
00734869 83 7D F8 00          cmp         dword ptr [x],0  
0073486D 7F 0B                jg          main+4Ah (073487Ah)  
    10: 		x++;
0073486F 8B 45 F8             mov         eax,dword ptr [x]  
00734872 83 C0 01             add         eax,1  
00734875 89 45 F8             mov         dword ptr [x],eax  
00734878 EB 09                jmp         main+53h (0734883h)  
    11: 	else
    12: 		x--;
0073487A 8B 45 F8             mov         eax,dword ptr [x]  
0073487D 83 E8 01             sub         eax,1  
00734880 89 45 F8             mov         dword ptr [x],eax  
    13: 
    14: 	return 0;
00734883 33 C0                xor         eax,eax

我们逐步分析这段代码,先从scanf函数开始

     7: 	scanf("%d", &x);
00734858 8D 45 F8             lea         eax,[x]  
0073485B 50                   push        eax  
0073485C 68 CC 7B 73 00       push        offset string "%d" (0737BCCh)  
00734861 E8 C3 CB FF FF       call        __vfscanf_l (0731429h)  
00734866 83 C4 08             add         esp,8  

这段代码表示出了scanf进入函数前的操作(scanf在汇编里的具体实现过程中比较复杂,这里就不讲了),如果没有特别设置则函数调用约定的话默认是cdecl。cdecl调用约定中函数的参数自右向左入栈,入栈的目的就是保护参数的数据防止在调用函数后被修改,这里将x的地址和字符串压入了栈中。而esp的值增加了8,是因为函数调用完后要将调用函数后压入栈中的参数进行清理,在cdecl下由调用方清理。

offset是汇编语言的一个指令,意为获取变量的地址。

9: 	if (x <= 0)
00734869 83 7D F8 00          cmp         dword ptr [x],0  
0073486D 7F 0B                jg          main+4Ah (073487Ah)

这段代码中将x与0进行了比较,若jg(意为大于就跳转)成立则跳转至else(else的地址就是0073487A)

    10: 		x++;
0073486F 8B 45 F8             mov         eax,dword ptr [x]  
00734872 83 C0 01             add         eax,1  
00734875 89 45 F8             mov         dword ptr [x],eax  
00734878 EB 09                jmp         main+53h (0734883h) 

前三行即为x++的操作过程,这里不再赘述。操作结束后跳转至return 0;(return 0;的地址为00734883)

    12: 		x--;
0073487A 8B 45 F8             mov         eax,dword ptr [x]  
0073487D 83 E8 01             sub         eax,1  
00734880 89 45 F8             mov         dword ptr [x],eax

与上述相同,这里不再赘述。不过值得注意的是这里没有jmp指令,因为下面的指令就是return 0;(xor eax, eax)


下面讲一讲比较烦人的if-else嵌套语句在汇编语言中是怎么实现的,下面是个简单的三个数比大小的算法

嵌套的if-else

#include <stdio.h>

int main(void)
{
	int x, y, z;
	int min;

	scanf("%d%d%d", &x, &y, &z);

	if (x < y && x < z)
		min = x;

	else if (y < z)
		min = y;

	else
		min = z;

	printf("%d\n", min);

	return 0;
}

反汇编如下

5: 	int x, y, z;
     6: 	int min;
     7: 
     8: 	scanf("%d%d%d", &x, &y, &z);
00C15098 8D 45 E0             lea         eax,[z]  
00C1509B 50                   push        eax  
00C1509C 8D 4D EC             lea         ecx,[y]  
00C1509F 51                   push        ecx  
00C150A0 8D 55 F8             lea         edx,[x]  
00C150A3 52                   push        edx  
00C150A4 68 CC 7B C1 00       push        offset string "%d" (0C17BCCh)  
00C150A9 E8 7B C3 FF FF       call        _main (0C11429h)  
00C150AE 83 C4 10             add         esp,10h  
     9: 
    10: 	if (x < y && x < z)
00C150B1 8B 45 F8             mov         eax,dword ptr [x]  
00C150B4 3B 45 EC             cmp         eax,dword ptr [y]  
00C150B7 7D 10                jge         main+59h (0C150C9h)  
00C150B9 8B 45 F8             mov         eax,dword ptr [x]  
00C150BC 3B 45 E0             cmp         eax,dword ptr [z]  
00C150BF 7D 08                jge         main+59h (0C150C9h)  
    11: 		min = x;
00C150C1 8B 45 F8             mov         eax,dword ptr [x]  
00C150C4 89 45 D4             mov         dword ptr [min],eax  
00C150C7 EB 16                jmp         main+6Fh (0C150DFh)  
    12: 
    13: 	else if (y < z)
00C150C9 8B 45 EC             mov         eax,dword ptr [y]  
00C150CC 3B 45 E0             cmp         eax,dword ptr [z]  
00C150CF 7D 08                jge         main+69h (0C150D9h)  
    14: 		min = y;
00C150D1 8B 45 EC             mov         eax,dword ptr [y]  
00C150D4 89 45 D4             mov         dword ptr [min],eax  
00C150D7 EB 06                jmp         main+6Fh (0C150DFh)  
    15: 
    16: 	else
    17: 		min = z;
00C150D9 8B 45 E0             mov         eax,dword ptr [z]  
00C150DC 89 45 D4             mov         dword ptr [min],eax  
    18: 
    19: 	printf("%d\n", min);
00C150DF 8B 45 D4             mov         eax,dword ptr [min]  
00C150E2 50                   push        eax  
00C150E3 68 D4 7B C1 00       push        offset string "%d\n" (0C17BD4h)  
00C150E8 E8 50 C3 FF FF       call        __vfscanf_l (0C1143Dh)  
00C150ED 83 C4 08             add         esp,8  
    20: 
    21: 	return 0;
00C150F0 33 C0                xor         eax,eax

先从第一个if-else开始看起

10: 	if (x < y && x < z)
00C150B1 8B 45 F8             mov         eax,dword ptr [x]  
00C150B4 3B 45 EC             cmp         eax,dword ptr [y]  
00C150B7 7D 10                jge         main+59h (0C150C9h)  
00C150B9 8B 45 F8             mov         eax,dword ptr [x]  
00C150BC 3B 45 E0             cmp         eax,dword ptr [z]  
00C150BF 7D 08                jge         main+59h (0C150C9h)  
    11: 		min = x;
00C150C1 8B 45 F8             mov         eax,dword ptr [x]  
00C150C4 89 45 D4             mov         dword ptr [min],eax  
00C150C7 EB 16                jmp         main+6Fh (0C150DFh)  
    12: 
    13: 	else if (y < z)
00C150C9 8B 45 EC             mov         eax,dword ptr [y]  
00C150CC 3B 45 E0             cmp         eax,dword ptr [z]  
00C150CF 7D 08                jge         main+69h (0C150D9h)  
    14: 		min = y;
00C150D1 8B 45 EC             mov         eax,dword ptr [y]  
00C150D4 89 45 D4             mov         dword ptr [min],eax  
00C150D7 EB 06                jmp         main+6Fh (0C150DFh)

x先与y比较,若条件成立(jge意为大于等于就跳转)则跳转至else if(0C150C9);然后x再与y比较,若条件成立也跳转至else if。最后将x赋值给min最后跳转至printf;(00C150DF)。可以看到if中有逻辑运算符&&,但是在汇编语言中则没有明显的体现出来,逻辑运算符会放在后面讲

从汇编角度中去理解复杂的if-else语句要明了的多。


switch

#include <stdio.h>

int main(void)
{
	int x;

	scanf("%d", &x);

	switch (x)
	{
	case 1:
		x = x + 1;
		break;
	case 2:
		x = x + 1;
		break;

	default:
		x = x;
	}

	return 0;
}

反汇编如下

5: 	int x;
     6: 
     7: 	scanf("%d", &x);
00B84858 8D 45 F8             lea         eax,[x]  
00B8485B 50                   push        eax  
00B8485C 68 CC 7B B8 00       push        offset string "%d" (0B87BCCh)  
00B84861 E8 C3 CB FF FF       call        _main (0B81429h)  
00B84866 83 C4 08             add         esp,8  
     8: 
     9: 	switch (x)
00B84869 8B 45 F8             mov         eax,dword ptr [x]  
00B8486C 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
00B84872 83 BD 30 FF FF FF 01 cmp         dword ptr [ebp-0D0h],1  
00B84879 74 0B                je          main+56h (0B84886h)  
00B8487B 83 BD 30 FF FF FF 02 cmp         dword ptr [ebp-0D0h],2  
00B84882 74 0D                je          main+61h (0B84891h)  
00B84884 EB 16                jmp         main+6Ch (0B8489Ch)  
    10: 	{
    11: 	case 1:
    12: 		x = x + 1;
00B84886 8B 45 F8             mov         eax,dword ptr [x]  
00B84889 83 C0 01             add         eax,1  
00B8488C 89 45 F8             mov         dword ptr [x],eax  
    13: 		break;
00B8488F EB 11                jmp         main+72h (0B848A2h)  
    14: 	case 2:
    15: 		x = x + 1;
00B84891 8B 45 F8             mov         eax,dword ptr [x]  
00B84894 83 C0 01             add         eax,1  
00B84897 89 45 F8             mov         dword ptr [x],eax  
    16: 		break;
00B8489A EB 06                jmp         main+72h (0B848A2h)  
    17: 
    18: 	default:
    19: 		x = x;
00B8489C 8B 45 F8             mov         eax,dword ptr [x]  
00B8489F 89 45 F8             mov         dword ptr [x],eax  
    20: 	}
    21: 
    22: 	return 0;
00B848A2 33 C0                xor         eax,eax

先看看这段

9: 	switch (x)
00B84869 8B 45 F8             mov         eax,dword ptr [x]  
00B8486C 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
00B84872 83 BD 30 FF FF FF 01 cmp         dword ptr [ebp-0D0h],1  
00B84879 74 0B                je          main+56h (0B84886h)  
00B8487B 83 BD 30 FF FF FF 02 cmp         dword ptr [ebp-0D0h],2  
00B84882 74 0D                je          main+61h (0B84891h)  
00B84884 EB 16                jmp         main+6Ch (0B8489Ch)

可以看到00B84872和00B84879这两段就是跳转语句,分别对应x值为1和2的情况(je指令意为等于就跳转)。若x的值两者都不是则执行00B84884语句,即跳转至default(0B8489C)。这里我们将1赋值给x,x进入case 1

11: 	case 1:
    12: 		x = x + 1;
00B84886 8B 45 F8             mov         eax,dword ptr [x]  
00B84889 83 C0 01             add         eax,1  
00B8488C 89 45 F8             mov         dword ptr [x],eax  
    13: 		break;
00B8488F EB 11                jmp         main+72h (0B848A2h)

在对x的操作结束后就会直接跳转到return 0;(0B848A2)如果是default中的操作则会继续执行后面的操作,不会跳转。


这里我们就可以从汇编角度去理解为什么switch中每一个选项(除了default)都要有break语句

没有break语句的switch

#include <stdio.h>

int main(void)
{
	int x;

	scanf("%d", &x);

	switch (x)
	{
	case 1:
		x = x + 1;

	case 2:
		x = x + 1;
		

	default:
		x = x;
	}

	return 0;
}

反汇编如下

5: 	int x;
     6: 
     7: 	scanf("%d", &x);
00D04858 8D 45 F8             lea         eax,[x]  
00D0485B 50                   push        eax  
00D0485C 68 CC 7B D0 00       push        offset string "%d" (0D07BCCh)  
00D04861 E8 C3 CB FF FF       call        _main (0D01429h)  
00D04866 83 C4 08             add         esp,8  
     8: 
     9: 	switch (x)
00D04869 8B 45 F8             mov         eax,dword ptr [x]  
00D0486C 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
00D04872 83 BD 30 FF FF FF 01 cmp         dword ptr [ebp-0D0h],1  
00D04879 74 0B                je          main+56h (0D04886h)  
00D0487B 83 BD 30 FF FF FF 02 cmp         dword ptr [ebp-0D0h],2  
00D04882 74 0B                je          main+5Fh (0D0488Fh)  
00D04884 EB 12                jmp         main+68h (0D04898h)  
    10: 	{
    11: 	case 1:
    12: 		x = x + 1;
00D04886 8B 45 F8             mov         eax,dword ptr [x]  
00D04889 83 C0 01             add         eax,1  
00D0488C 89 45 F8             mov         dword ptr [x],eax  
    13: 
    14: 	case 2:
    15: 		x = x + 1;
00D0488F 8B 45 F8             mov         eax,dword ptr [x]  
00D04892 83 C0 01             add         eax,1  
00D04895 89 45 F8             mov         dword ptr [x],eax  
    16: 		
    17: 
    18: 	default:
    19: 		x = x;
00D04898 8B 45 F8             mov         eax,dword ptr [x]  
00D0489B 89 45 F8             mov         dword ptr [x],eax  
    20: 	}
    21: 
    22: 	return 0;
00D0489E 33 C0                xor         eax,eax

取部分选项

11: 	case 1:
    12: 		x = x + 1;
00D04886 8B 45 F8             mov         eax,dword ptr [x]  
00D04889 83 C0 01             add         eax,1  
00D0488C 89 45 F8             mov         dword ptr [x],eax  
    13: 
    14: 	case 2:
    15: 		x = x + 1;
00D0488F 8B 45 F8             mov         eax,dword ptr [x]  
00D04892 83 C0 01             add         eax,1  
00D04895 89 45 F8             mov         dword ptr [x],eax

我们可以看到如果没有break语句,那么就没有jmp指令,若case 1操作结束后则指令会直接从00D0488F运行下去,不会直接跳转至return 0;


总结:

  1. C语言中的自增/自减运算经过反汇编后,并没有使用汇编自带的自增/自减指令INC/DEC,而是使用add R, 1的形式(R为寄存器)

  2. C语言的判断语句在经过反汇编后,汇编代码大量使用cmp与跳转指令(如je、jge、jmp等)相互搭配组合的形式来实现

  3. C语言的函数调用约定默认为cdecl,这种调用约定的特点即为参数从右往左压栈,并且调用函数前压入栈的参数由调用方清理

上一篇:Windows异常学习笔记(五)—— 未处理异常


下一篇:go 编译过程-day01