滴水三期:day11.1-C正向基础

本文详细介绍了C语言中变量的声明与赋值,全局变量与局部变量的区别,以及它们在内存中的分配。讨论了类型转化的规则,包括有符号与无符号数据类型的转化,并通过实例解释了不同类型数据运算时的处理方式。此外,还分析了表达式的特性,特别是涉及不同类型数据运算时的扩展规则。最后,提到了关系运算符、逻辑运算符和单目运算符的反汇编实现。文章通过实际代码和反汇编示例,帮助理解C语言底层运算的细节。

一、变量的声明和赋值

  • 声明变量:就是告诉计算机,我要用一块内存,你给我留着,宽度存储格式由数据类型决定

  • 计算机何时给变量分配内存:取决于变量的作用范围;如果是全局变量,在程序编译完就已经分配了空间,如果是局部变量,只有所在的程序被调用的时候,才会分配空间

  • 全局变量如果不赋初始值默认是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

      image-20211206092759085 image-20211206092823991

      image-20211206093036609 image-20211206093054449

    • 而如果char转int,而此时char为0x73,符号位为0,则高位会补0,类型转化完变成0x00000073

      void Func(){
      	char c = 0x73;
      	int i = c;
      }
      
      image-20211206093504078

    无符号也用同样的方法查看,就不详细说明了,直接总结

    • 所以如果有符号小转大先符号扩展,再传送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

      image-20211206094428994

三、表达式

  • 特点一:表达式无论多么复杂,都只有一个结果

  • 特点二:只有表达式,可以编译通过,但并不生成汇编指令,需要与赋值或者其他流程控制语句一起组合的时候才有意义

  • 特点三:当表达式中存在不同宽度的变量时,结果将转换为宽度最大的那个(但是看反汇编会发现char和short做运算,也是扩展成32位做运算,感觉特点三总结的有问题)

    void Func(){
    	short c = 10;
    	int i = 20;
    	printf("%d",c+i);   //c+i最后的宽度应该为int的宽度,即4字节
    }
    

    可以发现在不同宽度的数据类型做运算前,反汇编指令先会把低宽度的数据类型做扩展,扩展成高宽度,再做运算,且由于默认是有符号数,所以使用MOVSX扩展

    image-20211206095926464
  • 特点四:当表达式中同时存在有符号和无符号数的时候,表达式的结果将转换为无符号数。这里只是计算机认为此时应该为无符号数,但是至于对结果的输出或者再次运算或者赋值等操作应该被当成有符号数还是无符号数应该由我们自己决定,比如我想把它定义为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,这个结论明显不正确!!

    屏幕截图 2021-12-06 112354

    如果我们将结果赋给一个我们规定了数据类型和宽度的变量,那么结果会按照什么宽度存储和输出呢?比如:

    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
    }
    
    屏幕截图 2021-12-06 112632

    下面再举几个例子综合说明一下结果是有符号还是无符号是我们自己决定的!!同时涉及到有无符号、类型转化也是如此:

    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
    }
    
    image-20211206125501767
  • 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");	
    	}	
    }
    
    D8B6EF9717E8B1EAC45137AF0AD54840

    可以看到像这种连续判断,只要有一个不满足条件就不用在比较了,直接跳转到printf error,这种就是&&运算;如果全部满足才打印ok

  • ||

    void Func(int x,int y,int z)			
    {
    	if(x<1 || y>1 || z>1)	
    	{
    		printf("OK");
    	}
    	else
    	{		
    		printf("Error");	
    	}	
    }
    
    6F1B1EA35622487B988E5E2646BDB5DC

    可以看到三个表达式一个不满足条件没事,继续往后比较,如果最后一个还不满足条件,则跳转打印error;只要满足一个条件,就打印ok

  • !

    void Func(int x,int y,int z)			
    {
    	if(x!=1)	
    	{
    		printf("OK");
    	}
    	else
    	{		
    		printf("Error");	
    	}	
    }
    
    image-20211206132812951

    !就反汇编就是je,反着的如果相等则跳转执行error,若不不相等则跳转执行ok

3.单目与三目运算符

  • 单目运算符:++--+=-=

    • 反汇编如下:

      void Func()	
      {	
      	int i = 10; 
      	int k = ++i;       //先i+1,再把i的值赋给k
      	printf("%d-%d\n",i,k);
      }
      
      image-20211206134251300

      先add操作,再赋值

      void Func()	
      {	
      	int i = 10; 
      	int k = i++;       //先把i的值赋给k,i再+1
      	printf("%d-%d\n",i,k);
      }
      
      image-20211206134450586

      先做的赋值操作,再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);
      }
      
      image-20211206134758208

      其实就是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(),没有地址可以返回,即程序不知道跳到哪里去,于是就会报错

      image-20211206143031717
  • 分析永不停止的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-堆栈图中详细分析过此死循环的原因,这里简单概述一下:

    FE3096DDD59B24B183491CF921F837F7
1讲:2015-01-12(进制01) 第2讲:2015-01-13(进制02) 第3讲:2015-01-14(数据宽度-逻辑运算03) 第4讲:2015-01-15(通用寄存器-内存读写04) 第5讲:2015-01-16(内存寻址-堆栈05) 第6讲:2015-01-19(EFLAGS寄存器06) 第7讲:2015-01-20(JCC) 第8讲:2015-01-21(堆栈图) 第8讲:2015-01-21(宝马问题) 第9讲:2015-01-22(堆栈图2) 第10讲:2015-01-23(C语言01_后半段) 第10讲:2015-01-23(C语言完整版) 第11讲:2015-01-26(C语言02_数据类型) 第12讲:2015-01-27(C语言03_数据类型_IF语句) 第13讲:2015-01-28(C语言04_IF语句逆向分析上) 第14讲:2015-01-28(C语言04_IF语句逆向分析下) 第15讲:2015-01-29(C语言04_正向基础) 第16讲:2015-01-30(C语言05_循环语句) 第17讲:2015-02-02(C语言06_参数_返回值_局部变量_数组反汇编) 第18讲:2015-02-02(2015-01-30课后练习) 第19讲:2015-02-03(C语言07_多维数组) 第20讲:2015-02-03(2015-02-02课后练习) 第21讲:2015-02-04(C语言08_结构体) 第22讲:2015-02-05(C语言09_字节对齐_结构体数组) 第23讲:2015-02-06(C语言10_Switch语句反汇编) 第24讲:2015-02-26(C语言11_指针1) 第25讲:2015-02-27(C语言11_指针2) 第26讲:2015-02-28(C语言11_指针3) 第27讲:2015-02-28(C语言11_指针4) 第28讲:2015-03-02(C语言11_指针5) 第29讲:2015-03-03(C语言11_指针6) 第30讲:2015-03-04(C语言11_指针7) 第31讲:2015-03-06(C语言11_指针8) 第32讲:2015-03-09(位运算) 第33讲:2015-03-10(内存分配_文件读写) 第34讲:2015-03-11(PE头解析_手动) 第35讲:2015-03-12(PE头字段说明) 第36讲:2015-03-13(PE节表) 第37讲:2015-03-16(FileBuffer转ImageBuffer) 第38讲:2015-03-17(代码节空白区添加代码) 第39讲:2015-03-18(任意节空白区添加代码) 第40讲:2015-03-19(新增节添加代码) 第41讲:2015-03-20(扩大节-合并节-数据目录) 第42讲:2015-03-23(静态连接库-动态链接库) 第43讲:2015-03-24(导出表) 第44讲:2015-03-25(重定位表) 第45讲:2015-03-26(移动导出表-重定位表) 第46讲:2015-03-27(IAT表) 第47讲:2015-03-27(导入表) 第48讲:2015-03-30(绑定导入表) 第49讲:2015-03-31(导入表注入) 第50讲:2015-04-01(C++ this指针 类 上) 第51讲:2015-04-01(C++ this指针 类 下) 第52讲:2015-04-02(C++ 构造-析构函数 继承) 第53讲:2015-04-03(C++ 权限控制) 第54讲:2015-04-07(C++ 虚函数表) 第55讲:2015-04-08(C++ 动态绑定-多态-上) 第56讲:2015-04-08(C++ 动态绑定-多态-下) 第57讲:2015-04-09(C++ 模版) 第58讲:2015-04-10(C++ 引用-友元-运算符重载) 第59讲:2015-04-13(C++ new-delete-Vector) 第60讲:2015-04-14(C++Vector实现) 第61讲:2015-04-15(C++链表) 第62讲:2015-04-16(C++链表实现) 第63讲:2015-04-16(C++二叉树) 第64讲:2015-04-17(C++二叉树实现) 第65讲:2015-04-20(Win32 宽字符) 第66讲:2015-04-21(Win32 事件-消息-消息处理函数) 第67讲:2015-04-22(Win32 ESP寻址-定位回调函数-条件断点) 第68讲:2015-04-23(Win32 子窗口-消息处理函数定位) 第69讲:2015-04-24(Win32 资源文件-消息断点) 第70讲:2015-04-27(Win32 提取图标-修改标题) 第71讲:2015-04-28(Win32 通用控件-VM_NOTIFY) 第72讲:2015-04-29(Win32 PE查看器-项目要求) 项目一:PE查看器 开发周期(5天) 需求文档 第73讲:2015-05-07(Win32 创建线程) 第74讲:2015-05-08(Win32 线程控制_CONTEXT) 第75讲:2015-05-11(Win32 临界区) 第76讲:2015-05-12(Win32 互斥体) 第77讲:2015-05-13(Win32 事件) 第78讲:2015-05-14(Win32 信号量) 第79讲:2015-05-15(Win32 线程同步与线程互斥) 第80讲:2015-05-18(Win32 进程创建_句柄表) 第81讲:2015-05-20(Win32 以挂起形式创建进程) 第82讲:2015-05-21(Win32 加密壳_项目说明) 项目二:加密壳 开发周期(5天) 需求文档 第83讲:2015-05-28(Win32 枚举窗口_鼠标键盘事件) 第84讲:2015-05-29(Win32 CE练习) 第85讲:2015-06-01(Win32 OD练习) 第86讲:2015-06-03(Win32 ShellCode_远程线程注入) 第87讲:2015-06-04(Win32 加载EXE_模块隐藏) 第88讲:2015-06-09(Win32 IAT_HOOK) 第89讲:2015-06-10(Win32 InlineHook) 第90讲:2015-06-11(Win32 进程通信) 第91讲:2015-06-11(Win32 进程监控_项目说明) 项目三:进程监控 开发周期(5天) 需求文档 第92讲:2015-06-15(硬编码_01) 第93讲:2015-06-16(硬编码_02) 第94讲:2015-06-17(硬编码_03) 第95讲:2015-06-18(硬编码_04) 第96讲:2015-06-19(硬编码_05)
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值