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中有逻辑运算符&&,但是在汇编语言中则没有明显的体现出来,逻辑运算符会放在后面讲
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;
C语言中的循环
在C语言中有三种常见的循环:for、while、do...while
先介绍for循环
for循环
#include <stdio.h>
int main(void)
{
int x = 0;
for (int i = 0; i < 5; i++)
x = i;
return 0;
}
反汇编如下
5: int x = 0;
000E4398 C7 45 F8 00 00 00 00 mov dword ptr [x],0
6:
7: for (int i = 0; i < 5; i++)
000E439F C7 45 EC 00 00 00 00 mov dword ptr [ebp-14h],0
000E43A6 EB 09 jmp main+41h (0E43B1h)
000E43A8 8B 45 EC mov eax,dword ptr [ebp-14h]
000E43AB 83 C0 01 add eax,1
000E43AE 89 45 EC mov dword ptr [ebp-14h],eax
000E43B1 83 7D EC 05 cmp dword ptr [ebp-14h],5
000E43B5 7D 08 jge main+4Fh (0E43BFh)
8: x = i;
000E43B7 8B 45 EC mov eax,dword ptr [ebp-14h]
000E43BA 89 45 F8 mov dword ptr [x],eax
000E43BD EB E9 jmp main+38h (0E43A8h)
9:
10:
11: return 0;
000E43BF 33 C0 xor eax,eax
在这里dword ptr [ebp-14h]指的就是变量i,意思就是将ebp的地址减14后指向的空间作为变量i的空间。000E43A6与000E43B1之间的代码就是i++操作,而000E43B5与000E43BD之间的代码就是i < 5以及循环下的x = i操作。
单循环比较简单,下面看看for循环的嵌套形式
for循环的嵌套形式
#include <stdio.h>
int main(void)
{
int x = 0;
for (int j = 0; j < 5; j++)
for (int i = 0; i < 5; i++)
x = i;
return 0;
}
反汇编如下
5: int x = 0;
006B4398 C7 45 F8 00 00 00 00 mov dword ptr [x],0
6:
7: for (int j = 0; j < 5; j++)
006B439F C7 45 EC 00 00 00 00 mov dword ptr [ebp-14h],0
006B43A6 EB 09 jmp main+41h (06B43B1h)
006B43A8 8B 45 EC mov eax,dword ptr [ebp-14h]
006B43AB 83 C0 01 add eax,1
006B43AE 89 45 EC mov dword ptr [ebp-14h],eax
006B43B1 83 7D EC 05 cmp dword ptr [ebp-14h],5
006B43B5 7D 22 jge main+69h (06B43D9h)
8: for (int i = 0; i < 5; i++)
006B43B7 C7 45 E0 00 00 00 00 mov dword ptr [ebp-20h],0
006B43BE EB 09 jmp main+59h (06B43C9h)
006B43C0 8B 45 E0 mov eax,dword ptr [ebp-20h]
006B43C3 83 C0 01 add eax,1
006B43C6 89 45 E0 mov dword ptr [ebp-20h],eax
006B43C9 83 7D E0 05 cmp dword ptr [ebp-20h],5
006B43CD 7D 08 jge main+67h (06B43D7h)
9: x = i;
006B43CF 8B 45 E0 mov eax,dword ptr [ebp-20h]
006B43D2 89 45 F8 mov dword ptr [x],eax
006B43D5 EB E9 jmp main+50h (06B43C0h)
006B43D7 EB CF jmp main+38h (06B43A8h)
10:
11:
12: return 0;
006B43D9 33 C0 xor eax,eax
从006B43B7到006B43D5为内循环操作,操作完后再执行006B43D7跳转至006B43A8中完成一次外循环操作,周而复始,最后执行006B43B5跳转至return 0;
while循环
#include <stdio.h>
int main(void)
{
int x = 0;
while (x < 5)
x++;
return 0;
}
反汇编如下
5: int x = 0;
00444398 C7 45 F8 00 00 00 00 mov dword ptr [x],0
6:
7: while (x < 5)
0044439F 83 7D F8 05 cmp dword ptr [x],5
004443A3 7D 0B jge main+40h (04443B0h)
8: x++;
004443A5 8B 45 F8 mov eax,dword ptr [x]
004443A8 83 C0 01 add eax,1
004443AB 89 45 F8 mov dword ptr [x],eax
004443AE EB EF jmp main+2Fh (044439Fh)
9:
10:
11: return 0;
004443B0 33 C0 xor eax,eax
可以看的出来while循环经过反汇编后得到的汇编代码比较符合C语言while循环的操作步骤
do...while循环
#include <stdio.h>
int main(void)
{
int x = 0;
do
{
x++;
} while (x < 5);
return 0;
}
反汇编如下
5: int x = 0;
00044398 C7 45 F8 00 00 00 00 mov dword ptr [x],0
6:
7: do
8: {
9: x++;
0004439F 8B 45 F8 mov eax,dword ptr [x]
000443A2 83 C0 01 add eax,1
000443A5 89 45 F8 mov dword ptr [x],eax
10: } while (x < 5);
000443A8 83 7D F8 05 cmp dword ptr [x],5
000443AC 7C F1 jl main+2Fh (04439Fh)
11:
12: return 0;
000443AE 33 C0 xor eax,eax
13: }
do...while循环与while循环最大的不同就在于do...while循环无论循环条件是什么都会先进行一次操作。这里也可以看的出来反汇编后的汇编代码比较符合C语言中do...while循环的操作步骤,当do...while的循环条件不满足时就会从000443AC跳出(jl指令意为小于就跳出)
总结:
-
C语言中的自增/自减运算经过反汇编后,并没有使用汇编自带的自增/自减指令INC/DEC,而是使用add/sub R, 1的形式(R为寄存器)
-
C语言的判断语句在经过反汇编后,汇编代码大量使用cmp与跳转指令(如je、jge、jmp等)相互搭配组合的形式来实现
-
C语言的函数调用约定默认为cdecl,这种调用约定的特点即为参数从右往左压栈,并且调用函数前压入栈的参数由调用方清理
-
C语言中的for、while、do...while循环中,while和do...while循环反汇编后的汇编代码比较符合C语言执行这两个循环的操作步骤,for循环则有些差别。
-
在用VS2019或其他C/C++编译器时尽量关闭源代码和符号名提示,以提高对汇编的理解
本文中涉及到的汇编指令:
Size ptr :意为改变目标变量的数据类型,Size可以是byte(字节)、word(字)、dwrod(双字)、qword(四字)
offset:意为获取变量的地址。
jg:意为大于跳转,j为jump,g为great
jmp:无条件跳转
jge:意为大于等于就跳转
je:意为等于就跳转
jl:意为小于就跳出