Day 11 《存储与判断语句》
(一)字符存储
1.字符存储:
2.ASCII码:
ASCII码使用指定的7位或8位二进制数组合来表示128或256种可能;标准的只有7位;
扩展ASCII码:
由于标准ASCII码字符集字符数目有限,在实际应用中往往无法满足要求。为此,国际标准化组织又将ASCII码字符集扩充为8位扩展ASCII码的扩充。这样,ASCII码的字符集可以扩充128个字符,也就是实用8位扩展ASCII码能为256个字符提供编码,这些扩充字符的编码均为高位1的8位代码(即十进制数128~255),称为扩展ASCII码。扩展ASCII码所增加的字符包括加框文字、圆圈和其他图形符号;
3.GB2312:
GB2312是用两个ASCII码表示,即16位;
(二)内存
1.内存图:
所谓的可读可写限制是对正向开发人员而言的。我们的源代码都在代码区中,数值保存在对应的区,比如int x = 2;这个代码放在代码区中,但是x对应的数值存储在全局变量区;
2.全局变量的特点:
1. 全局变量在程序编译完成后地址就已经确定下来了,只要程序启动,全局变量就已经存在;是否有值取决于声明时是否给定了初始值,如果没有,默认为0;
2. 全局变量的值可以被所有函数所修改,里面存储的是最后一次修改的值;
3. 全局变量所占内存会一直存在,直到整个进程结束;
4. 全局变量的反汇编识别:
mov 寄存器,byte/word/dword ptr ds:[0x12345678]
通过寄存器的宽度,或者byte/word/dword来判断全局变量的宽度
全局变量就是所谓的基址;
注意:函数名也是全局变量;
3.局部变量的特点:
1. 局部变量在程序编译完成后并没有分配固定的地址;
2. 在所属的方法没有被调用时,局部变量并不会分配内存地址,只有当所属的程序被调用时,才会在堆栈中分配内存;
3. 当局部变量所属的方法执行完毕后,局部变量所占用的内存将变成垃圾数据,局部变量消除;
4. 局部变量只能再方法内部使用,函数A无法使用函数B的局部变量;
5. 局部变量的反汇编识别:
[ebp-4]这个是局部变量,即ebp-多少,
但是这个有时候也不是绝对的,有的编译器使用ESP+多少之类的;
4.判断函数的参数个数:
步骤一:
步骤二:参数的传递未必都是通过堆栈,还能使用寄存器
当函数调用处的代码无法查看:
步骤一:查看函数内用于赋值的寄存器:eax、ecx、edx、ebx、esi、edi
不考虑ebp、esp,找到用于赋值的寄存器后,查看其来源
如果该寄存器的值不是再函数内存赋值的,那一定是传进来的参数;
步骤二:
观察ebx-x之类的代码
查看ret返回时的操作数
寄存器 + ret x = 参数个数
寄存器 + [ebp + x] + … = 参数个数;
5.函数内部功能分析:
1. 分析参数:
2. 分析局部变量
3. 分析全局变量
4. 功能分析
练习:
(三)if语句的反汇编判断
1.执行各类影响标志位的指令:jxx xxxx
mov eax,dword ptr [ebp+8]
cmp eax,dword ptr [ebp+0Ch]
jle 00401059
分析:cmp指令 影响标志位
jle: 小于或等于就跳转到00401059
mov eax,dword ptr [ebp+8]
cmp eax,dword ptr [ebp+0Ch]
jl 00401059
分析:cmp指令 影响标志位
jl: 小于则跳转
2017/5/20 23:39:08
Day 12 《反汇编C语法结构》
(一)练习
1.从堆栈的角度理解这个程序为什么可以执行:
答案:
2.分析此循环为什么永远无法停止:
答案:
(二)MOVSX 和 MOVZX
MOVSX (move with sign-extend) 符号位扩展
适用于有符号整数,将源操作数内容复到目标操作数,用较小操作数的最高位填充所有扩展位(根据符号位使用0或1扩展)
MOVZX (move with zero-extend) 零扩展传送
适用于无符号整数,将源操作数内容复制到目标操作数,用0扩展到16位/32 位,适用于无符号整数
(三)反汇编C基本语法
1.++/–
2.数组
数组是按顺序存于栈中的;
3.循环语句
执行效率如图,由上至下递减;
(四)练习
#include<stdio.h>
int Found(int arr[], int count)
{
int i = 0;
int _max = arr[0];
do
{
if (arr[i] < arr[i + 1])
{
_max = arr[i + 1];
}
++i;
} while (i<count-1);
return _max;
}
int Sum(int arr[], int count)
{
int i = 0;
int _sum=0;
do
{
_sum += arr[i];
++i;
} while (i<count);
return _sum;
}
int Prime(int x)
{
int i, k;
k = (int)sqrt(x);
for (i = 2; i <= k; ++i)
{
if(x%i==0)
break;
}
return i == k;
}
int main()
{
//1.将两个变量的值交换
int i = 5;
int j = 4;
int temp = i;
i = j;
j = temp;
//2.将一个数组中的数倒序输出
i = 4;
int arr[5] = { 1,2,3,4,5 };
do
{
printf("%d ", arr[i]);
--i;
} while (i>=0);
//3.找出数组里面最大的值,并返回
int _max = Found(arr, 5);
//4.将数组所有的元素相加,将结果返回
int _sum = Sum(arr, 5);
//5.将两个等长数组相同位置的值相加,存储到另外一个等长的数组中
i = 4;
int arr2[5] = { 9,8,7,6,5 };
int arr3[5];
do
{
arr3[i] = arr[i] + arr2[i];
++i;
} while (i<=4);
//6.写一个函数int prime(int x),如果x是素数返回值为1,否则为0.
int j = Prime(9);
//7.俩俩比较数组的值,获取最大的一个值。
int _max = 0;
for (i = 0; i < 5; i++)
{
if (arr[i] > arr2[i])
{
j = arr[i];
}
else
{
j = arr2[i];
}
if (_max < j)
_max = j;
}
}
2017/5/23 17:50:04
Day 13 《函数缓冲与变量内存》
(一)练习
1.long long类型在C中如何存储
2.char arr[3]={1,2,3} 与 char arr[4]={1,2,3,4}哪个更省空间?
3.找出下面赋值过程的反汇编代码
void Funcition()
{
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];
}
(二)函数内缓冲
1.函数调用传入实参换成char或short,会节省空间吗?
2.参数和局部变量的区别
参数在函数调用的时候分配空间,位于函数栈的ebp下;局部变量是在函数执行的时候分配空间,位于函数栈的ebp上面(即函数缓冲区内);
小于32位的局部变量,在空间分配时,按照32位的数据规格来分配,根据编译器的不同,可能每增加一个4字节及以下的变量会增加8个字节,有的会增加12(Ch)个字节;使用时按实际的宽度使用。
3.数组
4.数组的赋值
#include<stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int x = 1;
int y = 2;
int r;
int arr[5] = { 1,2,3,4,5 };
r = arr[1];
r = arr[x];
r = arr[x + y];
r = arr[x * 2 + y];
r = arr[arr[1] + arr[2]];
r = arr[Add(1, 2)];
r = arr[100];
return 0;
}
反汇编结果:
每条赋值语句都能够执行;
数组下标不但可以越界,而且可以做到一些不可思议的事情。堆栈图必须过硬。
5.二维数组
其内存的存储上,完全一样,并无区别。
但其取值上有区别:
6.缺省一维数组个数的二维数组定义
2017/5/24 17:49:57
Day 14 《结构体》
(一)结构体的存储与传递
1.反汇编结构体
查看反汇编,发现结构体和数组的存储几乎一样,难以区分;
数组和游戏中的地图有关,结构体和游戏中的角色有关;
所以我们逆向的时候,只需要实现相同的功能就行;
2.结构体作为参数时的传递方式
这里没有使用push的堆栈方式,而是直接提升堆栈,在堆栈中使用变址寄存器完成结构体数据的复制,或者直接movs数据以传递参数;
3.结构体作为返回值
注意:
开发中,最好不要使用结构体当参数,或结构体当返回值,这样将大量的浪费空间;
(二)结构体的内存对齐
1.#pragma pack的基本用法
#pragma pack(n)
struct MyStruct
{
};
#pragma pack()
n为字节对齐数,其取值为1,2,4,默认是8;
如果这个n值比结构体成员的sizeof值小,那么该成员的偏移量应该以此n值为准,即是说,结构体成员的偏移量应该取二者的最小值;
2.深入结构体内存对齐
对齐原则:
1. 结构的数据成员,第一个成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的证书倍开始(比如int再32位机为4字节,则要从4的整数倍地址开始存储);
2. 结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐;
3. 如果一个结构里有某些结构体成员,则结构成员要从其内部最大元素大小的整数倍地址开始存储(比如struct a里有struct b,b里有char int double等元素,那b应该从8的整数倍开始存储);
4. 对齐参数如果比结构体成员的sizeof值小,该成员的偏移量应该以此值为准,也就是说,结构体成员的偏移量应该取二者的最小值;建议:按照数据类型由小到大的顺序进行书写;
3.练习
1.分析结构体大小
struct S1
{
char c;
double i;
};
struct S2
{
char c1;
struct S1 s;
char c2;
char c3;
};
struct S3
{
char c1;
struct S1 s;
char c2;
double i;
};
struct S4
{
int c1;
char c2[10];
};
答案:
int main()
{
//8+8=16字节
int i = sizeof(struct S1);
struct S1 s = { 1,1 };
//8+16+8=32字节(根据S1里成员最大的数据类型为double,倍数为8)
int j = sizeof(struct S2);
//8+16+8+8=40字节
int k = sizeof(struct S3);
//8+8=16字节(此处是先填满前8个字节再填后面的)
int l = sizeof(struct S4);
return 0;
}
2.递归函数反汇编分析
#include<stdio.h>
int test(int i)
{
if (i < 1)
return 1;
int k = test(--i);
return k;
}
int main()
{
int i = test(3);
return 0;
}
main中调用test(3)后,传入3,在第一次test中,i 不小于 1,则调用函数test(2),同样,再调用test(1),第3次test中,i=1,i不小于1,同样,调用test(0),则此第4次,0<1,则返回1;第3次中,k=1,返回k;在第2次中,k=1,返回k;在第1次中,k=1,返回k;在main中,i = 1;
2017/5/25 15:33:21
Day 15 《逆向switch》
(一)switch
1.switch的反汇编
switch 在游戏中和技能配合使用最多, 比如 F 加血等等
上方为什么要减去1再比较? (图中已说明)
Switch 会建立一个大的表,把我们要执行的语句对应的地址都保存起来。
2.switch的地址表
情况一:3个分支
情况二:4个分支
情况三:分支常量值顺序打乱
当case分支少于4个时,并不会生成case分支地址表,因为编译器会生成类似if…else之类的反汇编;
case后面的常量可以是无序的,并不会影响表的生成;且其表内的元素排列会按照case分支的值从小到大排列,表是有序的;
3.switch的case分支语句地址的寻址方式
1. 情况一:
2. 情况二:
从上面两种情况来看,在编译器的处理中,如果switch的case常量之间差值太大的话,会采取情况二的方法(大表+小表)。如果差值不大的话,会采取情况一(大表)的方式去处理;
情况一:差值之间的数据统一都使用default语句的EIP地址填充;
情况二:该方法有利于节省空间;
(二)switch练习
1.写一个switch语句,不产生大表与小表
2.写一个switch语句,只产生大表
3.写一个switch语句,产生大表与小表
#include<stdio.h>
char Fun1(char c)
{
char j = 0;
switch (c)
{
case 'a':
j = 'a';
break;
case 'b':
j = 'b';
break;
case 'c':
j = 'c';
break;
default:
break;
}
return j;
}
char Fun2(char c)
{
char j = 0;
switch (c)
{
case 'a':
j = 'a';
break;
case 'b':
j = 'b';
break;
case 'c':
j = 'c';
break;
case 'd':
j = 'd';
break;
case 'e':
j = 'e';
break;
case 'f':
j = 'f';
break;
case 'g':
j = 'g';
break;
case 'h':
j = 'h';
break;
case 'n':
j = 'n';
break;
default:
break;
}
}
char Fun3(char c)
{
char j = 0;
switch (c)
{
case 'a':
j = 'a';
break;
case 'b':
j = 'b';
break;
case 'c':
j = 'c';
break;
case 'n':
j = 'n';
break;
default:
break;
}
return j;
}
int main()
{
int i = Fun1('n');
int j = Fun2('n');
int k = Fun3('n');
return 0;
}
(三)分析循环语句
2017/5/26 12:44:11