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 //HEX
  • 二进制 – 通常b结尾,例如1010b //BIN
  • 八进制 – 通常oq结尾,例如77o //OCT
  • 十进制 – 默认无后缀,或有时用d //DEC

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. 局部变量在使用前必须要赋值
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 运算符优先级

没必要记住所有运算优先级
通过()灵活地改变运算优先级即可
C语言运算优先级和结合性一览表

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. 结构体

19.1 概念

由于数组的返回类型是确定的,所以不能用作一个能够存储多种数据长度的容器
实际上C语言中并没有这样的类型,但是我们可以自定义这样的容器

19.2 结构体类型的定义

1
2
3
4
5
6
struct 类型名{
//可以定义多种类型
int a;
char b;
short c;
};

<1> char/int数组 等是编译器已经认识的类型:内置类型
<2> 结构体是编译器不认识的,用的时候需要告诉编译器:自定义类型
<3> 定义结构体仅仅是告诉编译器自定义类型是什么样的,本身并不占用内存
<4> 结构体在定义的时候,除了自身以外,可以使用任何类型(st2可以用st1)

[!example]

1
2
3
4
5
6
7
8
9
>struct stStudent
>{
int stucode;
char stuName[20];
int stuAge;
char stuSex;
>};

>struct stStudent student={101,"张三",18,'M'};

19.3 结构体类型变量的读写

//读
x = point.x;
y = point.y;

//写
point.x = 100;
point.y = 200;

//结构体赋值时不可以使用{}(仅初始化时可以),可以通过 结构体变量名=(struct 类型名){} 赋值,这是复合字面量赋值方法

19.4 定义结构体类型的时候,直接定义变量

1
2
3
4
struct stPoint{
int x;
int y;
}point 1,point 2,point 3;

//这种方式是分配内存的,因为定义结构体时顺便定义了3个全局变量

20. 字节对齐

20.1 什么是字节对齐

字节对齐:

  • 一个变量占用n个字节,则该变量的起始地址必须是n的整数倍,即:存放起始地址%n=0
  • 如果是结构体,那么结构体的起始地址是其最宽数据类型成员的整数倍
    //字节对齐可提升程序执行效率
    //结构体中的成员也遵守字节对齐

20.2 sizeof的使用

sizeof可以计算出当前类型的宽度
//sizeof计算结构体时别忘了字节对齐

20.3 对齐方式

当对空间要求较高时,可以通过#pragma pack(n)来改变结构体成员的对齐方式

1
2
3
4
5
6
#pragma pack(1)
struct Test{
char a;
int b;
}
#pragma pack()

<1> #pragma pack(n)中n用来设定字节以n字节为对齐方式(1、2、4、8)
<2> 若需取消强制对齐方式,则可用命令#pragma pack()

20.4 结构体大小

结构体的总大小:是N的整数倍
N=Min(最大成员,对齐参数)

21. 结构体数组

21.1 结构体数组的定义

类型 变量名[常量表达式]

1
2
3
4
5
6
7
8
struct stStudent
{
int Age;
int Level;
};

struct stStudent st; //定义结构体变量
struct stStudent arr[10] 或 stStudent arr[10]; //定义结构体数组

21.2 结构体数组初始化

1
2
3
4
5
6
7
8
9
struct stStudent
{
int Age;
int Level;
};
struct stStudent arr[5]={{0,0},{1,1},{2,2},{3,3},{4,4}};

arr[0].Age=100;
arr[0].Level=100;

21.3 结构体成员的使用

格式:结构体数组名[下标].成员名

21.4 字符串成员的处理

1
2
3
4
5
6
struct stStudent
{
int Age;
char Name[0x20];
};
struct stStudent arr[3]={{1,"张三"},{2,"李四"},{3,"王五"}};

//读
char buffer[0x20];
strcpy(buffer,arr[0].Name);

//写
strcpy(arr[0].Name,”张三”);

21.5 结构体数组的内存结构

结构体数组在内存中是连续存储的

22. 指针类型

指针类型是一种数据类型

22.1 定义带”*“类型的变量

char x -> char* x;
int z -> int* z;
float f -> float* f
stStudent st -> stStudent* st;

  1. 带有*的变量类型的标准写法:变量类型* 变量名
  2. 任何类型都可以带* 加上*后就是新的类型,统称为指针类型
  3. *可以是任意多个

22.2 指针变量赋值

1
2
3
4
5
6
7
8
char* x;
x=(char*)1; //指针类型赋值时,括号内不能省略不写。其他类型可省略不写

int**** x;
int**** y;
x=(int****)4;
y=x;
//类型一致即可直接赋值

22.3 指针变量宽度

直接总结:
指针类型的变量宽度永远是4字节,无论类型、*数量

22.4 指针类型的自加自减

非指针类型:++/– 都是自加1/自减1;
指针类型:++/– 是去掉一个*后,变量的类型宽度

[!example] 指针类型的++/–

1
2
3
4
5
6
7
8
>char* a;
>char** b;

>a = (char*) 100;
>b = (char**) 100;

>a++;
>b++;

结果a=101b=104,即a自加1,b自加4

22.5 指针类型的加减运算

  1. 指针类型变量可以加、减一个整数,但是不可以乘、除
  2. 指针类型与其他整数的加减法则:
    • 指针类型变量 + N = 指针类型变量 + N *(去掉一个*后类型的宽度)
    • 指针类型变量 - N = 指针类型变量 - N *(去掉一个*后类型的宽度)

22.6 指针类型的比较

相同指针类型的变量可以进行比较大小

23. &的使用

23.1 定义

&是取地址符,任何变量都可以使用&来获取地址,但不能用在常量上
//本质:常量在汇编中不分配地址,所以无法取地址

23.2 &变量的类型

可以通过编写程序的报错信息进行类型的判断
得出&变量是原类型的指针变量(即在原类型后加一*)

23.3 指针变量赋值

1
2
char x;
p1=&x;

24. 取值运算符

24.1 “*“的几种用途

<1>乘法运算符

1
2
3
int x=1;
int y=2;
int z=x*y;

<2>定义新的类型

1
2
char x;
char* y;

<3>取值运算符
* + 指针类型的变量

1
2
int* a=(int*)1;
printf("%x \n",*a);

24.2 *指针类型 的类型

一样,利用报错信息可以判断数据类型
得出 *指针类型 是 原类型去掉一个*后的类型

24.3 取值运算符举例

1
2
3
4
5
6
int x=1;
int* p=&x; //把x的地址存到p中
printf("%x %x\n",p,*(p));

*(p)=2; //给 p中所存的内存地址相对应的值 赋值2
printf("%d\n",x);

25. 数组参数传递

25.1 基本类型参数传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int x=1;

void plus(int p)
{
p=p+1;
}

int main()
{
int x=1;
plus(x); //参数传递,传的是值,不是变量,所以x没变
printf("%d\n",x);
return 0;
}

25.2 数组传参

1
2
3
4
5
6
7
void PrintArray(int arr[],int nLength)
{
for(int i=0;i<nLength;i++)
{
printf("%d\n",arr[i]);
}
}
  1. 数组作为参数传递时,传递的是地址
  2. 数组作为参数传递时,应该传递数组的长度

25.3 指针操作数组

1
2
3
4
5
6
7
void PrintArrayByPoint(int* p,int nLength)
{
for(int i=0;i<nLength;i++)
{
printf("%d\n",*(p+i));
}
}

26. 指针与字符串

26.1 字符串的不同表达方式

1
2
3
4
5
char str[6]={'A','B','C','D','E','F'}; //用printf函数打印时,数组末尾记得加'\0'或0
char str[]="ABCDEF"; //编译器末尾填0
char* str="ABCDEF"; //常量区

printf("%s\n",str);

26.2 常用的字符串函数

  1. int strlen(char* s)
    返回值时字符串s的长度(不包括结束符’\0’)
  2. char* strcpy(char* dest,char* src)
    复制字符串src到dest中。返回指针为dest的值
  3. char* strcat(char* dest,char* src)
    将字符串src追加到dest尾部。返回指针为dest的值
  4. int strcmp(char* s1,char* s2)
    s1和s2相同时返回0,不同时返回1(常用于验证字符串是否相等)

26.3 指针函数

本质就是函数,只不过函数的返回类型是某一类型的指针

[!example]
strcpy()函数、strcat()函数

27. 指针取值的两种方式

27.1 一级和多级指针

1
2
3
4
5
6
int i;

int* p1=&i;
int** p2=&p1;
int*** p3=&p2;
int**** p4=&p3;

27.2 *()与[]的互换表示

*(*(*(p)))=*(*(*(p+0)+0)+0)=p[0][0][0]

27.3 总结

1
2
3
4
*(p+i)=p[i]
*(*(p+i)+k)=p[i][k]
*(*(*(p+i)+k)+m)=p[i][k][m]
*(*(*(*(*(p+i)+k)+m)+w)+t)=p[i][k][m][w][t]

*()和[]可以相互转换

28. 结构体指针