第十九章-VC6.0的使用、C函数的反汇编、C补充
新建项目
- 新建项目
- 打开VC6.0
- 文件->新建->工程->Win32 Console Application(win32控制台应用程序)
- 设置工程名称:
- 选择保存路径
- 确定
- 选择一个空工程
- 点击完成,确定即可
- 添加文件
- 点击文件视图:FileView
- 选择SourceFiles
- 点击工具栏的文件->新建->文件->C++ Source File
- 设置名称
- 确定即可
插入汇编代码
__asm{
//汇编代码
}
常用快捷键
F7 编译
F5 运行
ctrl + F5 编译+运行
F9 设置、取消断点
F10 单步执行(单步步过)
F11 单步步入
调试程序
- 查看汇编代码
- 设置断点
- 运行程序到断点处
- 右击->GO TO Disassembly
- 返回:
shift +F5
- 查看内存与寄存器
- 在工具栏的最右边找到memory与registers
- 通过F10执行,可以查看寄存器以及内存数据的变化。
- 查看某个变量的值
- 在工具栏的最右边找到监视器,戴眼镜的图标,点击打开
- 将要查看的变量,拖入就可以查看。
- VC6.0在转化为汇编调用call语句的时候,会转化成调用jmp指令,这个不影响。
C函数的本质
-
C函数
int plus(int x, int y) { return 0; }
-
反汇编
push ebp mov ebp,esp //为函数执行,划出内存64个字节,即40h,这一块也叫函数的缓冲区 sub esp,40h push ebx push esi push edi /****将内存缓冲区全部填充为0CCCCCCCCh******/ //将edi指向缓冲区的开始位置 lea edi,[ebp-40h] //ecx 中存放循环的次数,10h为16次 mov ecx,10h //eax中存放0CCCCCCCCh mov eax,0CCCCCCCCh //rep循环,循环次数由ecx决定,stos串写入指令 //把eax的值存放到edi指向的位置,然后修改地址指针,循环执行16次 rep stos dword ptr [edi] //清空eax的值为0 xor eax,eax pop edi pop esi pop ebx mov esp,ebp pop ebp ret
- C语言中参数是如何传递的?
- 使用堆栈传参,参数从右到左传递
- C语言中返回值存储在EAX中
C中的变量
全局变量
- 编译的时候就已经确定了内存的地址和宽度,变量名就是内存的地址别名。
- 如果不重新编译,全局变量的内存地址不变。游戏外挂中的找基址,其实就是找全局变量
- 全局变量中的值任何程序都可以改,是公用的。
局部变量
- 局部变量是函数内部申请的,如果函数没有执行,那么局部变量没有内存空间
- 局部变量的内存是在堆栈中分配的,程序执行时才分配,我们无法预知程序何时执行,因此我们无法确定局部变量的内存地址。
- 因为局部变量的内存地址是不确定的,所以,局部变量只能在函数内部使用,其他函数不能使用。
- 全局变量有默认值为0,局部变量必须先初始化,再使用。
变量存储的位置
- 局部变量存储在函数帧栈的缓冲区中。
-
C语言下,要熟记帧栈图!!!!!:
EBP 原EBP的内容 EBP+4 返回地址 EBP+8 参数区 EBP-4 局部变量区域,即缓冲区 EAX 存储返回值
- 寄存器保护区
- 缓冲区
- EBP
- EBP+4 返回地址
- EBP+8 参数区
- EBP-4 局部变量区域,即缓冲区
-
这两句函数的汇编有什么意义呢?
//此时的eax为函数内部计算的中间结果,相当于局部变量,因此放在缓冲区域内。 mov [ebp-4],eax //函数即将返回,将函数最总的即时结果放入到eax,这是约定俗成。 mov eax, [ebp-4]
函数嵌套调用的内存布局
int plus1(int x,int y)
{
return x+y;
}
int plus(int x,int y,int z)
{
int m = plus1(1,2);
ret m+z;
}
int main()
{
int r ;
r = plus(1,3,4);
return 0;
}
-
反汇编中有这么几句代码
//将esp恢复到分配缓冲区前的位置 add esp,44h //比较esp与ebp的值 cmp esp,ebp //检查是否上面的比较相等,不相等直接报错 call __chkesp(00401120) mov esp,ebp
- 为何多了3句呢?通常回复esp直接是这一句
mov esp,ebp
就行了 这是debug环境下为了检查堆栈平衡 - 查看是否破坏了堆栈,如果破坏了堆栈,那么直接回报错在
__chkesp
函数了。
- 为何多了3句呢?通常回复esp直接是这一句
ASCII码表的本质
- 原理:
- 由于计算机只能存储1和0,不能存储字符形状,因此就把我们通常使用的符号编上编码,存入到计算机内存
- 这些编码与符号的对应关系,就叫ASCII表
- 共127个符号,这127个符号可以表达出美国人所有的语言。
- 那么如何显示呢?
- 从内存中取出来的也是一个编号,比如:
41h
- 如果我们想要输出为字符,用putchar函数
- 这个函数将这个编号,按照ascii表的对应关系,找到相应的字符
A
,然后将A
画在终端上。
- 从内存中取出来的也是一个编号,比如:
中文字符
- 中文是怎么存储的呢? ASCII表中并没有中文对应
- ASCII有个扩展表,128-255,编码了一些奇异符号。
- 中国把这些扩展重新编码,整成一张表,叫GB2312或GB2312-80
- 这张表,0-127和原来一样,127以后,两个大于127的字符连接在一起时,就表示一个汉字
- 在这些编码里,0-127中本来就有的数字、标点、字母统统重新编了2个字节长的编码,这就是常说的全角字符,而原来在127号以下的那些就交半角字符了
- 输入法有全角半角切换,中文分号与英文分号不同的由来。
- 即2个大于127的编码表示一个汉字。即汉字占用2个字节。
- 用反汇编查看可以发现,一个汉字对应2个字节,而且每个字节的编码值大于127.
GB2312或GB2312-80的弊端
- 两种编码可能使用相同的数字代表2个不同的符号。
- 同一个编码,中国代表一个汉字。日本也这么弄,那就代表是日文。
- Unicode编码就是为了解决这个问题出现的。
switch 语句比if else高效
- 当条件非常少的时候二者效率差不多
- 当条件非常多的时候,switch就比if效率高很多了
- 可以汇编查看分析。
windows常用功能
- 查看开机启动
- 先按快捷键Win+R,输入msconfig,弹出的窗口中,点击“启动”,列表中展示的就是当前系统的默认开启启动项。
- 将某个程序设置成开机启动
- 先按快捷键Win+R,输入regedit,找到:
KEEY_CURRENT_USER\Software\Micorosoft\Windows\CurrentVersion\Run
- 右击“新建”->”字符串值”,设置名称,然后双击打开,会有个弹框,让输入“数值数据”
- 将要开机启动的程序路径输入里面,比如:
C:\Users\Administrator\Desktop\2016_firstdemo\TestDemo\Debug\TestDemo.exe
,点击确定即可。
- 先按快捷键Win+R,输入regedit,找到:
- 常用的Dos命令
- 先按快捷键Win+R,输入cmd,进入Dos系统
-
常用命令
//设置控制台颜色 color A(可选值 0 - F) //打开某个程序 start C:\Dbgview.exe //删除某个文件 del C:\a.txt //10s后自动关机 shutdown -f -s -t 10
- system函数
-
该函数能够执行DOS命令
system("pause"); system("color 0A"); system("start C:\Dbview.exe"); system("del C:\a.txt"); system("shutdown -f -s -t 10");
-
- 解决办法
- 安全模式(开机按F8)下,删除自己添加的启动项,然后重启电脑。
- 先按快捷键Win+R,输入regedit,找到:
KEEY_CURRENT_USER\Software\Micorosoft\Windows\CurrentVersion\Run
- 找到启动项,删除即可。
- 通过以上功能,可以写一个自动关机程序,然后加入到开启启动项中,形成每次开机就自动关闭的bug。
字节对齐
-
什么是字节对齐?
char x, short y; int z;
- 一个变量占用n个字节,则该变量的起始地址必须是n的整数倍,即:存放起始地址%n = 0;
- 如果是结构体,那么结构体的起始地址是其最宽数据类型成员的整数倍。
指针补充
- 放弃固定思维: 指针是专门用来存地址的
- 指针就是一个数据类型,大小占据4个字节,什么东西都可以放
- 样式:
基本数据类型 n个*
- 指针的特性:
- 可以加、减、自增、自减、比较
- 注意比较的时候,按照无符号数比较。
指针的自增自减(++/–)
- VC6.0环境,指针类型占据4个字节
- 总结:
- 不带
*
类型的变量,++或者–,都是加1或者减1 - 带
*
类型的变量,++或者–新增(减少)的数量是去掉一个*
后变量的宽度
- 不带
-
举例:
- 例1:
char *a; short *b; int *c; a = (char*)100; b = (short*)100; c = (short*)100; a++; b++; c++; printf("%d %d %d",a,b,c); //打印结果为101,102,104 //分析,abc分别砍掉一个*,变量为char a,short b,int c,宽度为1,2,4,所以自加后结果为101,102,104
-
例2:
char **a; short **b; int **c; a = (char**)100; b = (short**)100; c = (short**)100; a++; b++; c++; printf("%d %d %d",a,b,c); //打印结果为104,104,104 //分析,abc分别砍掉一个*,变量为char* a,short* b,int* c,宽度为4,4,4,所以自加后结果为104,104,104
- 例1:
指针的加法减法
- 指针类型的变量可以加、减一个整数,但是不可以乘、除
- 指针类型的变量与其他整数相加或者相减时:
- 指针类型变量+N = 指针变量 + N
*
(去掉一个*
后类型的宽度) - 指针类型变量-N = 指针变量 - N
*
(去掉一个*
后类型的宽度)
- 指针类型变量+N = 指针变量 + N
&变量 的类型
&变量
的类型就是变量原来类型加一个*
-
即:
&a
类型就是a的类型加一个*
char *****a; &a的类型就是char ******;
*指针变量
的类型
-
*指针变量
的类型就是变量原来的类型减一个*
char *****a; *a的类型就是char ****;
字符串常用函数
int strlen(char *s);
//返回值是字符串s的长度,不包括字符/0
char *strcpy(char*dest,char*src);
//复制字符串src到dest中。返回指针为dest的值。
char *strcat(char*dest,char*src);
//将字符串src添加到dest尾部。返回指针为dest的值。
int strcmp(char *s1,char*s2);
//一样返回0,不一样返回非0
调用约定
- VC6.0中编译器有如下默认规定
- 传参从右到左
- 传参使用堆栈
- 运算结果放到eax
-
常见的几种调用约定
调用约定 参数压栈的顺序 平衡堆栈 __cdecl 从右到左入栈 外平栈 __stdcall 从左到右入栈 内平栈 __fastcall ecx/edx传送前2个参数,剩下参数从右到左入栈 内平栈
- 所谓的调用约定就是告诉编译器如何编译代码。
-
使用方法
int __fastcall sub(int a,int b,int c){ return 0; }
- 应用场景
- 自己写代码很少用,主要常见于window api ,出现了要明白怎么回事。