• c/c++基本语法逆向分析


    c/c++基本语法逆向分析

    基本数据类型

    在c中基本数据类型分为:char,short,int,long,float,double

    以上数据类型除float和double外均可以分为有符号(singed)和无符号(unsigned)两类

    有符号时最高位为符号位,用来表示数据的正负

    无符号情况下最高位为正常的数据位不做特殊含义

    类型占位
    Char1
    short2
    int4
    long8
    float4
    Double8

    浮点数类型的存储

    浮点数类型是比较特殊的,首先他是交给专门的cpu来处理的,比如在80386中就引入了8087协处理器来专门处理浮点数的计算

    C中的浮点数存储方式采用了浮点实数存储方式,也就是在全部二进制位上选取一段用来表示实数另一段表示小数点的位置,如952.7可以分为9527和0.1

    C的浮点数的编码采用的是ieee标准编码格式

    如float类型下将浮点数分为三部分:符号位(1bit)、小数位(8bit)、实数位(23bit)

    double:符号位(1bit)、小数位(11bit)、实数位(52bit)

    举例:12.25f拆分:符号位:0

    ​ 小数位:1000 0010

    ​ 实数位:10001 后续均为0

    字符类型的存储

    字符类型是根据字符的编码格式将对应字符的数字表示存储为二进制。

    具体的字符编码解析可以看我另外一篇文章:https://blog.csdn.net/qq_43147121/article/details/127968159

    指针和引用类型

    在C中用指针类型(TYPE*)来表示一个用来存储一个地址的DWORD类型,用&符号来表示取一个变量的地址

    如:int* a;此时a则会被认为是一个指针类型,在对a进行操作时则会被编译器编译为汇编中的间接操作

    举例:

    int tmp = 10;
    int* a = &tmp;
    (*a)+=1;
    
    对应的汇编简单来写如下:
    mov dword ptr [esp],0Ah;           将10存在栈中
    lea eax,[esp]						取得tmp所在的地址
    mov dword ptr [esp-4],eax					将tmp所在的地址存储到栈中
    mov ecx,dword ptr [esp-4]					取出tmp所在的地址
    mov ecx,dword ptr [eax]
    add dword ptr ecx,1								将tmp所在地址所指向的内容加一
    mov dword ptr[eax],ecx
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在c中以引用类型(type&)来表示一个操作的集合,每次对这个引用类型的操作都是取变量的内容将内容作为地址修改此地址中的数据并写回的一个操作的集合

    举例:

    int tmp = 10;
    int& a = &tmp;
    a+=1;
    对应的汇编简单来写如下:
    mov dword ptr [esp],0Ah;           将10存在栈中
    lea eax,[esp]						取得tmp所在的地址
    mov dword ptr [esp-4],eax					将tmp所在的地址存储到栈中
    mov ecx,dword ptr [esp-4]					取出tmp所在的地址
    mov ecx,dword ptr [eax]
    add dword ptr ecx,1								将tmp所在地址所指向的内容加一
    mov dword ptr[eax],ecx
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以看到引用类型和指针类型操作编译为汇编其实是基本一样的,区别就在于指针类型变量所存储的地址也可以进行算术运算

    举例:

    int tmp = 10;
    int* a = &tmp;
    a++;
    
    对应的汇编简单来写如下:
    mov dword ptr [esp],0Ah;           将10存在栈中
    lea eax,[esp]						取得tmp所在的地址
    mov dword ptr [esp-4],eax					将tmp所在的地址存储到栈中
    mov eax,dword ptr [esp-4]		
    add eax,4												此时加的不在是1而是当前指针所表示类型的大小
    mov dowrd ptr [esp-4],eax
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    常量数据类型

    常量类型表示在程序运行前便已久可以确认的数据,一般存储在只读数据区,这块内存在页的属性上便是不可写只可读,所以对这段内存的写操作都会抛出内存访问异常。

    常量举例:如define所定义的常量,或者char* str = “ABC”;这种方式所定义的字符串。

    注意const修饰符所修饰的变量并不意味着是在内存层面上的常量,他仅仅是编译器会在编译过程中进行检测,在程序运行中完全可以通过取地址并修改的间接修改方式对其内存数据进行修改。

    函数

    在内存的识图中并没有函数这一个说法只存在段的层级每个段都有自己的内存属性可读可写可执行等待,函数的目的便是能够将某一段内存明确的用一种概念来分开,而不至于将全部的代码片段都混杂在一段内存中而没有明确的一个分界和定义。

    函数简单的来看便是将一块代码封装到一起。下面直接反汇编一个函数的调用看一下

    首先要说明的是ebp代表了栈底指针,esp代表了栈顶指针

    c代码

    int test(int a,int b){
    	return a+b;
    }
    
    int _tmain(int argc, _TCHAR* argv[])
    {
    	int a=10,b=1;
    	int res = test(a,b);
    	printf("%d",res);
    	return 0;
    }
    
    简单汇编代码:
    
    int test(int a,int b){
    009D1A50  push        ebp  								//同样是保存和初始化堆栈
    009D1A51  mov         ebp,esp 
    009D1A53  sub         esp,0C0h 
    009D1A59  push        ebx  
    009D1A5A  push        esi  
    009D1A5B  push        edi  
    009D1A5C  lea         edi,[ebp-0C0h] 			
    009D1A62  mov         ecx,30h 
    009D1A67  mov         eax,0CCCCCCCCh 
    009D1A6C  rep stos    dword ptr es:[edi] 
    	return a+b;
    009D1A6E  mov         eax,dword ptr [a] 		//取出将a,b做合
    009D1A71  add         eax,dword ptr [b] 		//此处的a是ebp+4h,b是ebp+8h
    }
    009D1A74  pop         edi  
    009D1A75  pop         esi  
    009D1A76  pop         ebx  
    009D1A77  mov         esp,ebp 
    009D1A79  pop         ebp  									//回退堆栈
    009D1A7A  ret     													//返回
    
    int _tmain(int argc, _TCHAR* argv[])
    {
    009D1AF0  push        ebp  								//保存ebp
    009D1AF1  mov         ebp,esp 						//将栈底指向当前栈顶
    009D1AF3  sub         esp,0E4h 						//提升堆栈
    009D1AF9  push        ebx  								//保存寄存器
    009D1AFA  push        esi  
    009D1AFB  push        edi  
    009D1AFC  lea         edi,[ebp-0E4h] 			//初始化堆栈内容
    009D1B02  mov         ecx,39h 
    009D1B07  mov         eax,0CCCCCCCCh 
    009D1B0C  rep stos    dword ptr es:[edi] 
    	int a=10,b=1;														//这里开始进入我们在main中写的代码
    009D1B0E  mov         dword ptr [a],0Ah 	//a其实是ebp-4h,这里将10存入到ebp-4,也就是栈底的第																							一个4字节内存
    009D1B15  mov         dword ptr [b],1 		//这里同上b是ebp-8h,将1放入栈底开始的第二个4字节中
    	int res = test(a,b);										//下面要注意,下面压栈是从esp开始压栈,前面的通过ebp																					所操作的赋值语句是将内容存放到开始提升堆栈所占有的内存
    009D1B1C  mov         eax,dword ptr [b] 	//这里是取出1到eax
    009D1B1F  push        eax  								//将eax压栈
    009D1B20  mov         ecx,dword ptr [a] 	//取出10到ecx
    009D1B23  push        ecx  								//ecx压栈
    009D1B24  call        func (9D126Ch) 			//调用我们的test方法此时可以看做一个																														jmp详细的后续再讲
    009D1B29  add         esp,8 							//平衡传入参数时提升的堆栈
    009D1B2C  mov         dword ptr [res],eax //eax便是返回值
    	printf("%d",res);
    009D1B2F  mov         esi,esp 
    009D1B31  mov         eax,dword ptr [res] 
    009D1B34  push        eax  
    009D1B35  push        offset string "%d" (9D774Ch) 
    009D1B3A  call        dword ptr [__imp__printf (9DA40Ch)] 
    009D1B40  add         esp,8 
    009D1B43  cmp         esi,esp 
    009D1B45  call        @ILT+435(__RTC_CheckEsp) (9D11B8h) 
    	return 0;
    009D1B4A  xor         eax,eax 
    }
     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    从上面的例子可见函数的调用便是从代码段中的一块跳转到另一块去执行,在执行结束后再返回,

    函数的参数是通过栈来传递的,在函数结束后要重新保证栈回退到调用函数之前的状态。

    其次call命令可以分为两个部分

    1. 压入当前地址作为函数调用结束后回退时用
    2. jmp到对应的位置(如果是跨段调用则是jmp far)

    函数调用的约定分为三类

    1. stdcall:标准的winapi调用约定平栈操作交给函数自行处理,通过ret arg来实现
    2. cdecl:c语言调用约定,平栈操作交给调用方实现,也就是上面例子中的调用
    3. fastcall:参数通过寄存器传递,如eax,ebx

    结构体和类

    结构体就是将一系列数据整合到一起的一块内存,下面通过例子来看一下

    struct test_struct{
    	int a;
    	char b;
    	int c;
    };
    int _tmain(int argc, _TCHAR* argv[])
    {
    	struct test_struct s;
    	s.a = 10;
    	s.b = 11;
    	s.c = 12;
    	test(&s);
    	return 0;
    }
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    首先建立了一个结构体有三个参数

    先来看一下结构体在内存中的存储方式

    int _tmain(int argc, _TCHAR* argv[])
    {
    。。。。。。。
    	struct test_struct s;
    	s.a = 10;
    00E524DE  mov         dword ptr [s],0Ah 			//这里的s可以简单看为ebp-4
    	s.b = 11;
    00E524E5  mov         byte ptr [ebp-0Ch],0Bh 
    	s.c = 12;
    00E524E9  mov         dword ptr [ebp-8],0Ch 
    	test(&s);
    00E524F0  lea         eax,[s] 						//lea为取地址的指令,前面我们也遇到过
    00E524F3  push        eax  								//将这个地址作为参数传递
    00E524F4  call        test (0E511B8h) 
    00E524F9  add         esp,4 
    	return 0;
    00E524FC  xor         eax,eax 
    。。。。。
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    可以看出来结构体在内存中的存储方式便是将数据按顺序排放在内存中并根据字段类型的大小计算偏移量来取得对应的字段内容

    如果我们直接将struct关键字改为class看看会不会出错

    class test_struct{
    public:
    	int a;
    	char b;
    	int c;
    };
    
    void test(test_struct* s){
    	printf("%d",s->a);
    }
    
    int _tmain(int argc, _TCHAR* argv[])
    {
    	test_struct s;
    	s.a = 10;
    	s.b = 11;
    	s.c = 12;
    	test(&s);
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    改后的代码,完全可以运行

    并且如果看返汇编的话会发现汇编代码也没有变化

    下面我们将函数放到class中看一下汇编是否会有变化

    class test_struct{
    public:
    	int a;
    	char b;
    	int c;
    	
    	void test(test_struct* s){
    	printf("%d",s->a);
    }
    };
    
    
    int _tmain(int argc, _TCHAR* argv[])
    {
    	test_struct s;
    	s.a = 10;
    	s.b = 11;
    	s.c = 12;
    	s.test(&s);
    	return 0;
    }
    
    汇编只看main这部分的代码
      
      int _tmain(int argc, _TCHAR* argv[])
    {
    。。。。。
    	test_struct s;
    	s.a = 10;
    00D3339E  mov         dword ptr [s],0Ah 
    	s.b = 11;
    00D333A5  mov         byte ptr [ebp-0Ch],0Bh 
    	s.c = 12;
    00D333A9  mov         dword ptr [ebp-8],0Ch 
    	s.test(&s);
    00D333B0  lea         eax,[s] 
    00D333B3  push        eax  
    00D333B4  lea         ecx,[s] 
    00D333B7  call        test_struct::test (0D311D6h) 
    	return 0;
    00D333BC  xor         eax,eax 
      。。。。。。。。
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    注意 lea ecx,[s] 这段代码,这个ecx便是所谓的this指针,通过编译器将结构体自己的地址作为参数传入函数这样就可以通过this符号访问结构体自己了。其余的部分完全没有变化,调用class的函数时也是通过地址调用的。

    注意:数据在内存中的存储还取决于数据对齐,这部分的知识在我前面的笔记中有详细解析

    面向对象的特性

    面向对象的特性有

    1. 封装
    2. 继承
    3. 多态

    封装在上一块我们已经看过了,便是将操作数据的算法和存放数据的结构体封装到一起来调用,真正的实现通过编译器来实现。

    下面说一下继承

    首先写一个结构体

    异常处理

  • 相关阅读:
    (生物信息学)R语言绘图初-中-高级——3-10分文章必备——热图(初级)
    十万访问量的背后。。。
    php7.3 centos7.9安装sqlserver扩展
    二叉树基础知识总结
    elemtui 提示消息element-ui message被遮罩层覆盖解决方法
    滴滴发布十一大数据:延边出行需求上涨280% 西部省份成旅游热点
    vant 日历组件优化
    【机器学习】svm
    .NET WebAPI 使用 GroupName 对 Controller 分组呈现 Swagger UI
    实战二十六:基于字符串匹配的实体对齐任务 代码+数据 (可作为毕设)
  • 原文地址:https://blog.csdn.net/qq_43147121/article/details/127999220