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(最大成员,对齐参数)
//对齐参数 即 对齐方式中修改的n

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

28.1 使用结构体指针

  • 创建结构体
1
2
3
Point p;
p.x=10;
p.y=20;
  • 声明结构体指针
1
Point* ps;
  • 为结构体指针赋值
1
ps=&p;
  • 通过指针读取数据
1
printf("%d\n",ps->x);
  • 通过指针修改数据
1
2
3
ps->y=100;

printf("%d\n",ps->y);

28.2 结构体指针不一定指向结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Point
{
int x;
int y;
}

int arr[10]={1,2,3,4,5,6,7,8,9,10};
Point* p=(Point*)arr;

for(int i=0;i<5;i++,p++) //结构体宽度为8,指针p++<=>p=p+8,由于int宽度为4,则数组位数加2
{
printf("%d %d\n",p->x,p->y);
}

29. 指针数组与数组指针

29.1 指针数组的定义

1
2
3
4
char arr[10];
char* arr[10];
Point* arr[10];
int***** arr[10];

29.2 指针数组的赋值

1
2
3
4
5
6
7
8
9
10
11
12
char* a="Hello";

char* arr[2]={a,b};

char* arr[2];
arr[0]=a;
arr[1]=b;

char* arr[2]={
"Hello",
"World"
}; //数组中存储的是两个字符串的地址

29.3 结构体指针数组

1
2
3
4
5
6
7
8
9
struct Point
{
int x;
int y;
}

Point p; //8 Byte
Point arr[10]; //8*10 Byte
Point* arrPoint[10]; //4*10 Byte

29.4 初识数组指针

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

int* p=arr; //int* p=&arr[0]的简写形式
int* p=&arr; //数组指针 int(*)[10]

29.5 数组指针的定义

1
2
3
4
5
6
int(*px)[5];
char(*px)[3]; //一维数组指针

int(*px)[2][2]; //二维数组指针

char(*px)[3][3][3]; //三维数组指针

29.6 数组指针的宽度与赋值

数组指针本质还是指针,所以遵循规则,数组指针的宽度也一定是4个字节

1
2
px1=(int(*)[5])1;
px2=(char(*)[2][2])2;

29.7 数组指针的运算

1
2
3
4
5
6
7
8
9
10
11
12
int(*px1) [5];
char(*px2) [3];
int(*px3) [2][2];
char(*px4) [3][3][3];

px1=(int(*)[5])1;
px2=(char(*)[3])1;
px3=(int(*)[2][2])1;
px4=(char(*)[3][3][3])1;

px1++;px2++;px3++;px4++;
//px1=21;px2=4;px3=17;px4=28;

29.8 数组指针的使用

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

int(*px)[10]=&arr;

printf("%d %d\n",(*px)[0],px[0][0]);

/*
px++;
printf("%x %x\n",(*px)[0],px[0][0]);
打印内容为数组指针跳过一整个数组后指向的值
*/

29.9 多维数组指针的理解

多维数组指针和一维数组指针没有本质上的区别,两者都是指针,指向的都是地址
所以可以用多维数组指针访问一维数组

[!example]

1
2
>int arr[]={1,2,3,4,5,6,7,8,9,0};
>int(*px)[2][2]=(int(*)[2][2])arr;

若是赋值*px=[1][1],则赋值结果应为4

[!note]

1
2
>int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
>int (*p)[2][2] = (int (*)[2][2])arr;

(*p)[0][1]和*(p[0][1])的区别:

  • (*p)[0][1]:先解引用,得到arr数组,然后[0][1]二维数组索引第一行第二个的值2
  • *(p[0][1]):由于p是二维数组指针,所以p[0]指针先指向第一组,然后[1]二维数组索引第一行,默认取第一行首地址,最后对其解引用得其中的值3

***造成不同情况的根本原因:索引时,由于指针特性导致的起始地址不同***

30. 调用约定

30.1 函数调用约定

通俗来讲就是告诉编译器,怎么传递参数、怎么传递返回值、怎么平衡堆栈

30.2 常见的调用约定

调用约定 参数压栈顺序 平衡堆栈
__cdecl 从右至左入栈 调用者清理堆栈(外平栈)
__stdcall 从右至左入栈 自身清理堆栈(内平栈)
__fastcall ECX/EDX传送前两个,剩下的从右至左入栈 自身清理堆栈(内平栈)

//返回值均存在寄存器EAX中

31. 函数指针

31.1 函数指针类型变量的定义

定义格式:返回类型 (调用约定 *变量名)(参数列表);
int (__cdecl *pFun)(int,int);

31.2 函数指针类型变量的赋值与使用

1
2
3
4
5
6
//定义
int (__cdecl *pFun)(int,int);
//赋值
pFun=(int(__cdecl *)int,int)10;
//使用
pFun(1,2);

31.3 通过函数指针绕过断点

<1>函数指针变量的定义:

1
int (__stdcall *pFun)(int,int,int,int,int);

<2>正常的调用:

1
MessageBox(0,0,0,0);

<3>函数指针绕过:

1
2
pFun=(int(__stdcall *)(int,int,int,int,int))0xFFFFFFFF
pFun(0,0,0,0,0);

[!note] 函数指针个人理解
目的:可以通过指针,跳转到函数的入口地址,调用函数
意义:因为不是直接调用函数,可以跳过一些简单的变量名断点,达到一定的反调试目的
总结:按照给出的要求,调用指定的地址

//函数指针无法进行加减运算,因为去掉一个*后,函数的宽度未知

32. 预处理之宏定义

32.1 定义

  • 预处理一般是指在程序源代码被转换为二进制代码之前,由预处理器对程序源代码文本进行处理,处理后的结果再由编译器进一步编译。

  • 预处理的功能主要包括宏定义,文件包含,条件编译三部分

32.2 宏定义

<1>简单宏:#define 标识符 字符序列

1
#define PI 3.14

<2>带参数的宏:#define 标识符(参数表) 字符序列

1
#define MAX(A,B) ((A)>(B)?(A):(B))

[!tip] 注意事项

  1. 只进行字符序列替换,不检查语法
  2. 多行声明时,每行末尾都需加上”\“再换行,即”\[enter]”
  3. 定义末尾不需要分号

宏定义函数的特点:每一次使用都将定义的内容以完整的指令集形式编译,单次调用时会减少不必要的堆栈使用,节省空间。但是多次调用时,由于每次都是完整编译,会更多的占据空间

33. 条件编译与文件包含

33.1 条件编译

1
2
3
#if 0 //改为1则会进行编译
printf("---------------");
#endif

33.2 常见的预处理指令

指令 用途
#define 定义宏
#undef 取消已定义的宏
#if 如果条件为真,则编译下面代码
#elif 如果#if给定的条件为假,当前条件为真,则编译下面代码
#else 同else
#endif 结束一个#if……#else条件编译块
#ifdef 如果宏已定义,则编译下面代码
#ifndef 如果宏未定义,则编译下面代码
#include 包含文件

33.3 文件包含的两种格式

#include "file"#include <file>

  1. 使用" "时,系统首先到当前目录下查找被包含文件,再到系统指定的“包含文件目录”查找
  2. 使用< >时,直接到系统指定的”包含文件目录“查找

[!note]

  • 包含系统自带的文件时,使用< >
  • 包含自定义文件时,使用" "

33.4 重复包含