C语言逆向分析语法超详细分析
ch132 人气:0基本数据类型
在c中基本数据类型分为:char,short,int,long,float,double
以上数据类型除float和double外均可以分为有符号(singed)和无符号(unsigned)两类
有符号时最高位为符号位,用来表示数据的正负
无符号情况下最高位为正常的数据位不做特殊含义
类型 | 占位 |
---|---|
Char | 1 |
short | 2 |
int | 4 |
long | 8 |
float | 4 |
Double | 8 |
浮点数类型的存储
浮点数类型是比较特殊的,首先他是交给专门的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
字符类型的存储
字符类型是根据字符的编码格式将对应字符的数字表示存储为二进制。
具体的字符编码解析可以划到底部
指针和引用类型
在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
在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
可以看到引用类型和指针类型操作编译为汇编其实是基本一样的,区别就在于指针类型变量所存储的地址也可以进行算术运算
举例:
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
常量数据类型
常量类型表示在程序运行前便已久可以确认的数据,一般存储在只读数据区,这块内存在页的属性上便是不可写只可读,所以对这段内存的写操作都会抛出内存访问异常。
常量举例:如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 }
从上面的例子可见函数的调用便是从代码段中的一块跳转到另一块去执行,在执行结束后再返回,
函数的参数是通过栈来传递的,在函数结束后要重新保证栈回退到调用函数之前的状态。
其次call命令可以分为两个部分
- 压入当前地址作为函数调用结束后回退时用
- jmp到对应的位置(如果是跨段调用则是jmp far)
函数调用的约定分为三类
- stdcall:标准的winapi调用约定平栈操作交给函数自行处理,通过ret arg来实现
- cdecl:c语言调用约定,平栈操作交给调用方实现,也就是上面例子中的调用
- 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; }
首先建立了一个结构体有三个参数
先来看一下结构体在内存中的存储方式
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 。。。。。 }
可以看出来结构体在内存中的存储方式便是将数据按顺序排放在内存中并根据字段类型的大小计算偏移量来取得对应的字段内容
如果我们直接将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; }
改后的代码,完全可以运行
并且如果看返汇编的话会发现汇编代码也没有变化
下面我们将函数放到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 。。。。。。。。 }
注意 lea ecx,[s] 这段代码,这个ecx便是所谓的this指针,通过编译器将结构体自己的地址作为参数传入函数这样就可以通过this符号访问结构体自己了。其余的部分完全没有变化,调用class的函数时也是通过地址调用的。
注意:数据在内存中的存储还取决于数据对齐,这部分的知识在我前面的笔记中有详细解析
面向对象的特性
面向对象的特性有
- 封装
- 继承
- 多态
封装在上一块我们已经看过了,便是将操作数据的算法和存放数据的结构体封装到一起来调用,真正的实现通过编译器来实现。
下面说一下
字符编码
计算机中的存储是以字节为单位的,能反映的也仅仅是数字而已,为了能够用数字将文字信息反映出来人们设计出了各种字符编码表,将数字与文字对应。
1、ASCI编码
1.原始Ascii编码
原始ASCI使用1到127(0X00~0X7F)来对应常用的一些字母等文本,127到255则是扩展到一些不常用的类似于=号这种内容。
但原始ASCI所支持的字符仅仅能够反映英文国家的使用场景。
2.ASCI扩展编码
对于某些地区是无法用原始ASCI编码来反映当地的语言的,所以就有了ASCI扩展编码的这种形式。
扩展ASIC编码将不常用的127到255的(0x80~0xFF)位置采用两个数字对应一个文字的方式
如:可能128和129代表一个中,129和130代表一个国
呢么中国对应的编码就是:0x 8081 8182
例如国内常用的GBK、GB2312和台湾的big5等编码方式都是采取的此类
但是这种编码方式有一个问题,就是他占用了ASCI表的127到255的位置并且不同的地区这部分的编码均不一样,呢么国内的中文文件发到国外采用了不同的编码去读取则会出现乱码
2、Unicode编码
Unicode编码就是为了解决ASCii扩展码在不同地区的实现下解码后对应不同的文字这个问题
Unicode编码将全世界常用的符合都构建到一个表中,这个表的范围是:0x10 FF FF到0
Unicode仅仅提供了一个表,他并没有对存储做过多的要求,而下面所说的utf-8和utf-16以及现在的utf-32则是对Unicode编码存储方式的不同实现
1.unicode编码实现(utf-8)
utf-8较之utf-16在存储上更为复杂,但是所占空间是更小的,这也是网络传输大多为utf-8的格式的原因
存储规则:
如果目标符合在Unicode中为:0到00007F则会被编码为:0xxx xxxx
如果目标符合在Unicode中为:80到00007FF则会被编码为:110xx xxx 10xx xxxx
如果目标符合在Unicode中为:800到00FFFF则会被编码为:1110 xxxx 10xx xxxx 10xx xxxx
如果目标符合在Unicode中为:10000到10FFFF则会被编码为:1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx
2.unicode编码实现(utf-16)
utf-16名如其意,就是以两个byte为单位进行存储。
举例:如果说在Unicode编码中 中对应0x10 61 62
那么采用utf-16存储在文件中的byte就是 0x 0010 6162
加载全部内容