滴水-C语言
1. C语言的汇编表示
1.1 基本信息
- 入口程序:程序开始执行的地方
- return:执行结束
1.2 什么是函数
函数是一些列指令的集合,用以完成某个会被重复使用的特定功能
- C语言中函数格式:
返回类型 函数名(参数列表){}
<1>返回类型、函数名不能省略
<2>参数列表可以省略
[!info] 函数名、参数名的命名规则
- 只能以字母、数字、下划线组成,且首字母不为数字
- 区分大小写
- 不能使用关键字
1.3 基本操作
- 创建exe文件(F7)
- 运行(F5)
- 断点(F9)
- 步过(F10)
- 步入(F11)
1.4 函数的调用
- 汇编中的函数调用:
CALL/JMP指令
- C语言中的函数调用:
- 函数名(参数1,参数2);
实际上:函数名就是编译器起的内存地址的别名
2. 参数传递与返回值
2.1 函数定义
1 | 返回类型 函数名(参数列表) |
数据类型:
int – 4 Byte
short – 2 Byte
char – 1 Byte
进制标识:
- 十六进制 – 通常
h结尾,例如40h - 二进制 – 通常
b结尾,例如1010b - 八进制 – 通常
o或q结尾,例如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] 函数名、参数名的命名规则
- 只能以字母、数字、下划线组成,且首字母不为数字
- 区分大小写
- 不能使用关键字
3.2 全局变量
- 编译的时候就已经确定了内存地址和宽度,变量名就是内存地址的别名
- 如果不重写编译,全局变量的内存地址保持不变。游戏外挂中的找“基址”,就是找全局变量
- 全局变量中的值任何程序都可以修改,是公用的
如:CE搜索基址
3.3 局部变量
- 局部变量是函数内部申请的,如果函数没有执行,那么局部变量没有内存空间。
- 局部变量的内存是在堆栈中分配的,程序执行时才分配。我们无法预知程序何时执行,所以我们无法确定局部变量的内存地址。
- 因为局部变量地址内存不确定,所以,局部变量只能在函数内部使用,其他函数不能使用
3.4 变量初始值
- 全局变量可以没有初始值而直接使用,系统默认初始值为0
- 局部变量在使用前必须要赋值
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 – 00xFF0xFFFF
short – 16 BIT –2 Byte – 0
int – 32 BIT – 4 Byte – 00xFFFFFFFF0xFFFFFFFF
long – 32 BIT – 4 Byte – 0
特别说明:int在16位(32位以上)计算机中与short(long)宽度一致
6.3 数据溢出
- char x=0xFF; //1111 1111
- 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/无符号数补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 | int x='A'; int y='B'; |
ASCII表
8.2 字符类型
ASCII表最后一位为127位(7F),因此一个字节即可存储
于是同常使用char x='A'来存储字符
实际上char是一个只能存储1字节的整型,而不是字符类型
8.3 转义字符
转义字符:\
1 | char i='n'; //输出n |
8.4 printf函数的使用
字符串:一堆字符的ASCII拼在一起
1 | printf("Hello_World!\n"); //打印字符串 |
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 | if(表达式) |
11.2 if…else…语句
1 | if(表达式) |
11.3 if…else if…else if…else
1 | if(表达式) |
PS:多个if判断只会成立一个,也就是说先判断的if语句,判断为真后,后续else if不会再进行判断了
11.4 if语句嵌套
1 | if(表达式) |
12. switch语句
12.1 语法格式
1 | switch(表达式) |
- 表达式结果不能是浮点数,需要为整型
- case后的值不能一样
- casw后的值必须是常量
- switch语句判定的case成立后,会执行当前开始往后的所有同级语句,直到碰到break语句为止(区分条件合并与循环结束)
- default语句和位置无关,区别在于写不写break语句
12.2 条件合并的写法
1 | switch(n) |
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 | while(表达式) |
14.3 continue语句
当程序运行到continue语句时,立即结束当前循环,并回到循环语句开头,继续执行
15. 循环语句-do…while,for
15.1 do…while语句
1 | do |
特点:
即使表达式不成立,也会执行一次
15.2 for语句
1 | 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 | system("pause"); |
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 | arr[0]=1; |
- 读取:
1 | r=arr[0]; |
声明数组长度时只能为常量,使用数组时可以使用变量
17.4 数组越界访问
1 | int arr[10]; |
数组长度为10,但我们将数据存在第11位,且正常通过编译,这种情况我们称为数组越界
数组越界可以访问,通过堆栈理解
17.5 缓冲区溢出
1 |
|
18. 多维数组
18.1 多维数组的定义
int arr[45] ⇔ int arr[5*9] ⇔ int arr[5][9]
int arr[5][9],又称为多维数组
18.2 二维数组的初始化
1 | int arr[3][4]={ |
//3个花括号,每个花括号里4个值
18.3 二维数组的存储方式
int arr[3][4]
内存中为连续存储,因此在底层中,没有多维数组的概念,因为不管多少维,存储方式均相同
多维数组只是方便处理使用
1 | int arr[3][4]={ |
18.4 二维数组的读写
1 | int arr[5][12]={ |
- 第一组的第9个数据 arr[0][8](C语言) ⇔ arr[0*12+8](编译器)
- 第二组的第8个数据 arr[1][7](C语言) ⇔ arr[1*12+7](编译器)
18.5 多维数组的存储与读写
假设一共有5个班,每班4组,每组3人
1 | int arr[5][4][3]={ |
获得第2个班级、第3组、第2人的年龄arr[1][2][1]
编译器计算 arr[1*4*3+2*3+1]


