1. C语言的汇编表示

1.1 基本信息

  • 入口程序:程序开始执行的地方
  • return:执行结束

1.2 什么是函数

函数是一些列指令的集合,用以完成某个会被重复使用的特定功能

  • C语言中函数格式:
    返回类型 函数名(参数列表){}

<1>返回类型、函数名不能省略
<2>参数列表可以省略

[!info] 函数名、参数名的命名规则

  1. 只能以字母、数字、下划线组成,且首字母不为数字
  2. 区分大小写
  3. 不能使用关键字

1.3 基本操作

  • 创建exe文件(F7)
  • 运行(F5)
  • 断点(F9)
  • 步过(F10)
  • 步入(F11)

1.4 函数的调用

  • 汇编中的函数调用:
    • CALL/JMP指令
  • C语言中的函数调用:
    • 函数名(参数1,参数2);

实际上:函数名就是编译器起的内存地址的别名

2. 参数传递与返回值

2.1 函数定义

1
2
3
4
5
6
7
8
9
10
返回类型 函数名(参数列表)

return;
}

举例:
int plus(int x,int y){
return x+y;
}

数据类型:
int – 4 Byte
short – 2 Byte
char – 1 Byte

进制标识:

  • 十六进制 – 通常h结尾,例如40h
  • 二进制 – 通常b结尾,例如1010b
  • 八进制 – 通常oq结尾,例如77o
  • 十进制 – 默认无后缀,或有时用d

2.2 堆栈图理解

画堆栈图可以方便我们从汇编角度理解C语言命令是怎么运行的

  • 堆栈缓冲区会填入CC
  • 堆栈使用后,需要平栈
  • 堆栈使用后,原先使用的堆栈并没有被清除,会被利用

2.3 参数传递

C语言:堆栈传参 从右向左

2.4 返回值存储

C语言:返回值存储在EAX中

3. 变量

3.1 声明变量

数据类型的宽度:

  • int – 4 Byte
  • short – 2 Byte
  • char – 1 Byte

[!info] 函数名、参数名的命名规则

  1. 只能以字母、数字、下划线组成,且首字母不为数字
  2. 区分大小写
  3. 不能使用关键字

3.2 全局变量

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

3.3 局部变量

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

3.4 变量初始值

  1. 全局变量可以没有初始值而直接使用,系统默认初始值为0
  2. 局部变量在使用前必须要赋值

4. 变量与参数的内存空间

缓冲区:用于存储局部变量

EBP-4 局部变量
EBP 原EBP值
EBP+4 返回地址
EBP+8 参数区

5. 函数嵌套调用的内存布局

画堆栈图理解
函数与嵌套函数:在堆栈上是相邻完整的函数堆栈

6. 整数类型

6.1 C语言变量类型

  • 基本类型
    • 整数类型
    • 浮点类型
  • 构造类型
    • 数组类型
    • 结构体类型
    • 共用体(联合)类型
  • 指针类型
  • 空类型(void)

6.2 整数类型的宽度

char、short、int、long

char – 8 BIT – 1 Byte – 00xFF
short – 16 BIT –2 Byte – 0
0xFFFF
int – 32 BIT – 4 Byte – 00xFFFFFFFF
long – 32 BIT – 4 Byte – 0
0xFFFFFFFF

特别说明:int在16位(32位以上)计算机中与short(long)宽度一致

6.3 数据溢出

  1. char x=0xFF; //1111 1111
  2. char y=0x100; //0001 0000 0000
    数据溢出时舍弃高位

6.4 存储格式

有符号数补码存储
char x=1; //0000 0001(0x01)
char x=-1; //1111 1111(0xFF)

6.5 有符号与无符号数

signed、unsigned

  1. 数据存储时都为文本直接存储,使用时再判断类型
  2. 数据宽度变大时,存储在低位,有符号数补1/无符号数补0

7. 浮点类型

7.1 浮点类型的种类

float – 4 Byte
double – 8 Byte
long double – 8 Byte(某些平台的编译器可能是16字节)

建议:
float x=1.23F;
double d=2.34;
long double d=2.34L;

7.2 浮点类型的存储格式

float和double在存储方式上都遵从IEEE编码规范

float的存储方式:符号位(1) – 指数部分(8) – 尾数部分(23)
double的存储方式:符号位(1) – 指数部分(11) – 尾数部分(52)

7.2.1 十进制整数转二进制

整数部分转换二进制:

如:8d -> 1000b
8/2=4 ··· 0
4/2=2 ··· 0
2/2=1 ··· 0
1/2=0 ··· 1
————————————
1000b

存储方式:取上一次计算结果/2得到的余数部分,直到结果为0停止,从下往上读

9/2=4 ··· 1
4/2=2 ··· 0
2/2=1 ··· 0
1/2=0 ··· 1
————————————
1001b

总结:所有的整数一定可以完整转换成二进制

7.2.2 十进制小数转二进制

小数部分转换二进制:

如:0.25
0.25*2=0.5 – 0
0.5*2=1.0 – 1
01

存储方式:取上一次计算结果的小数部分*2得到的整数部分,直到小数部分为0停止,从上往下读

0.4*2=0.8 – 0
0.8*2=1.6 – 1
0.6*2=1.2 – 1
0.2*2=0.4 – 0
···

总结:二进制描述小数,不可能做到完全精准

7.2.3 科学计数法

[!info]
二进制数*2,左移一位
二进制数/2,右移一位(可移至小数点后)

存储方式

  • 符号位:+为0,-为1
  • 指数部分:
    • 科学计数法时小数点左(右)移,最高位为1(0)
    • 指数-1得到的数转换为二进制,从最低位填入指数部分
  • 尾数部分:小数部分填入尾数部分的最高位
  • 其余空处补0
  • 最后将32位的二进制数转为16进制存储

[!example]
8.25 -> 1000.01 -> 1.00001*2^3
1.00001*^3
0 10000010 00001000000000000000000
0100 0001 0000 0100 0000 0000 0000 0000
41040000h

7.3 浮点类型的精度

float和double的精度是由尾数的位数决定的
float:2^23=8388608,共7位,故最多有7位有效数字
double:2^52=4503599627370496,共16位,故最多有16位有效数字

8. 字符与字符串

8.1 字符的使用

1
2
3
int x='A'; int y='B';

putchar(x) //#include <stdio.h>

ASCII表

8.2 字符类型

ASCII表最后一位为127位(7F),因此一个字节即可存储
于是同常使用char x='A'来存储字符
实际上char是一个只能存储1字节的整型,而不是字符类型

8.3 转义字符

转义字符:\

1
2
char i='n'; //输出n
char i='\n'; //输出换行

8.4 printf函数的使用

字符串:一堆字符的ASCII拼在一起

1
2
3
4
5
6
7
printf("Hello_World!\n"); //打印字符串

printf("%d %u %x\n); //打印整型
//%d有符号数,%u无符号数,%x十六进制

printf("%6.2f\n",f); //打印浮点数
//6位,其中小数点后2位

9. 中文字符

  • 拓展ASCII码表后,将两个大于127的字符连在一起,表示一个汉字,这种编码规则就是GB2312或GB2312-80
  • 在这些编码里,连在ASCII里本来就有的数字、标点、字母都重新编了两个字节长的编码,这就是常说的全角字符,而原来的127号之前的就叫做半角字符

弊端:
<1>两种编码可能使用相同的数字代表两个不同的符号
因此,出现了Unicode编码

10. 运算符与表达式

10.1 运算符与表达式

表达式的结果:
char –> short –> int –> float –> double

10.2 算数运算符

+ - * / % ++ –
加 减 乘 除 取余 自增 自减
++x; //
x++; //
1050

10.3 关系运算符

< <= > >= == !=

  • 关系运算符的值只能是0和1
  • 关系运算符的值为真(假)时,结果值都为1(0)

10.4 逻辑运算符

! && ||
逻辑非 逻辑与(且) 逻辑或(或)

[!note] 汇编层面 &&(逻辑与)和&(与运算)的不同
&&:在汇编中,判断&&前面的条件如果不成立,会直接JMP到后面然后直接赋值0
&:判断所有条件,最后再依次与运算得到结果

  • 因此条件判断时逻辑与效率更高

10.5 位运算符

<< >> ~ | ^ &

10.6 赋值运算符

=拓展赋值 += -= *= /= <<=

10.7 条件运算符

? : //三目运算符

  • 表达式?值1:值2
    表达式结果为真(1)时,返回值1
    表达式结果为假(!1)时,返回值2

10.8 运算符优先级

没必要记住所有运算优先级
通过()灵活地改变运算优先级即可

11. 分支语句

11.1 if语句

1
2
3
4
5
6
7
8
9
10
11
if(表达式)
语句;

或者

if(表达式)
{
语句1
语句2
}

11.2 if…else…语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(表达式)
语句;
else
语句;

或者

if(表达式)
{
语句;
}
else
{
语句;
}

11.3 if…else if…else if…else

1
2
3
4
5
6
7
8
9
if(表达式)
{
语句;
}
else if(表达式)
{
语句;
}
...

PS:多个if判断只会成立一个,也就是说先判断的if语句,判断为真后,后续else if不会再进行判断了

11.4 if语句嵌套

1
2
3
4
5
6
7
8
if(表达式)
{
if...
}
else
{
if...
}

12. switch语句

12.1 语法格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
switch(表达式)
{
case 常量表达式1:
语句;
break;

case 常量表达式2:
语句;
break;
...
case 常量表达式n:
语句;
break;
default:
语句;
break:
}
  1. 表达式结果不能是浮点数,需要为整型
  2. case后的值不能一样
  3. casw后的值必须是常量
  4. switch语句判定的case成立后,会执行当前开始往后的所有同级语句,直到碰到break语句为止(区分条件合并与循环结束)
  5. default语句和位置无关,区别在于写不写break语句

12.2 条件合并的写法

1
2
3
4
5
6
7
8
9
10
11
12
switch(n)
{
case 1:case 2:
printf("----------\n");
break;
case 3:
printf("++++++++++\n");
break;
default:
printf("==========\n");
break:
}

12.3 break语句

  • break语句用于停止当前所在的一层循环或switch语句(没有if语句,在if语句中会无视if语句,跳出自身所在的最近一层循环)。也就是说break实际跳出的是包含它的循环,而不是外部循环

12.3 Switch语句与if…else语句的区别

<1>switch语句只进行等值判断,而if…else可以进行区间判断
<2>switch结构的执行效率远高于if…else,在分支条件比较多的情况愈发明显

13. switch语句高效的原因

  • 游戏快捷键往往使用switch语句来进行编写,可读性和效率更高

14. 循环语句-while

C语言中的goto语句,跳转到;对应反汇编里的JMP指令

14.1 循环语句的种类

  • while语句
  • do while语句
  • for语句

14.2 while语句

1
2
3
4
5
6
7
8
9
10
while(表达式)
语句;

或者

while(表达式)
{
语句;
语句;
}

14.3 continue语句

当程序运行到continue语句时,立即结束当前循环,并回到循环语句开头,继续执行

15. 循环语句-do…while,for

15.1 do…while语句

1
2
3
4
do
{
语句;
}while(表达式);

特点:
即使表达式不成立,也会执行一次

15.2 for语句

1
2
3
4
for(表达式1;表达式2;表达式3)
{
语句;
}

for语句的表达式均可省略

[!note]
反汇编中JMP指令跳回前面指令的,一定有循环语句

16. 自动关机小程序

16.1 开机启动设置

  • 在注册表的”HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run”目录文件夹中 新建字符串值 并配置好软件路径 即可添加开机启动项

16.2 常见的DOS命令

  • 设置控制台颜色:color A(可选值0-F)
  • 打开某个程序: start C:\Dbgview.exe
  • 删除某个文件:del C:\a.txt
  • 关机:shutdown f -s -t 10(10秒后自动关机)

16.3 system函数

头文件:#include <stdlib.h>

1
2
3
4
5
6
7
8
9
system("pause");

system("color 0A");

system("start C:\Dbgview.exe");

system("del C:\a.txt");

system("shutdown f -s -t 10");

16.4 解决方法

安全模式(开机F8)下删除自己添加的启动项,重启

17. 数组

17.1 数组的定义

定义格式:数据类型 变量名[常量];

17.2 数组的初始化

  • 方法一:
    int arr[10]={0,0,0,0,0,0,0,0,0,0};

  • 方法二:
    int arr[]={1,2,3,4,5,6,7,8,9,10};

17.3 数组的内存分配

[!important] 本机宽度
32位计算机 – 4字节
16位计算机 – 2字节
64位计算机 – 8字节
支持最好,效率高但是占存储更多

  • 写入:
1
2
arr[0]=1;
arr[1]=2;
  • 读取:
1
r=arr[0];

声明数组长度时只能为常量,使用数组时可以使用变量

17.4 数组越界访问

1
2
int arr[10];
arr[10]=100;

数组长度为10,但我们将数据存在第11位,且正常通过编译,这种情况我们称为数组越界
数组越界可以访问,通过堆栈理解

17.5 缓冲区溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <windows.h>

void Fun(){
while(1)
{
printf("缓冲区溢出\n");
}
}
int check(){
int arr[8];
arr[9]=(int)&Fun;
return 0;
}
void main(){
check();
getchar();
return;
}

18. 多维数组

18.1 多维数组的定义

int arr[45] ⇔ int arr[5*9] ⇔ int arr[5][9]
int arr[5][9],又称为多维数组

18.2 二维数组的初始化

1
2
3
4
5
int arr[3][4]={
{1,2,3,4},
{5,6,7,8},
{9,7,6,5}
}

//3个花括号,每个花括号里4个值

18.3 二维数组的存储方式

int arr[3][4]
内存中为连续存储,因此在底层中,没有多维数组的概念,因为不管多少维,存储方式均相同
多维数组只是方便处理使用

1
2
3
4
5
6
7
int arr[3][4]={
{1,2,3,4},
{5,6,7,8}.
{9,7,6,5}
}
等价于
int arr[3*4]={1,2,3,4,5,6,7,8,9,7,6,5};

18.4 二维数组的读写

1
2
3
4
5
6
7
int arr[5][12]={
{1,2,1,4,5,6,7,8,9,1,2,3}
{1,2,1,4,5,6,7,8,9,1,2,3}
{1,2,1,4,5,6,7,8,9,1,2,3}
{1,2,1,4,5,6,7,8,9,1,2,3}
{1,2,1,4,5,6,7,8,9,1,2,3}
}
  • 第一组的第9个数据 arr[0][8](C语言) ⇔ arr[0*12+8](编译器)
  • 第二组的第8个数据 arr[1][7](C语言) ⇔ arr[1*12+7](编译器)

18.5 多维数组的存储与读写

假设一共有5个班,每班4组,每组3人

1
2
3
4
5
6
7
int arr[5][4][3]={
{{1,2,3},{4,5,6},{7,8,9},{11,12,13}},
{{11,12,13},{14,15,16},{17,18,19},{111,112,113}},
{{21,22,23},{24,25,26},{27,28,29},{211,212,213}},
{{31,32,33},{34,35,36},{37,38,39},{311,312,313}},
{{41,42,43},{44,45,46},{47,48,49},{411,412,413}},
}

获得第2个班级、第3组、第2人的年龄arr[1][2][1]
编译器计算 arr[1*4*3+2*3+1]

19. 结构体