一、变量的声明和赋值
-
声明变量:就是告诉计算机,我要用一块内存,你给我留着,宽度和存储格式由数据类型决定
-
计算机何时给变量分配内存:取决于变量的作用范围;如果是全局变量,在程序编译完就已经分配了空间,如果是局部变量,只有所在的程序被调用的时候,才会分配空间
-
全局变量如果不赋初始值默认是0,但局部变量在使用前一定要赋初值
因为画过堆栈图的都知道,局部变量是存储在缓冲区的,缓冲区如果未被使用过,则值为0xCCCCCCCC,如果上一个函数使用过这块内存,那么可能这个函数的全局变量所在的内存中有垃圾数据,就被赋了垃圾值
//全局变量的声明
int a,b,c; //全局变量的声明
void Fun()
{
a = 10; //全局变量的赋值
b = 20;
c = a;
}
void Fun1()
{
int x,y,z; //局部变量的声明
x = 10; //局部变量的赋值
y = 20;
z = x;
}
二、类型转化(movsx/movzx)
-
分为两种:从数据宽度小的转化成数据宽度大的;从数据宽度大的转化成数据宽度小的
-
什么时候用类型转化呢?当我们开发时如果发现现在一个变量中要存储的数值超过了数据类型的宽度,则要小转大;如果发现现在一个变量中存的数很小,不需要很大的宽度来存储,则大转小
-
但是从汇编的角度将类型转化,还要区分有符号和无符号数(二进制)
-
小转大:
-
有符号正向代码:
void Func(){ char c = 0xFF; //unsigned char c = 0xFF short s = c; //char类型转short类型 int i = c; //unsigned short s = c //unsigned int i = c } -
反汇编:可以发现在未进行char类型的0xFF转short类型转化之前,eax中的值为CCCCCCCC,但是执行MOVSX之后,发现虽然0xFF只有一个字节,但是却影响了ax的两字节宽度,而且高位补1 ;而且如果是char转int,ecx从0变成了FFFFFFFF,则高位补1


-
而如果char转int,而此时char为0x73,符号位为0,则高位会补0,类型转化完变成0x00000073
void Func(){ char c = 0x73; int i = c; }
无符号也用同样的方法查看,就不详细说明了,直接总结
-
所以如果有符号小转大:先符号扩展,再传送(MOVSX)
如果小类型的数值符号位为0,则高位全扩展0再传送给大类型;如果小符号位为1,则高位全扩展1再传送
MOV AL,0xFF MOVSX CX,AL //有符号扩展指令,8bit扩展成16bit MOV AL,0x80 MOVSX CX,AL -
如果是无符号小转大:先零扩展,再传送(MOVZX)
//unsigned char c = 0xFF MOV AL,0xFF MOVZX CX,AL //无符号扩展指令 MOV AL,0x80 MOVSX CX,AL
-
-
大转小:
-
从低位开始截取指定的宽度,由小的类型决定截取多少宽度的字节
void Func(){ int i = 0x12345678; short s = i; //大转小 }这里int类型转short类型,short类型宽度为两字节,所以从0x12345678取低16位存到short类型变量所在内存中,最终eax的值从0xCCCCCCCC变成0xCCCC5678
-
三、表达式
-
特点一:表达式无论多么复杂,都只有一个结果
-
特点二:只有表达式,可以编译通过,但并不生成汇编指令,需要与赋值或者其他流程控制语句一起组合的时候才有意义
-
特点三:当表达式中存在不同宽度的变量时,结果将转换为宽度最大的那个(但是看反汇编会发现char和short做运算,也是扩展成32位做运算,感觉特点三总结的有问题)
void Func(){ short c = 10; int i = 20; printf("%d",c+i); //c+i最后的宽度应该为int的宽度,即4字节 }可以发现在不同宽度的数据类型做运算前,反汇编指令先会把低宽度的数据类型做扩展,扩展成高宽度,再做运算,且由于默认是有符号数,所以使用MOVSX扩展
-
特点四:当表达式中同时存在有符号和无符号数的时候,表达式的结果将转换为无符号数。这里只是计算机认为此时应该为无符号数,但是至于对结果的输出或者再次运算或者赋值等操作应该被当成有符号数还是无符号数应该由我们自己决定,比如我想把它定义为unsigned计算机在对它做运算时就应该当成是无符号数做运算,定义为signed就计算机就会当做事有符号数来处理。或者说如果最后输出结果使用的是**%d**,则告诉计算机结果应该被当成有符号数输出,如果使用的是**%u那么结果就应该被当成无符号**数输出。所以特点四其实没什么用,还是由程序员自己决定。下面举一个特殊的地方:
void Func(){ unsigned char a; char b; a = 0xFE; b = 1; printf("%d",a+b); }==上述中按照我们的理解应该是:a变量是一字节宽度,b变量也是一字节宽度,那么最后做a+b结果应该还是一字节,但是这里就要注意了,通过反汇编可以发现,宽度相同,一个有符号一个无符号相加时:两个char类型数值都会被扩展成4字节宽度来运算,即用32位的寄存器来存储,占了寄存器的32位!!!且最后传入printf函数时也是32位,==所以这里结果为0x000000FF,而且由于输出使用的是%d,则告诉计算机结果应该当做有符号数输出,那么0x000000FF当做有符号数应该为255。这里千万不能理解为特点四,有符号与无符号做运算,结果为无符号,所以最后输出应该是255,而不是-1,这个结论明显不正确!!
如果我们将结果赋给一个我们规定了数据类型和宽度的变量,那么结果会按照什么宽度存储和输出呢?比如:
void Func(){ unsigned char a; char b; a = 0xFE; b = 1; char c = a + b; //虽然这里将a+b的结果先存入内存中,占8位 printf("%d",c); //但是这里调用printf函数之前,编译器加了一个mobsx扩展,又将char扩展成32位,那么有符号扩展结果为0xFFFFFFFF,而且最后使用%d打印,则被当成有符号数输出,则值为-1 printf("%u",c); //这里c的值在内存中的二进制是永远一样的,只是当做无符号数输出,将0xFFFFFFFF转为无符号十进制数为4294967295 }
下面再举几个例子综合说明一下结果是有符号还是无符号是我们自己决定的!!同时涉及到有无符号、类型转化也是如此:
void Func1(){ unsigned char a; int b; a = 0xFE; b = 1; printf("%d",a+b);//a要先做类型转换,转成32位,而且是无符号扩展即MOVZX.而且使用%d,结果当成有符号数 } //0x000000FE + 0x00000001 = 0x000000FF 当做有符号数应该为255 void Func2(){ unsigned int a; char b; a = 1; b = 0xFE; printf("%d",a+b);//b要先做类型转化,转成32位,而且是有符号扩展即MOVSX.而且使用%d,结果当成有符号数 } //0x00000001 + 0xFFFFFFFE = 0xFFFFFFFF 当做有符号数打印为-1 void Func3(){ unsigned int a; char b; a = 1; b = 0xFE; printf("%u",a+b);//b要先做类型转化,转成32位,而且是有符号扩展即MOVSX.而且使用%u,结果当成无符号数 } //0x00000001 + 0xFFFFFFFE = 0xFFFFFFFF 当做无符号打印为4294967295 -
总结:一个有符号数和无符号数做运算,且宽度也不同
- 则宽度较小的数先做扩展,且如果宽度较小的数我们定义为有符号数就做符号扩展,如果为无符号数就做零扩展;
- 接着再按照无符号数做运算(这里可以理解为,按照内存中存的两个数的二进制补码做运算),结果就存储在内存中(就是一个二进制补码数)
- 最后我们可以决定想以有符号还是无符号的方式输出,编译器就会根据我们想做的,把这个二进制数取出来,当做有符号数或者无符号数显示出来。
四、运算符
1.关系运算符
-
关系运算符:
==、!=、>=、<=、>、<。含关系运算符的表达式只能返回两种结果0或者1,0表示false,1表示true -
关系运算符的反汇编:
//如果关系运算符与if语句结合,则就是一个影响标志寄存器紧跟一个JCC指令 //如果是==做表达式赋值反汇编是什么样呢? int m; void Func1(){ int a = 1; int b = 2; m = a == b; //先做a == b运算,运算结果为0或1,再将0或1赋值给m }
-
sete指令:和JCC功能类似,即如果两个操作数的值相等,即ZF标志为1,则将cl寄存器的值设为1;如果不相等,则cl寄存器设置为0(还有setne)
2.逻辑运算符
-
逻辑运算符有:
&&、||、!。表示与、或、非 -
逻辑运算符反汇编:
-
&&
//同样逻辑运算符通常与if语句连用,用作判断 void Func(int x,int y,int z) { if(x>1 && y>1 && z>1) { printf("OK"); } else { printf("Error"); } }
可以看到像这种连续判断,只要有一个不满足条件就不用在比较了,直接跳转到printf error,这种就是&&运算;如果全部满足才打印ok
-
||
void Func(int x,int y,int z) { if(x<1 || y>1 || z>1) { printf("OK"); } else { printf("Error"); } }
可以看到三个表达式一个不满足条件没事,继续往后比较,如果最后一个还不满足条件,则跳转打印error;只要满足一个条件,就打印ok
-
!
void Func(int x,int y,int z) { if(x!=1) { printf("OK"); } else { printf("Error"); } }
!就反汇编就是je,反着的如果相等则跳转执行error,若不不相等则跳转执行ok
3.单目与三目运算符
-
单目运算符:
++、--、+=、-=-
反汇编如下:
void Func() { int i = 10; int k = ++i; //先i+1,再把i的值赋给k printf("%d-%d\n",i,k); }
先add操作,再赋值
void Func() { int i = 10; int k = i++; //先把i的值赋给k,i再+1 printf("%d-%d\n",i,k); }
先做的赋值操作,再add
-
-
三目运算符:
x > y ? x : y,表示如果x>y成立则返回x;反之返回y-
反汇编如下:
void Func(int x,int y) { int r = x>y?x:y; printf("%d\n",r); }
其实就是if else的反汇编指令
-
五、语句的执行条件
void Func(int x,int y)
{
if(1){
printf("1\n"); //执行
}
if(2){
printf("2\n"); //执行
}
if(-1){
printf("3\n"); //执行
}
if(0){
printf("4\n"); //不执行
}
if(x>y){
printf("5\n"); //不执行
}
if(x=2){
printf("6\n"); //执行,因为这里是赋值语句,结果为2
}
if(x=0){
printf("7\n") //不执行,因为这里赋值语句结果为0
}
if(x==2){
printf("8\n"); //不执行,因为上面的赋值语句中x=0了,所以x!=2
}
}
int main(int argc,char* argv[]){
Func(1,2)
}
六、作业
-
分析下列程序为什么出错
#include "stdafx.h" void HelloWorld() { printf("Hello World"); getchar(); } void Func() { int arr[5] = {1,2,3,4,5}; arr[6] = (int)HelloWorld; } int main(int argc,char* argv[]){ Func(); return 0; }-
在Func()函数执行前设置断点,进入到Func函数中,Func函数中定义了一个长度为5字节的数组,分别赋值,数组中的元素存储到缓冲区中,因为数组是函数内定义的局部变量,且是倒着存的,5先入栈存到[ebp-4],接着是4入栈[ebp-8],以此类推。所以arr[0]应该是[ebp-14h]这个地址,arr[4]应该是[ebp-4]这个内存地址,那么如果此时有值想存入arr[6],就等于将值存入[ebp+4]这个内存空间中,这个地址中我们知道存放的是Func函数的返回地址,所以如果此时将HelloWorld函数所在的起始地址存入arr[6],那么Func函数执行完后将会跳转到HelloWorld函数的起始地址开始执行此函数,但是由于进入helloworld函数的方式是非正常的,即不是用call执行的,那么helloworld函数最后执行完getchar(),没有地址可以返回,即程序不知道跳到哪里去,于是就会报错
-
-
分析永不停止的Helloworld函数
void Helloworld() { int i; int arr[5] = {0}; for(i=0;i<=5;i++) { arr[i] = 0; printf("Hello World!\n"); } } int main(int argc,char* argv[]){ Helloworld(); return 0; }day01.3-堆栈图中详细分析过此死循环的原因,这里简单概述一下:
本文详细介绍了C语言中变量的声明与赋值,全局变量与局部变量的区别,以及它们在内存中的分配。讨论了类型转化的规则,包括有符号与无符号数据类型的转化,并通过实例解释了不同类型数据运算时的处理方式。此外,还分析了表达式的特性,特别是涉及不同类型数据运算时的扩展规则。最后,提到了关系运算符、逻辑运算符和单目运算符的反汇编实现。文章通过实际代码和反汇编示例,帮助理解C语言底层运算的细节。
1078





