指针类型
在C语言里面指针是一种数据类型,是给编译看的,也就是说指针与int、char、数组、结构体是平级的,都是一个类型。
带"*"号的变量我们称之为指针类型,例如:
char* x;short* y;int* a;float* b;...任何类型都可以带这个符号,格式就是:类型* 名称;星号可以是多个。
指针变量的赋值格式如下:

指针类型的变量宽度永远是4字节,无论类型是什么,无论有几个星号。
指针类型和其他数据类型是一样的,也可以做自增、自减,但与其他数据类型不一样的是,指针类型自增、自减是加、减去掉一个星号后的宽度。
如下代码:
#include <stdio.h>int main(){ int** a; char** b; a = (int**)1; b = (char**)2; a++; b++; printf("%d %d \n", a, b); return 0;}第一次自增,a+4 = 5,b+4 = 6,因为减去一个星号还是指针,指针的宽度永远是4:

自减同理可得,那么我们知道了自增、自减就可以知道指针类型的加减运算规律:
因为自增、自减,本质上就是+1,所以我们可以得出算式:
指针类型变量 + N = 指针类型变量 + N * (去掉一个*后类型的宽度)
指针类型变量 - N = 指针类型变量 - N * (去掉一个*后类型的宽度)
但需要注意,指针类型无法做乘除运算。
最后:指针类型是可以做比较的。

&的使用
&符号是取地址符,任何变量都可以使用&来获取地址,但不能用在常量上。
如下代码:
#include <stdio.h>struct Point { int x; int y;};char a;short b;int c;Point p;int main(){ printf("%x %x %x %x \n", &a, &b, &c, &p); return 0;}
在这里使用取地址符可以直接获取每个变量、结构体的地址,但是这种格式可能跟我们之前看到的8位不一样,前面少了2个0,这时候可以将%x占位符替换为%p来打印显示。

那么取地址符(&变量)的类型是什么呢?我们可以来探测下:
char a;short* b;int** c;int x = &a;int x = &b;int x = &c;以上代码我们在编译的时候是没法成功的,它的报错内容是:

通过报错内容我们可以看出类型不同无法转换,但是我仔细观察报错内容:char*无法转换成int,short**无法转换成int...那么就说明了一点,在我们使用取地址符时,变量会在其原本的数据类型后加一个星号。
可以这样进行指针赋值:
char x;char* p1;char** p2;char*** p3;char**** p4;p1 = &x; // &x -> char*p2 = &p1;p3 = &p2;p4 = &p3;p1 = (char*)p4;取值运算符
取值运算符就是我们之前所了解的“*”星号,“*”星号有这几个用途:
-
乘法运算符(1*2)
-
定义新的类型(char*)
-
取值运算符(星号+指针类型的变量);也就是取地址的存储的值。
如下代码就是使用取值运算符:
int* a = (int*)1;printf("%x \n", *a);这段代码可以编译,但是是无法运行的,我们可以运行一下然后来看看反汇编代码:


如上反汇编代码,我们可以清楚的看见首先0x1给了局部变量(ebp-4),之后这个局部变量(ebp-4)给了eax,而后eax又作为了内存地址去寻找对应存储的值,但是这里eax为0x1,所以在内存中根本就不存在这个地址,也就没办法找到对应的值,自然就无法运行。
那么取值运算符(星号+指针类型)是什么类型呢?我们来探测下:
int*** a;int**** b;int***** c;int* d;int x = *(a);int x = *(b);int x = *(c);int x = *(d);以上代码我们在编译的时候是没法成功的,它的报错内容是:

通过报错内容我们可以看出类型不同无法转换,但是我仔细观察报错内容:int**无法转换成int,int***无法转换成int...那么就说明了一点,在我们使用取值运算符时,变量会在其原本的数据类型后减去一个星号。
取值运算符举例:
int x = 1;int* p = &x;int** p2 = &p;*(p) = 2;int*** p3 = &p2;int r = *(*(*(p3)));数组参数传递
之前我们学过几本类型的参数传递,如下代码所示:
#include <stdio.h>void plus(int p) { p = p + 1;}int main(){ int x = 1; plus(x); printf("%d \n", x); return 0;}如上代码变量x最终值是多少?相信很多人都知道答案了,是1,原封不动。

为什么是1?这是因为在变量x作为参数传入plus函数,是传入值而不是这个变量本身,所以并不会改变变量本身。
数组也是可以作为参数传递,我们想要传入一个数组,然后打印数组的值(定义数组参数时方括号中不要写入常量):
#include <stdio.h>void printArray(int arr[], int aLength) { for (int i=0; i<aLength; i++) { printf("%d \n", arr[i]); }}int main(){ int arr[] = {0,1,2,3,4}; printArray(arr, 5); return 0;}我们想打印数组不仅要知道数组是什么,也要获取数组的长度,所以需要两个参数(实际上我们也有其他方法获取长度,这里先不多说)。

我们来看下反汇编代码,看看数组是否和基本类型一样传入的是值:

通过如上代码所示,我们可以很清晰的看见,ebp-14也就是数组的第一个值的地址给了eax,最后eax压入堆栈,也就是传递给了函数。
所以我们得出结论:数组参数传递时,传入的是数组第一个值的地址,而不是值;换而言之,我们在printArray函数中修改传入的数组,也就修改了数组本身。
我们再换个思路,在数组作为参数传递的时候,可以换一种形式,直接传入地址也是可以打印的,也就是使用指针来操作数组:

事实证明这是可行的,我们在传入参数的时候使用数组第一个值的地址即可。
指针与字符串
在学习完指针类型后,我们可以来了解一下这些函数:
int strlen(char* s); // 返回类型是字符串s的长度,不包含结束符号\0char* strcpy(char* dest, char* src); // 复制字符串src到dest中,返回指针为dest的值char* strcat(char* dest, char* src); // 将字符串src添加到dest尾部,返回指针为dest的值int strcmp(char* s1, char* s2); // 比较s1和s2,一样则返回0,不一样返回非0字符串的几种表现形式:
char str[6] = {'A','B','C','D','E','F'};char str[] = "ABCDE";char* str = "ABCDE";指针函数(本质就是函数,只不过函数的返回类型是某一类型的指针):
char* strcpy(char* dest, char* src);char* strcat(char* dest, char* src);指针取值的两种方式
如下图所示的则是一级指针(一个星号)和多级指针(多个星号):

这段代码看着很复杂,但我们有基础后再看它轻而易举,脑子里浮现的就是汇编代码。
指针取值有两种方式,如下代码:
#include <stdio.h>int main(){ int* p = (int*)1; printf("%d %d", *(p), p[0]); return 0;}我们可以使用取值运算符,也可以使用数组的方式,因为其本质都是一样的,我们来看下反汇编代码:

也就说明:*()与[]的互换,如下是互换的一些例子:
int* p = (int*)1;printf("%d %d \n",p[0],*p); //p[0] = *(p+0) = *p int** p = (int**)1;printf("%d %d \n",p[0][0],**p); printf("%d %d \n",p[1][2],*(*(p+1)+2));int*** p = (int***)1;printf("%d %d \n",p[1][2][3],*(*(*(p+1)+2)+3)); /**(*(*(*(*(*(*(p7))))))))= *(*(*(*(*(*(p7+0)+0)+0)+0)+0)+0)= p7[0][0][0][0][0][0][0]*/总结:
*(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] 结构体指针
我们来了解一下结构体指针,如下代码:
#include <stdio.h>struct Point { int a; int b;};int main(){ Point p; Point* px = &p; printf("%d \n", sizeof(px)); return 0;}我们打印结构体指针的宽度,最终结果是4,这时候我们需要知道不论你是什么类型的指针,其特性就是我们之前说的指针的特性,并不会改变。
如下代码就是使用结构体指针:
// 创建结构体Point p;p.x=10;p.y=20;// 声明结构体指针Point* ps;// 为结构体指针赋值ps = &p;// 通过指针读取数据printf("%d \n",ps->x);// 通过指针修改数据ps->y=100;printf("%d\n",ps->y);提问:结构体指针一定要指向结构体吗?如下代码就是最好的解释:
#include <stdio.h>struct Point{ int x; int y;};int main(){ int arr[10] = {1,2,3,4,5,6,7,8,9,10}; Point* p = (Point*)arr; int x = p->x; int y = p->y; printf("%d %d \n", x, y); return 0;}
指针数组与数组指针
指针数组和数组指针,这两个是完全不一样的东西,指针数组的定义:
char* arr[10];Point* arr[10];int********** arr[10];指针数组的赋值方式:
char* a = "Hello";char* b = "World";// onechar* arr[2] = {a, b};// twochar* arr1[2];arr1[0] = a;arr1[1] = b;// threechar* arr2[2] = {"Hello", "World"};一共有三种赋值方式,在实际应用中我们更偏向于第三种方式。
结构体指针也有数组,我们可以看下其定义和对应宽度:

接下来我们要学习的是数组指针,数组指针在实际应用很少用到,数组指针是最难学的。
首先分析一下如下代码:
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};int* p = &arr[0];int* p = arr; // &arr[0] int* p = (int *)&arr; // &arr -> int *[10]&arr就是我们要学的数组指针,也就是int *[10],数组指针的定义如下:
int(*px)[5]; // 一维数组指针char(*px)[3];int(*px)[2][2]; // 二维数组指针char(*px)[3][3][3]; // 三维数组指针px就是我们随便定义的名字,本质上就是指针,那么也就有着指针的特性,无论是长度还是加减法...
思考:int *p[5] 与 int(*p)[5]有什么区别?我们可以来看看宽度:

可以看见一个是20,一个是4;一个是指针变量的数组,一个是数组指针,本质是不一样的。
数组指针的宽度和赋值:
int(*px1)p[5];char(*px2)[3]; int(*px3)[2][2];char(*px4)[3][3][3];printf("%d %d %d %d\n",sizeof(px1),sizeof(px2),sizeof(px3),sizeof(px4));// 4 4 4 4 px1 = (int (*)[5])1;px2 = (char (*)[3])2;px3 = (int (*)[2][2])3;px4 = (char (*)[3][3][3])4;数组指针的运算:
int(*px1)p[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++; //int(4) *5 +20 =21px2++; //char(1) *3 +3 =4px3++; //int(4) *2 *2 +16 =17px4++; //char(1) *3 *3 *3 +9 =10printf("%d %d %d %d \n",px1,px2,px3,px4);数组指针的使用:
// 第一种:int arr[] = {1,2,3,4,5,6,7,8,9,0};int(*px)[10] = &arr;// *px 是啥类型? int[10] 数组类型// px[0] 等价于 *px 所以 *px 也等于 int[10]数组printf("%d %d \n",(*px)[0],px[0][0]);px++; // 后 (*px)[0]就访问整个数组地址后的地址内的数据// 第二种:int arr[3][3] = { {1,2,3}, {4,5,6}, {7,8,9}};// 此时的 px指针 指向的 {1,2,3}这个数组的首地址int(*px)[3] = &arr[0];// *px -> 此时就是数组{1,2,3}本身// 越过第一个数组 此时px指针指向 {4,5,6}的首地址px++;printf("%d %d \n",(*px)[0],px[0][0]);// 这里打印的就是 4 4思考:二维数组指针可以访问一维数组吗?
int arr[] = {1,2,3,4,5,6,7,8,9,0};int(*px)[2][2] = (int(*)[2][2])arr;是可以的,因为*px实际上就是int[2][2],我们之前学过多维数组,int[2][2]也就等于int[4],所以{1,2,3,4}就给了int[2][2],也就是{{1,2}, {3,4}},所以(*px)[1][1]为4。

调用约定
函数调用约定就是告诉编译器怎么传递参数,怎么传递返回值,怎么平衡堆栈。
常见的几种调用约定:
|
调用约定 |
参数压栈顺序 |
平衡堆栈 |
|
__cdecl |
从右至左入栈 |
调用者清理栈 |
|
__stdcall |
从右至左入栈 |
自身清理堆栈 |
|
__fastcall |
ECX/EDX 传送前两个,剩下:从右至左入栈 |
自身清理堆栈 |
一般情况下自带库默认使用 __stdcall,我们写的代码默认使用 __cdecl,更换调用约定就是在函数名称前面加上关键词:
int __stdcall method(int x,int y){ return x+y;}method(1,2);函数指针
函数指针变量定义的格式:
// 返回类型 (调用约定 *变量名)(参数列表);int (__cdecl *pFun)(int,int);函数指针类型变量的赋值与使用:
// 定义函数指针变量int (__cdecl *pFun)(int,int);// 为函数指针变量赋值pFun = (int (__cdecl *)(int,int))10;// 使用函数指针变量int r = pFun(1,2);我们来看下函数指针的反汇编代码:

可以很清晰的看见函数指针生成来一堆汇编代码,传参、调用、以及如何平衡堆栈;而如上这段代码最终会调用地址0xA,但它本身不存在,所以无法运行。
所以我们想要调用某个函数时可以将地址赋值给pFun即可,并且在定义时写好对应的参数列表即可;也就是说函数指针通常用来使用别人写好的函数。
我们也通过函数指针绕过调试器断点,假设有攻击者要破解你的程序,它在MessageBox下断点,你正常的代码就会被成功断点,但是如果你使用函数指针的方式就可以绕过。
首先,我们先写一段正常的MessageBox程序,然后使用DTDebug来下断点:

可以看见,我们下断点成功断下来了,断点本质上就是在MOV EDI, EDI所在那行的地址下断点,那么我们可以直接跳过这行调用调用下一行,实际上这段汇编的核心在于我标记的部分,准确一点的说就是CALL指令哪一行,我们可以右键Follo跟进:


我们可以从0x77D5057D开始执行,通过汇编代码可以知道这个函数需要5个参数,并且最后的RETN则表示内平栈则使用__stdcall,所以我们函数指针(操作系统API返回通常是4字节)可以这样写:
#include <windows.h>int main(){ int (__stdcall *pFun)(int,int,int,int,int); pFun = (int (__stdcall *)(int,int,int,int,int))0x77D5057D; MessageBox(0,0,0,0); pFun(0,0,0,0,0); return 0;}这样就可以绕过断点了:
