第十九章-VC6.0的使用、C函数的反汇编、C补充

新建项目

  1. 新建项目
    1. 打开VC6.0
    2. 文件->新建->工程->Win32 Console Application(win32控制台应用程序)
    3. 设置工程名称:
    4. 选择保存路径
    5. 确定
    6. 选择一个空工程
    7. 点击完成,确定即可
  2. 添加文件
    1. 点击文件视图:FileView
    2. 选择SourceFiles
    3. 点击工具栏的文件->新建->文件->C++ Source File
    4. 设置名称
    5. 确定即可

插入汇编代码

__asm{
    //汇编代码
}

常用快捷键

F7 编译
F5 运行
ctrl + F5 编译+运行
F9 设置、取消断点
F10 单步执行(单步步过)
F11 单步步入

调试程序

  1. 查看汇编代码
    1. 设置断点
    2. 运行程序到断点处
    3. 右击->GO TO Disassembly
    4. 返回:shift +F5
  2. 查看内存与寄存器
    1. 在工具栏的最右边找到memory与registers
    2. 通过F10执行,可以查看寄存器以及内存数据的变化。
  3. 查看某个变量的值
    1. 在工具栏的最右边找到监视器,戴眼镜的图标,点击打开
    2. 将要查看的变量,拖入就可以查看。
  4. VC6.0在转化为汇编调用call语句的时候,会转化成调用jmp指令,这个不影响。

C函数的本质

  1. C函数

     int  plus(int x, int y)
     {
     	return 0;
     }
    
  2. 反汇编

     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
    
  3. C语言中参数是如何传递的?
    1. 使用堆栈传参,参数从右到左传递
  4. C语言中返回值存储在EAX中

C中的变量

全局变量

  1. 编译的时候就已经确定了内存的地址和宽度,变量名就是内存的地址别名。
  2. 如果不重新编译,全局变量的内存地址不变。游戏外挂中的找基址,其实就是找全局变量
  3. 全局变量中的值任何程序都可以改,是公用的。

局部变量

  1. 局部变量是函数内部申请的,如果函数没有执行,那么局部变量没有内存空间
  2. 局部变量的内存是在堆栈中分配的,程序执行时才分配,我们无法预知程序何时执行,因此我们无法确定局部变量的内存地址。
  3. 因为局部变量的内存地址是不确定的,所以,局部变量只能在函数内部使用,其他函数不能使用。
  4. 全局变量有默认值为0,局部变量必须先初始化,再使用。

变量存储的位置

  1. 局部变量存储在函数帧栈的缓冲区中。
  2. C语言下,要熟记帧栈图!!!!!

     EBP 原EBP的内容
     EBP+4 返回地址
     EBP+8 参数区
     EBP-4 局部变量区域,即缓冲区
     EAX 存储返回值
    
    1. 寄存器保护区
    2. 缓冲区
    3. EBP
    4. EBP+4 返回地址
    5. EBP+8 参数区
    6. EBP-4 局部变量区域,即缓冲区
  3. 这两句函数的汇编有什么意义呢?

     //此时的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;
}
  1. 反汇编中有这么几句代码

     //将esp恢复到分配缓冲区前的位置
     add esp,44h
     //比较esp与ebp的值
     cmp esp,ebp
     //检查是否上面的比较相等,不相等直接报错
     call __chkesp(00401120)
        
     mov esp,ebp
    
    1. 为何多了3句呢?通常回复esp直接是这一句mov esp,ebp就行了 这是debug环境下为了检查堆栈平衡
    2. 查看是否破坏了堆栈,如果破坏了堆栈,那么直接回报错在__chkesp函数了。

ASCII码表的本质

  1. 原理:
    1. 由于计算机只能存储1和0,不能存储字符形状,因此就把我们通常使用的符号编上编码,存入到计算机内存
    2. 这些编码与符号的对应关系,就叫ASCII表
    3. 共127个符号,这127个符号可以表达出美国人所有的语言。
  2. 那么如何显示呢?
    1. 从内存中取出来的也是一个编号,比如:41h
    2. 如果我们想要输出为字符,用putchar函数
    3. 这个函数将这个编号,按照ascii表的对应关系,找到相应的字符A,然后将A画在终端上。

中文字符

  1. 中文是怎么存储的呢? ASCII表中并没有中文对应
  2. ASCII有个扩展表,128-255,编码了一些奇异符号。
  3. 中国把这些扩展重新编码,整成一张表,叫GB2312或GB2312-80
  4. 这张表,0-127和原来一样,127以后,两个大于127的字符连接在一起时,就表示一个汉字
  5. 在这些编码里,0-127中本来就有的数字、标点、字母统统重新编了2个字节长的编码,这就是常说的全角字符,而原来在127号以下的那些就交半角字符了
    1. 输入法有全角半角切换,中文分号与英文分号不同的由来。
  6. 即2个大于127的编码表示一个汉字。即汉字占用2个字节。
  7. 用反汇编查看可以发现,一个汉字对应2个字节,而且每个字节的编码值大于127.

GB2312或GB2312-80的弊端

  1. 两种编码可能使用相同的数字代表2个不同的符号。
    1. 同一个编码,中国代表一个汉字。日本也这么弄,那就代表是日文。
  2. Unicode编码就是为了解决这个问题出现的。

switch 语句比if else高效

  1. 当条件非常少的时候二者效率差不多
  2. 当条件非常多的时候,switch就比if效率高很多了
  3. 可以汇编查看分析。

windows常用功能

  1. 查看开机启动
    1. 先按快捷键Win+R,输入msconfig,弹出的窗口中,点击“启动”,列表中展示的就是当前系统的默认开启启动项。
  2. 将某个程序设置成开机启动
    1. 先按快捷键Win+R,输入regedit,找到:KEEY_CURRENT_USER\Software\Micorosoft\Windows\CurrentVersion\Run
    2. 右击“新建”->”字符串值”,设置名称,然后双击打开,会有个弹框,让输入“数值数据”
    3. 将要开机启动的程序路径输入里面,比如:C:\Users\Administrator\Desktop\2016_firstdemo\TestDemo\Debug\TestDemo.exe,点击确定即可。
  3. 常用的Dos命令
    1. 先按快捷键Win+R,输入cmd,进入Dos系统
    2. 常用命令

       //设置控制台颜色
       color A(可选值 0 - F)
       //打开某个程序
       start C:\Dbgview.exe
       //删除某个文件
       del C:\a.txt
       //10s后自动关机
       shutdown -f -s -t 10
      
  4. system函数
    1. 该函数能够执行DOS命令

       system("pause");
       system("color 0A");
       system("start C:\Dbview.exe");
       system("del C:\a.txt");
       system("shutdown -f -s -t 10");
      
  5. 解决办法
    1. 安全模式(开机按F8)下,删除自己添加的启动项,然后重启电脑。
    2. 先按快捷键Win+R,输入regedit,找到:KEEY_CURRENT_USER\Software\Micorosoft\Windows\CurrentVersion\Run
    3. 找到启动项,删除即可。
  6. 通过以上功能,可以写一个自动关机程序,然后加入到开启启动项中,形成每次开机就自动关闭的bug。

字节对齐

  1. 什么是字节对齐?

     char x,
     short y;
     int z;
    
    1. 一个变量占用n个字节,则该变量的起始地址必须是n的整数倍,即:存放起始地址%n = 0;
    2. 如果是结构体,那么结构体的起始地址是其最宽数据类型成员的整数倍。

指针补充

  1. 放弃固定思维: 指针是专门用来存地址的
  2. 指针就是一个数据类型,大小占据4个字节,什么东西都可以放
  3. 样式:基本数据类型 n个*
  4. 指针的特性:
    1. 可以加、减、自增、自减、比较
    2. 注意比较的时候,按照无符号数比较。

指针的自增自减(++/–)

  1. VC6.0环境,指针类型占据4个字节
  2. 总结:
    1. 不带*类型的变量,++或者–,都是加1或者减1
    2. *类型的变量,++或者–新增(减少)的数量是去掉一个*后变量的宽度
  3. 举例:

    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. 例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. 指针类型的变量可以加、减一个整数,但是不可以乘、除
  2. 指针类型的变量与其他整数相加或者相减时:
    1. 指针类型变量+N = 指针变量 + N*(去掉一个*后类型的宽度)
    2. 指针类型变量-N = 指针变量 - N*(去掉一个*后类型的宽度)

&变量 的类型

  1. &变量的类型就是变量原来类型加一个*
  2. 即:&a类型就是a的类型加一个*

     char *****a;
     &a的类型就是char ******;
    

*指针变量 的类型

  1. *指针变量的类型就是变量原来的类型减一个*

     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

调用约定

  1. VC6.0中编译器有如下默认规定
    1. 传参从右到左
    2. 传参使用堆栈
    3. 运算结果放到eax
  2. 常见的几种调用约定

     调用约定          参数压栈的顺序                               平衡堆栈
     __cdecl         从右到左入栈                                  外平栈
     __stdcall       从左到右入栈                                  内平栈
     __fastcall      ecx/edx传送前2个参数,剩下参数从右到左入栈        内平栈
    
  3. 所谓的调用约定就是告诉编译器如何编译代码。
  4. 使用方法

     int __fastcall sub(int a,int b,int c){
         return 0;
     }
    
  5. 应用场景
    1. 自己写代码很少用,主要常见于window api ,出现了要明白怎么回事。
Table of Contents