基础部分
- 裸函数:编译器不管,不自动生成ret
void __declspec(naked) Function()
{
__asm
{
ret
}
}
-
常见的几种调用约定:
-
程序入口
main 函数的识别:main 函数被调用前要先调用的函数如下
GetVersion()
_heap_init()
GetCommandLineA()
_crtGetEnvironmentStringsA()
_setargv()
_setenvp()
_cinit()
这些函数调用结束后就会调用main 函数,根据main函数调用的特征,寻找将3个参数压入栈内作为参数的函数。 -
学习数据类型的三个要素:
(1)存储数据的宽度
(2)存储数据的格式
(3)作用范围(作用域) -
整数类型:char short int long
char 8BIT 1字节
short 16BIT 2字节
int 32BIT 4字节
long 32BIT 4字节
整数类型分为有符号(signed)和无符号(unsigned)两种,在内存中存储的方式完全一样;在做运算和比较的时候需要注意 -
浮点类型:float double
将一个float型转化为内存存储格式的步骤为:
(1)先将这个实数的绝对值化为二进制格式
(2)将这个二进制格式实数的小数点左移或右移n位,直到小数点移动到第一个有效数字的右边。
(3)从小数点右边第一位开始数出二十三位数字放入第22到第0位。
(4)如果实数是正的,则在第31位放入“0”,否则放入“1”。
(5)如果n 是左移得到的,说明指数是正的,第30位放入“1”。如果n是右移得到的或n=0,则第30位放入“0”。
(6)如果n是左移得到的,则将n减去1后化为二进制,并在左边加“0”补足七位,放入第29到第23位。
如果n是右移得到的或n=0,则将n化为二进制后在左边加“0”补足七位,再各位求反,再放入第29到第23位。
练习:
1.用__declspec(naked)裸函数实现下面的功能
int plus1(int x, int y, int z)
{
int a = 2;
int b = 3;
int c = 4;
return x+y+z+a+b+c;
}
2.将float类型的12.5 转换成16进制
![](https://i-blog.csdnimg.cn/blog_migrate/a990274509c4916d7555c620edf77324.png)
- 英文字符存储
ASCII 码使用指定的 7 位或 8 位二进制数组合来表示 128 或 256 种可能的字符。
标准 ASCII 码使用 7 位二进制数来表示所有的大写和小写字母,数字 0 到 9、标点符号,以及在美式英语中使用的特殊控制字符。
扩展 ASCII 码允许将每个字符的第 8 位用于确定附加的 128 个特殊符号字符、外来语字母和图形符号。 - 中文字符存储
char* x = “啊”;
天朝专家把那些127号之后的奇异符号们(即EASCII)取消掉,规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。
if语句
- 内存图
- 全局变量的反汇编识别
MOV 寄存器,byte/word/dword ptr ds:[0x12345678]
通过寄存器的宽度,或者byte/word/dword 来判断全局变量的宽度
全局变量就是所谓的基址 - 局部变量的反汇编识别
如: [ebp-4]
[ebp-8]
[esp+4] - 如何判断函数有几个参数,以及分别是什么
可能存在的问题:
1.参数传递未必都是通过堆栈,还可能通过使用寄存器.
2.函数调用处的代码无法查看.
- IF语句的反汇编判断:
执行各类影响标志位的指令
jxx xxxx - IF…ELSE…语句的反汇编判断:
跳转执行一部分代码,不跳转执行另外一部分代码
第一个jxx跳转的地址前面有一个jmp ,可以判断是if…else…语句 - IF…ELSE IF…ELSE IF…多分支语句的反汇编判断:
当每个条件跳转指令要跳转的地址前面都有jmp 指令
这些jmp指令跳转的地址都是一样的
如果某个分支没有条件判断,则为else部分
正向基础
- 类型转换
MOVSX 先符号扩展,再传送。(有符号数)如:
MOV AL,0FF
MOVSX CX,AL
MOV AL,80
MOVSX CX,AL
MOVZX 先零扩展,再传送。(无符号数)如:
MOV AL,0FF
MOVZX CX,AL
MOV AL,80
MOVSX CX,AL
大转小,截取低位,高位舍弃 - 表达式
表达式无论多么复杂,都只有一个结果。
只有表达式,可以编译通过,但并不生成代码,需要与赋值或者其他流程控制语句一起组合的时候才有意义。
当表达式中存在不同宽度的变量时,结果将转换为宽度最大的那个。
当表达式中同时存在有符号和无符号数的时候,表达式的结构将转换为无符号数。
扩展的时候按自身类型扩展。
循环
- do…while语句反汇编
例:
总结:
根据条件跳转指令所跳转到的地址,可以得到循环语句块的起始地址。
根据条件跳转指令所在的地址,可以得到循环语句块的结束地址。
条件跳转的逻辑与源码相同。
- while语句反汇编
WHILE_BEGIN:
影响标志位的指令
jxx WHILE_END ;条件成立跳转到循环语句块结尾处
…
jmp WHILE_BEGIN ;跳转到条件比较处
WHILE_END:
例:
总结:
根据条件跳转指令所跳转到的地址,可以得到循环语句块的结束地址。
根据jmp 指令所跳转到的地址,可以得到循环语句块的起始地址。
在还原while 比较时,条件跳转的逻辑与源码相反。
- for语句反汇编
for(表达式1;表达式2;表达式3)
例:
总结:
第一个jmp 指令之前为赋初值部分.
第一个jmp 指令所跳转的地址为循环条件判定部分起始.
判断条件后面的跳转指令条件成立时跳转的循环体外面
条件判断跳转指令所指向的地址上面有一个jmp ,jmp地址为表达式3的起始位置
参数_返回值_局部变量_数组
- 返回值
本机尺寸:如果本机是32位的,那么对32位的数据支持最好,如果是64位的,那么对64位的支持最好。(?)
编译器遵守了这个规则: char类型或者short类型的参数不但没有节省空间,反而浪费了多余的操作。
结论:整数类型的参数,一律使用int类型。
参数传递的本质:将上层函数的变量,或者表达式的值“复制一份”,传递给下层函数。 - 参数和局部变量
小于32位的局部变量,空间在分配时,按32位(即4字节)分配.
使用时按实际的宽度使用.
不要定义char/short类型的局部变量.
参数与局部变量没有本质区别,都是局部变量,都在栈中分配.
完全可以把参数当初局部变量使用
练习:
- 返回值超过32位时,存在哪里?用long long(__int64)类型做实验。long long类型在VC6中对应的是__int64
__int64 Function()
{
__int64 x = 0x1234567890;
return x;
}
VC6下的反汇编:低位在低位,高位在高位
- char arr[3] = {1,2,3};与 char arr[4] = {1,2,3,4};哪个更节省空间,从反汇编的角度来说明你的观点
答:都分配了四个字节 - 找出下面赋值过程的反汇编代码
void function3()
{
int x = 1;
int y = 2;
int r;
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
r = arr[1];
r = arr[x];
r = arr[x+y];
r = arr[x*2+y];
}
VC6下的反汇编:(眼熟下数组的反汇编)
乘4是因为数组为int型
多维数组
- 基于缓冲区溢出的HelloWorld
void HelloWord()
{
printf("Hello World");
getchar();
}
void Fun()
{
int arr[5] = {1,2,3,4,5};
arr[6] = (int)HelloWord;
}
解释:arr[6]原本为ebp+4,即函数调用后的返回地址,函数名HelloWord是函数void HelloWord()的地址。
- 总结:
一维数组与多维数组在反汇编上没有区别.
数量如果不够,其他的值会补零.
不允许超过维度的最大长度.
可以省略里面的{ }
可以省略第一维的值
结构体_字节对齐
- 结构体做参数或者返回值实质就是内存之间的大量复制,最好不要直接让结构体做参数或者返回值
- 基本类型的sizeof,可以使用类型,也可以使用变量
- #pragma pack的基本用法为:
#pragma pack( n )
结构体
#pragma pack( )
对齐参数:n为字节对齐数,其取值为1、2、4、8,默认是8。
- 对齐原则:(对齐参数为默认值8)
原则一:数据成员对齐规则:结构的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置(起始位置看原则四偏移量)要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储).
原则二:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。
原则三:如果一个结构里有某些结构体成员,则要从子结构体最大成员或者实际对齐单位(还是取两者小的那个)的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储) // 但该子结构体所占内存仍为原本所占内存
原则四:对齐参数如果比结构体成员的sizeof值小,该成员的偏移量应该以此值为准. 也就是说,结构体成员的偏移量应该取二者的最小值.
参考博客
举例:
struct S4
{
int c1;
char c2[10];
};
所占的空间大小为16, c2[10]并不是基本数据类型,这里当做10个char型变量来处理,char占1个字节,小于实际对齐字节8,所以这10个char型变量可以直接挨着int c1后面放。
switch语句
- switch要求:
case后面必须是常量表达式
case后常量表达式的值不能一样
switch后面表达式必须为整数 - 尝试:(有惊喜)
(1)添加case后面的值,一个一个增加,观察反汇编代码的变化(何时生成大表).
(2)将3常量值的顺序打乱,观察反汇编代码(观察顺序是否会影响生成大表).
(3)将case后面的值改成从100开始到109,观察汇编变化(观察值较大时是否生成大表).
(4)将连续的10项中抹去1项或者2项,观察反汇编有无变化(观察大表空缺位置的处理).
(5)在10项中连续抹去,不要抹去最大值和最小值(观察何时生成小表).
(6)将case后面常量表达式改成毫不连续的值,观察反汇编变化.