1.基本数据类型表现
整数就不说了
浮点数:
1.浮点数分为float和double类型, 在内存中使用IEEE标准编码. float占4字节, double占8字节
2.浮点数表示 (float)
3个域: 符号位, 指数部分, 尾数部分
符号位和整数表示一样
指数部分是8bit, 后23bit是尾数部分.
表示规则: 先将浮点数化为2进制并且将小数点移动至科学计数表示形式. 然后看移动小数点方式: 向右移动几位则记下-几,向左移动几位则记下+几. 然后用127加上这个数就是指数部分. 不足8
位向高位补齐. 然后将小数点后面的内容向低位补齐至23位, 然后将 3个域拼接上即可. double类型的指数位是11bit, 42bi是尾数部分
3.字符串,布尔类型不说了
4.地址, 指针, 引用
对于引用, 其实本质上就是指针
引用版:
00411BA0 55 push ebp
00411BA1 8B EC mov ebp,esp
00411BA3 81 EC C0 00 00 00 sub esp,0C0h
00411BA9 53 push ebx
00411BAA 56 push esi
00411BAB 57 push edi
00411BAC 8D BD 40 FF FF FF lea edi,[ebp+FFFFFF40h]
00411BB2 B9 30 00 00 00 mov ecx,30h
00411BB7 B8 CC CC CC CC mov eax,0CCCCCCCCh
00411BBC F3 AB rep stos dword ptr es:[edi]
a=a+13;
00411BBE 8B 45 08 mov eax,dword ptr [ebp+8] ;获取这个引用变量参数值, ebp+8即这个变量的地址
00411BC1 8B 08 mov ecx,dword ptr [eax] ; 获取这个引用变量参数值(地址) 上的值
00411BC3 83 C1 0D add ecx,0Dh ;将值加13
00411BC6 8B 55 08 mov edx,dword ptr [ebp+8] ;获取这个引用变量参数值, ebp+8即这个变量的地址
00411BC9 89 0A mov dword ptr [edx],ecx ;将修改后的值覆盖过去
指针版
00411BA0 55 push ebp
00411BA1 8B EC mov ebp,esp
00411BA3 81 EC C0 00 00 00 sub esp,0C0h
00411BA9 53 push ebx
00411BAA 56 push esi
00411BAB 57 push edi
00411BAC 8D BD 40 FF FF FF lea edi,[ebp+FFFFFF40h]
00411BB2 B9 30 00 00 00 mov ecx,30h
00411BB7 B8 CC CC CC CC mov eax,0CCCCCCCCh
00411BBC F3 AB rep stos dword ptr es:[edi]
a=a+13;
00411BBE 8B 45 08 mov eax,dword ptr [ebp+8] ;参数地址取值
00411BC1 83 C0 34 add eax,34h
00411BC4 89 45 08 mov dword ptr [ebp+8],eax
main函数区域:
00411686 8D 45 F4 lea eax,[ebp-0Ch]
00411689 50 push eax
0041168A E8 0A FC FF FF call 00411299
5.#define 和const
const 修饰的变量:
const int a=4;
int* b=(int*)&a;
00411655 8D 45 F8 lea eax,[ebp-8]
00411658 89 45 EC mov dword ptr [ebp-14h],eax
*b=6;
0041165B 8B 45 EC mov eax,dword ptr [ebp-14h]
0041165E C7 00 06 00 00 00 mov dword ptr [eax],6
int x=a;
00411664 C7 45 E0 04 00 00 00 mov dword ptr [ebp-20h],4
这段代码执行后x=4而不是6. 编译器虽然将a用4替换, 但a本身可以被修改.
2.启动函数
xxxcrtstartup. 方法: 断在main函数,栈回溯找到启动函数. 调用堆栈可以看到
入口函数调用顺序, 可以用于识别main函数和入口函数:
3.各种表达式求值(主要看release版本)
加法:
测试代码:
15+20;
int var1=0;
int var2=0;
var1=var1+1;
var1=1=2;
var1=var1+var2
printf("%d",var1);
release版
release 无效代码优化,常量赋值优化,传参优化
减法:
sub指令
计算方式是加上目标值的补码
乘法规则:
尽量转为加法和移位指令,最后考虑imul mul指令
测试: 乘数不为2,4,8的组合 ,release版
除法:
自增,自减
关系运算和逻辑运算
1.表达式短路, 通过判断和跳转实现
2.条件表达式 如表达式1?表达式2:表达式3
方案1: 方案2:
方案3:
方案4: 使用判断,跳转指令实现
编译代码优化:
基本方案:
流水线优化:
4.流程控制语句
if(argc==0)
{printf("=0\n");
....
debug版:
cmp [ebp+8],0
jne 远地址
printf函数调用过程
也就是,如果不相等就跳过if下面的成功需要执行的代码, 符合汇编代码的顺序线性结构
if(argc==0)
printf("==0");
else
printf("!=0");
.....
debug:
cmp [ebp+8],0
jne c1
==0的printf调用
jmp c1 后面代码
c1代码
c1后面代码
形成了2个过程交叉形式的代码.
if else 语句形成了多个过程交叉, 条件跳转和jmp
switch
1.当分支小于4的时候将和if else一样
2.当分支是数字并且顺序序列:
以某个地址作为形成一个虚拟线性地址表,直接算出偏移即可到某个分支处,分支执行完后将jmp跳出整个switch区域
3..当分支是数字并且顺序序列外加一个不存在的case或者default(顺序断层), 这些个分支将在一个地方. 还是通过线性地址表计算相应的case地址,
执行完后jmp出去 .这2种情况debug和release一样
4.当分支是数字并且顺序序列外加一个不存在的case或者default(顺序断层)并加一个大数. 这时可以对齐到0并和最高值比,大于(包括负数)直接跳出.否则通过2个表:非线性索引表和
非线性地址表.
5.分支是数字并且乱序,则构建数型结构,中间值先作为根节点
循环: do while, while, for
do通过条件跳转向上跳
while:一上来就判断,执行完循环代码后jmp到比较并判断处
for: 4个部分: 赋初值(循环变量赋初值并jmp到循环体, 步长计算(第一个被赋初值跳过),条件比较条件跳转,循环体最后向上jmp到计算步长部分
release版本优化成do
5.函数
1.调用方式
__cdecl 不定参数
__stdcall 定参数,被调方平衡堆栈.
__fastcall: ecx,ebx 传递第1,2个参数,其他参数通过栈传递
2.函数开始
一般保存上一个函数栈,push ebx,push esi, push edi. 提升栈顶, 取出栈顶地址.
debug版在返回前进行栈平衡检测_-chkesp函数
3.调用方式优化
连续调用同一函数将统一一次平衡栈. 调用方式是_-cdecl
优化通过esp寻址. IDA通过变量var作为定位局部变量
4.函数返回值
正常以eax返回
如果是结构体变量则通过多个寄存器返回结构体各个成员
6.变量存储方式
1.全局变量和常量都在pe中. 都是通过立即数访问的
2.局部静态变量
这个和全局变量一样,存储在pe数据段中. 但是在同一个函数定义多个局部静态变量多次赋值值不变: 在第一个局部静态变量地址附近有一个1字节的区域用于存储局部静态变量
的是否初始化标志. 在定义的函数中,将一开始对eax清0,然后获取标志到al中,再对其与一个数字进行与运算.这个数字与这个局部静态变量的顺序有关. 然后就可以判断是否已被初始化
了,然后通过一些条件跳转再进入函数的其他地方去.
但是如果该局部静态变量被初始化一个常量将作为全局变量一样.通过立即数访问. 作用域是编译器实现的
3.堆变量
7.数组和指针寻址
1.数组初始化在局部变量区域进行一个个赋值. 对于字符数组,字符串初始化 对字符串(作为常量在数据区中)每4个字节进行赋值,最后多出的部分以2,1字节赋值
2.数组作为参数:传递数组首地址. 注意:在函数中sizeof数组参数时将为4
3.strlen与strcpy 优化的内联模式前者保存在ecx中,后者将复制目标地址存放在edi中
4.数组作为返回值: 将数组首地址返回,但是可能会导致函数返回后该数组被破坏. 警告中会提示
5.全局数组和静态数组和常量访问,存储一样.
6.数组寻址, 基址+偏移
7.多维数组, 连续存放. 寻址:多次利用下标和分量计算偏移
8.指针数组和数组指针
前者和二维指针类似, 后者是指针取值后是个低阶数组,是个数组类型的指针
9.函数指针.
8.结构体和类
1.对象
对象名只是个虚拟的东西,在内存中其实不存在,但可以求它的地址. 地址是该对象在内存成员首地址. 对象成员依次存放.
2.内存对齐vc6默认以8字节对齐.对于结构体和类有效, 实际对齐字节: min(max(成员),编译器要求的对齐). 通过#pragma pack(N)修改编译器对齐
3.this 指针: 在成员函数中调用是会通过ecx传入一个隐藏的参数,就是this.,如果强制将调用方式改为_-stdcall则在汇编层面通过栈传递对象地址
4.静态数据成员. 和其他静态变量一样,存放在.data中,只是赋予了特殊作用域
5.对象作为参数.传递对象的各个成员. 通过栈, 如果成员中包含数组,则传递改成员地址
6.对象作为返回值:将对象各个成员复制到主调函数栈中作为局部变量.如果将返回值赋值给一个定义的成员,则取得该对象地址后将成员数据复制过去.
- 以上2中方式都可能导致数据异常,在调用析构函数时可能会出现多次释放内存. 所以一般使用指针或引用
9.构造函数和析构函数
局部对象:当运行到定义对象时即调用构造函数,并且通过ecx寄存器传递该对象的地址. 最后虽然没有返回值但还是将该对象地址通过eax寄存器返回
堆对象: 先申请空间.,然后2分支判断是否失败.成功才调用构造函数
参数对象: 参数传递时将自动调用拷贝构造.默认的构造会将所有数据复制过去
返回对象: 调用方将局部对象地址作为栈参数传过去.当被调函数创建了一个局部对象后将该对象地址压栈,作为参数并取到调用方对象地址 调用拷贝构造函数.最后将地址(ebp+8)作为
eax参数返回.如果通过指针接收函数创建的局部对象将局部对象作为返回值
全局对象和静态对象: 在程序进行时,main函数调用前会进行一些调用,这时将在_cinit函数对象全局对象进行构造
2.默认构造函数的问题:
本类定义了虚函数或者父类中的成员或定义了虚函数. :因为需要初始化虚函数表,这个过程在构造函数中完成
父类或本类定义的成员对象带有构造函数.
3.析构函数作用域结束, 释放堆, 函数退出, 返回对象,main函数返回
4.堆对象:析构代理函数:调用前检查指针.如果为0跳过调用. 压入标记.. 压入对象个数,对象大小
10虚函数
虚表地址放在pe文件中
1.默认构造函数初始化虚表指针: 取到对象首地址将虚表地址存放到首地址上面
2.析构函数还对虚表指针通过虚表地址再赋值一次
3.虚函数识别
特征: 对成员首地址处赋值,并且赋的值是某个数组的地址. 前者在栈中,后者在004...... 地址处 固定不变. 数组中每个元素都是函数指针
4.全局对象虚函数和虚函数内联: 对某个全局变量或者常量(引用方式)进行取地址, 压栈. 可能在进行虚函数调用