指针数组
什么是指针数组
首先回顾一下先前关于数组的知识:
所谓数组就是用于存储相同数据类型的集合
再结合先前关于指针的知识:指针的本质也是一种数据类型
于是当数组中存储的成员的数据类型为指针时,该数组就可以称为指针数组(本质是数组)
#include "stdafx.h"
void function() {
int** arr[5]={(int**)1,(int**)2,(int**)3,(int**)4,(int**)5};
}
int main(int argc, char* argv[])
{
function();
return 0;
}
9: int** arr[5]={(int**)1,(int**)2,(int**)3,(int**)4,(int**)5};
00401038 mov dword ptr [ebp-14h],1
0040103F mov dword ptr [ebp-10h],2
00401046 mov dword ptr [ebp-0Ch],3
0040104D mov dword ptr [ebp-8],4
00401054 mov dword ptr [ebp-4],5
可以看到指针数组其实并没有什么特别之处,只不过存储的数组成员的数据类型为指针而已
指针数组的赋值也和先前对指针的赋值没有什么区别
结构体指针
什么是结构体指针
所谓结构体指针就是在结构体后加上若干个使其称为一个指针类型
#include "stdafx.h"
#include <typeinfo>
struct S1{
int a;
};
void function(){
S1* s1=(S1*)0x12345678;
printf("%x\n",s1);
}
int main(int argc, char* argv[])
{
function();
return 0;
}

可以看到,这里关于结构体指针的使用貌似和普通的指针没有什么区别,但此时会发现这里还没有操作结构体内部的成员
所以结构体指针的实际使用也并不是这样,下面看一个错误的例子
void function() {
S1* s1=(S1*)0x12345678;
int a=s1->a;
}
只是在上面代码的基础上添加了一个读取结构体成员的语句,查看运行结果

运行结果不出所料出错了,开始分析错误的原因
14: S1* s1=(S1*)0x12345678;
00401038 mov dword ptr [ebp-4],12345678h
15: int a=s1->a;
0040103F mov eax,dword ptr [ebp-4]
00401042 mov ecx,dword ptr [eax]
00401044 mov dword ptr [ebp-8],ecx
反汇编分析
0.执行前s1和s1->a的状态
s1:

s1->a:

1.为结构体指针s1赋值
14: S1* s1=(S1*)0x12345678;
00401038 mov dword ptr [ebp-4],12345678h

此时再看看s1->a:

可以发现对s1的赋值操作,改变的不是s1->a的值,而是改变了s1->a的地址
其实从执行前s1和s1->a的状态就可以看出,s1存储的内容并不是直接存储结构体成员的内容,而是存储指向结构体成员的地址
所以这里对于先前对于s1的赋值操作改变的只是成员的地址,而没有改变成员的值
并且刚开始时,结构体成员并没有被分配对应的内存地址
2.访问s1->a
15: int a=s1->a;
0040103F mov eax,dword ptr [ebp-4]
00401042 mov ecx,dword ptr [eax]
00401044 mov dword ptr [ebp-8],ecx
此时出错的原因已经显而易见了,先前对s1的赋值操作修改了s1->a的地址,使其指向了一个不可访问的地址而导致出错
正确代码
前面已经知道了出错的原因是访问了不可访问的地址导致出错,并且刚开始结构体成员没有被分配对应的内存地址
于是只要手动为结构体成员分配内存地址即可,这里将使用到malloc函数来进行分配内存地址
malloc函数
void *malloc(size_t size)
参数:size,内存块的大小,以字节为单位
返回值:返回一个指针 ,指向已分配大小的内存。如果请求失败,则返回 NULL
相关头文件:malloc.h、alloc.h、stdlib.h
大致了解了malloc函数,现在来看代码:
#include "stdafx.h"
#include <malloc.h> //这里使用了malloc.h
struct S1 {
int a;
int b;
int c;
};
void function() {
S1* s1=(S1*) malloc(sizeof(S1)); //申请一块空间大小正好为S1大小的内存
s1->a=610;
s1->b=666;
s1->c=52;
printf("%d\n",s1->a);
printf("%d\n",s1->b);
printf("%d\n",s1->c);
}
int main(int argc, char* argv[])
{
function();
return 0;
}
运行结果

可以看到结构体的成员能够正常地被改写和访问
反汇编代码
15: S1* s1=(S1*) malloc(sizeof(S1));
0040D778 push 0Ch
0040D77A call malloc (00401150)
0040D77F add esp,4
0040D782 mov dword ptr [ebp-4],eax
16: s1->a=610;
0040D785 mov eax,dword ptr [ebp-4]
0040D788 mov dword ptr [eax],262h
17: s1->b=666;
0040D78E mov ecx,dword ptr [ebp-4]
0040D791 mov dword ptr [ecx+4],29Ah
18: s1->c=52;
0040D798 mov edx,dword ptr [ebp-4]
0040D79B mov dword ptr [edx+8],34h
反汇编分析
1.先看这个malloc函数
15: S1* s1=(S1*) malloc(sizeof(S1));
0040D778 push 0Ch
0040D77A call malloc (00401150)
0040D77F add esp,4
0040D782 mov dword ptr [ebp-4],eax
压入了参数0C,对应十进制为12,也就是S1的大小
调用malloc函数
堆栈外平衡
将返回值eax赋值给S1
看看返回值eax的内容:

可以看到eax就对应了结构体中的成员
eax=结构体成员首地址,里面的结构体成员连续存储
2.赋值,将610对应十六进制262赋值给[eax],对应前面的003807B8
16: s1->a=610;
0040D785 mov eax,dword ptr [ebp-4]
0040D788 mov dword ptr [eax],262h
执行后:

3.赋值,将666对应十六进制29A赋值给[ecx+4],对应前面的003807BC
17: s1->b=666;
0040D78E mov ecx,dword ptr [ebp-4]
0040D791 mov dword ptr [ecx+4],29Ah
执行后:

4.赋值,将52对应十六进制34赋值给[edx+4],对应前面的003807C0
18: s1->c=52;
0040D798 mov edx,dword ptr [ebp-4]
0040D79B mov dword ptr [edx+8],34h
执行后:

小总结
结构体指针和普通的指针实际上并没有什么不同
在对结构体成员进行操作时,需要先对其进行初始化(为每个结构体成员分配内存地址)
结构体指针并不直接存储结构体成员,而是存储了指向结构体成员的地址,该地址里存放着所有结构体成员
数组指针
前面学了指针数组,现在又来个数组指针,中间用结构体指针作了过渡,避免混淆
什么是数组指针
所谓数组指针,就是指向数组的指针(本质是指针)
既然是指针自然满足先前指针的一切特征:指针的赋值、指针的数据宽度、指针的加减、指针类型相减、指针之间比较
这里就不再赘述先前的内容,有需要可回顾:逆向基础笔记二十 汇编 指针(一)
数组指针的声明
int (*px)[2];
声明如上,数组指针变量为px,类型为:int(*)[2];该数组指针指向的数组为int[2]
数组指针和指向数组的指针区别
代码
#include "stdafx.h"
void function(){
int arr[6]={1,2,3,4,5,6};
//声明一个数组指针,该指针指向数组为:int[2]
int (*px)[2];
//给数组指针赋值,使该数组指针指向arr数组的首地址
px=(int (*)[2]) &arr[0];
//用一个临时变量parr2 存储数组指针
int (*parr2)[2]=px;
//*px为数组的首地址,也就是arr,这里就相当于int* arr2=arr;此时的arr2就是指向数组的指针
int* arr2=*px;
//初始化变量,准备循环
int i;
//循环遍历数组
for(i=0;i<6;i++){
printf("%x\t%d\n",arr2+i,arr2[i]);
}
printf("\n");
int a=(int) (parr2+1);
int b=(int) (arr2+1);
printf("%x\t%x\n",a,b);
}
int main(int argc, char* argv[])
{
function();
return 0;
}
运行结果

首先可以看到数组的正常遍历
然后分别输出了parr21和arr21的结果,注意这里的结果不同
反汇编代码
8: int arr[6]={1,2,3,4,5,6};
00401038 mov dword ptr [ebp-18h],1
0040103F mov dword ptr [ebp-14h],2
00401046 mov dword ptr [ebp-10h],3
0040104D mov dword ptr [ebp-0Ch],4
00401054 mov dword ptr [ebp-8],5
0040105B mov dword ptr [ebp-4],6
9:
10: int (*px)[2];
11:
12: px=(int (*)[2]) &arr[0];
00401062 lea eax,[ebp-18h]
00401065 mov dword ptr [ebp-1Ch],eax
13:
14: int (*parr2)[2]=px;
00401068 mov ecx,dword ptr [ebp-1Ch]
0040106B mov dword ptr [ebp-20h],ecx
15: int* arr2=*px;
0040106E mov edx,dword ptr [ebp-1Ch]
00401071 mov dword ptr [ebp-24h],edx
16:
17: int i;
18:
19: for(i=0;i<6;i++){
00401074 mov dword ptr [ebp-28h],0
0040107B jmp function+66h (00401086)
0040107D mov eax,dword ptr [ebp-28h]
00401080 add eax,1
00401083 mov dword ptr [ebp-28h],eax
00401086 cmp dword ptr [ebp-28h],6
0040108A jge function+8Fh (004010af)
20: printf("%x\t%d\n",arr2+i,arr2[i]);
0040108C mov ecx,dword ptr [ebp-28h]
0040108F mov edx,dword ptr [ebp-24h]
00401092 mov eax,dword ptr [edx+ecx*4]
00401095 push eax
00401096 mov ecx,dword ptr [ebp-28h]
00401099 mov edx,dword ptr [ebp-24h]
0040109C lea eax,[edx+ecx*4]
0040109F push eax
004010A0 push offset string "%x\t%d\n" (00422024)
004010A5 call printf (00401160)
004010AA add esp,0Ch
21: }
004010AD jmp function+5Dh (0040107d)
22: printf("\n");
004010AF push offset string "\n" (00422020)
004010B4 call printf (00401160)
004010B9 add esp,4
23:
24: int a=(int) (parr2+1);
004010BC mov ecx,dword ptr [ebp-20h]
004010BF add ecx,8
004010C2 mov dword ptr [ebp-2Ch],ecx
25: int b=(int) (arr2+1);
004010C5 mov edx,dword ptr [ebp-24h]
004010C8 add edx,4
004010CB mov dword ptr [ebp-30h],edx
26: printf("%x\t%x\n",a,b);
004010CE mov eax,dword ptr [ebp-30h]
004010D1 push eax
004010D2 mov ecx,dword ptr [ebp-2Ch]
004010D5 push ecx
004010D6 push offset string "%x\t%x\n" (00422fa4)
004010DB call printf (00401160)
004010E0 add esp,0Ch
反汇编分析
1.数组的初始化
8: int arr[6]={1,2,3,4,5,6};
00401038 mov dword ptr [ebp-18h],1
0040103F mov dword ptr [ebp-14h],2
00401046 mov dword ptr [ebp-10h],3
0040104D mov dword ptr [ebp-0Ch],4
00401054 mov dword ptr [ebp-8],5
0040105B mov dword ptr [ebp-4],6
数组初始化后对应地址和内容为:

2.数组指针的赋值
12: px=(int (*)[2]) &arr[0];
00401062 lea eax,[ebp-18h]
00401065 mov dword ptr [ebp-1Ch],eax
直接将arr数组的首地址也就是0012FF14传给了eax
然后再将eax赋值给数值指针px
数值指针赋值后:

可以看到数值指针里存储的内容为0012FF14即arr的地址
3.将数值指针px赋值给另一个数组指针parr2
14: int (*parr2)[2]=px;
00401068 mov ecx,dword ptr [ebp-1Ch]
0040106B mov dword ptr [ebp-20h],ecx
赋值后:

可以看到此时parr2里存储的内容px里存储的内容0012FF14=arr首地址
4.将数组的首地址赋值给arr2,即arr2=arr
14: int* arr2=*px;
0040D82E mov edx,dword ptr [ebp-1Ch]
0040D831 mov dword ptr [ebp-24h],edx
这里要注意到这里和前面一样都是赋值了[ebp-1Ch]
也就是明明赋值的是*px,但是赋值却和px一样
也可以得出结论*px=px,那么*__为什么__px和px是一样的**?
首先要明确无论是px还是*px 它们都是指针,一个为数组指针,而另一个则为普通指针
它们所指向的地址相同,都指向了arr的首地址0012FF14
区别px和*px的本质就在于其数据类型是两种不同的指针结构
指针在相加减时,加减的基本单位是指针去掉一个*后的数据宽度(有疑惑可移步逆向基础笔记二十 汇编 指针(一))
px的数据类型为:int (*)[2],去掉一个*后变为int [2],数据宽度为int的数据宽度×数组的成员数4*28
*px的数据类型为:int*,去掉一个*后变为int,数据宽度=4
赋值后:

5.循环遍历数组
就是普通的指针循环数组,在先前的笔记中已经有详细介绍,这里不再赘述
6.第二种循环
24: int a=(int) (parr2+1);
004010BC mov ecx,dword ptr [ebp-20h]
004010BF add ecx,8
004010C2 mov dword ptr [ebp-2Ch],ecx
25: int b=(int) (arr2+1);
004010C5 mov edx,dword ptr [ebp-24h]
004010C8 add edx,4
004010CB mov dword ptr [ebp-30h],edx
通过前面可以得知*pxpx,parr2arr2,所以这里的[ebp-20h]=[ebp-24h]的:


这里的不同之处就在于一个add了8,另一个add了4,和先前所分析的指针加减的单位相符合,于是产生了不同的结果
小总结
在一个数组指针前加上*获得的就是指向数组的指针,如上例中的int __arr2=__px;
数值指针和指向数组的指针中存储的内容都是数组的首地址,如上例中的px=*pxarr0012FF14
数组指针和指向数组的指针的主要区别在进行运算时的单位不同,前者为数据类型宽度×数组成员数,后者为数据类型宽度
数组指针的应用
可以利用数组指针进行加减时的单位不同来遍历数组的固定间隔的成员
下例为从数组的第二个成员开始,取出间隔为3的数组成员
代码
#include "stdafx.h"
void function(){
int arr[15]={1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
int (*px)[3];
//从数组的第二个成员开始
px=(int (*)[3]) &arr[1];
int i=0;
for(i=0;i<15/3;i++){
printf("%x\t%d\n",px+i,**(px+i));
//注意这里取了两次*,第一次获得的是指向数组成员的指针,第二个获得的才是数组成员
}
}
int main(int argc, char* argv[])
{
function();
return 0;
}
运行结果

总结
| 指针数组 | 结构体指针 | 数组指针 | |
|---|---|---|---|
| 本质 | 数组 | 指针 | 指针 |
| 描述 | 数组成员类型为指针的数组 | 指向结构体的指针 | 指向数组的指针 |
| 声明 | type* arr[size] | struct *structName | int (*name)[size] |
| 补充 | 指针数组的赋值也和先前对指针的赋值没有什么区别 | 在对结构体成员进行操作时,需要先对其进行初始化;结构体指针并不直接存储结构体成员,而是存储了指向结构体成员的地址,该地址里存放着所有结构体成员 | 数组指针和指向数组的指针的主要区别在进行运算时的单位不同,前者为数据类型宽度×数组成员数,后者为数据类型宽度 |
指针作为参数
代码
#include "stdafx.h"
void function(int num){
num=52;
}
void function2(int* num){
*num=52;
}
int main(int argc, char* argv[])
{
int num=610;
function(num);
printf("%d\n",num);
function2(&num);
printf("%d\n",num);
return 0;
}
运行结果

可以看到没有通过指针进行传参的数据在function内部修改后并没有影响到原本的数值
而通过指针进行传参的数据在function2内部修改后就影响了原本的数值
反汇编代码
函数外部
num初始化
20: int num=610;
0040D978 mov dword ptr [ebp-4],262h
function和输出
21: function(num);
0040D97F mov eax,dword ptr [ebp-4]
0040D982 push eax
0040D983 call @ILT+15(function) (00401014)
0040D988 add esp,4
22: printf("%d\n",num);
0040D98B mov ecx,dword ptr [ebp-4]
0040D98E push ecx
0040D98F push offset string "%d\n" (0042201c)
0040D994 call printf (00401130)
0040D999 add esp,8
function2和输出
23: function2(&num);
0040D99C lea edx,[ebp-4]
0040D99F push edx
0040D9A0 call @ILT+20(function2) (00401019)
0040D9A5 add esp,4
24: printf("%d\n",num);
0040D9A8 mov eax,dword ptr [ebp-4]
0040D9AB push eax
0040D9AC push offset string "%d\n" (0042201c)
0040D9B1 call printf (00401130)
0040D9B6 add esp,8
函数内部
function
码8: void function(int num){
0040D870 push ebp
0040D871 mov ebp,esp
0040D873 sub esp,40h
0040D876 push ebx
0040D877 push esi
0040D878 push edi
0040D879 lea edi,[ebp-40h]
0040D87C mov ecx,10h
0040D881 mov eax,0CCCCCCCCh
0040D886 rep stos dword ptr [edi]
9: num=52;
0040D888 mov dword ptr [ebp+8],34h
10: }
0040D88F pop edi
0040D890 pop esi
0040D891 pop ebx
0040D892 mov esp,ebp
0040D894 pop ebp
0040D895 ret
function2
12: void function2(int* num){
0040D7E0 push ebp
0040D7E1 mov ebp,esp
0040D7E3 sub esp,40h
0040D7E6 push ebx
0040D7E7 push esi
0040D7E8 push edi
0040D7E9 lea edi,[ebp-40h]
0040D7EC mov ecx,10h
0040D7F1 mov eax,0CCCCCCCCh
0040D7F6 rep stos dword ptr [edi]
13: *num=52;
0040D7F8 mov eax,dword ptr [ebp+8]
0040D7FB mov dword ptr [eax],34h
14: }
0040D801 pop edi
0040D802 pop esi
0040D803 pop ebx
0040D804 mov esp,ebp
0040D806 pop ebp
0040D807 ret
反汇编分析
function分析
function外部
21: function(num);
0040D97F mov eax,dword ptr [ebp-4]
0040D982 push eax
0040D983 call @ILT+15(function) (00401014)
0040D988 add esp,4
将[ebp-4]也就是num赋值给eax,然后将eax作为参数压入到堆栈中
用内存窗口观察此时ebp-4的情况:

此时的ebp-4对应地址为0012FF7C,也就是前面的num
function内部
截取出赋值的关键代码
9: num=52;
0040D888 mov dword ptr [ebp+8],34h
注意到此时赋值为[ebp+8](参数),用内存窗口观察其地址并比较和先前的ebp-4的地址

可以看到此时要赋值的地址为0012FF2C,和先前的num地址0012FF7C不同,但值是相同的:0x262=610
于是就造成了就算修改了这里的地址中的值也不会影响原本的值


小总结
在函数内部获得的参数的值虽然和传入的参数的值一致,但实际的内存地址并不相同,于是在函数内部修改值并不会影响到原本的值
function2分析
function2外部
23: function2(&num);
0040D99C lea edx,[ebp-4]
0040D99F push edx
0040D9A0 call @ILT+20(function2) (00401019)
0040D9A5 add esp,4
将ebp-4的地址传给edx,然后将edx作为参数压入到堆栈中
观察此时ebp-4的情况:

此时ebp-4就是num的地址=0012FF7C
function2内部
截取出关键的赋值代码
13: *num=52;
0040D7F8 mov eax,dword ptr [ebp+8]
0040D7FB mov dword ptr [eax],34h
首先看[ebp+8]:

会发现此时的ebp+8所存的内容就是num的地址:0012FF7C
这里是将[ebp+8](参数)赋值给eax,赋值后的eax是等于外面的num的

然后再将0x34赋值到参数所存储的内容里

小总结
使用指针作为参数可以实现在函数内部修改后外部的值也可以同步被修改
指针作为参数总结
通过比较指针传参和非指针传参得到了以下结论:
无论是指针传参还是非指针传参,将参数传入函数内部后,只保证了参数的内容一致,但地址不同
之所以指针传参能够实现修改函数外部值的效果,是因为传入的参数的值就是外部值的地址,传入的是个地址,通过修改同个地址里的值而达到了影响外部值的效果
| 传入参数 | 差异 | |
|---|---|---|
| 非指针传参 | 参数是值 | 修改值只修改了函数内部的值,不影响函数外部 |
| 指针传参 | 参数是址 | 修改值是通过地址来修改的,因而能够影响函数外部 |
使用指针作为参数十分常见,因为通过这种方式就可以不用返回值,直接将要返回的数据保存在指针里即可
常见的例子:
NTSTATUS
ZwOpenProcess (
__out PHANDLE ProcessHandle, //进程句柄,PHANDLE 句柄指针
__in ACCESS_MASK DesiredAccess, //想要的权限
__in POBJECT_ATTRIBUTES ObjectAttributes, //指向ObjectAttributes的指针
__in_opt PCLIENT_ID ClientId //指向ClientId的指针
)
这是一个内核函数,用来打开进程,具体使用细节这里不做重点
这里的ProcessHandle是一个指针,它作为参数就是用来存储打开进程后获得的进程句柄
很多内核函数都采用了这种形式——指针作为参数来存储要返回的数据
指针作为返回值
先前在数组里跳过了将数组作为返回值来传递,因为那时还没有讲到指针,接下来看看指针作为返回值的情形
错误的返回值
代码
#include "stdafx.h"
#include <malloc.h>
int* function(){
int arr[5]={1,2,3,4,5};
return arr;
}
int main(int argc, char* argv[])
{
int*arr;
arr=function();
int i;
for(i=0;i<5;i++){
printf("%d\n",arr[i]);
}
return 0;
}
运行结果

可以看到运行结果并不是我们所期望的
结果分析
为什么输出的结果和预期结果不一致?
首先要知道,指针指向的地址是在函数内部的arr(临时变量存储在堆栈中),当函数内部的代码执行完毕后,这部分数据就变成了垃圾数据,可以被随意覆盖,而printf函数就是输出的时候讲这部分垃圾数据覆盖了,导致了输出结果的不正确
观察内存窗口,查看刚赋值完后的结果:
可以看到arr指向了一个地址,这个地址也是个堆栈地址:0012FF10

看看这个地址里存储的数据:

可以看到此时的数据是没有问题的
但是当printf执行后会看到:

原本的数据被覆盖了
小总结
指针作为返回值时,如果指针所指向的地址为一个堆栈地址,即指针所指向的为函数内部的临时变量时,其数据可能会在执行其它代码的时候被覆盖
如果想要让指针作为返回值可以正常使用,有以下两种方法:
让指针所指向的地址为一个全局变量,这样其数据就不会被覆盖
指针返回后,将其返回的数据拷贝到另一个数组里(拷贝时不能使用会影响堆栈的语句)
第一种方法比较简单,也容易理解,下面以第二种方法为例
正确的返回值
#include "stdafx.h"
#include <malloc.h>
int* function() {
int arr[5]={1,2,3,4,5};
return arr;
}
int main(int argc, char* argv[])
{
int*arr;
int*arr2=(int*)malloc(sizeof(int)*5); //注意是在函数调用前申请的空间,如果在函数执行后再申请会影响数据
arr=function();
int i;
for(i=0;i<5;i++){
*(arr2+i)=*(arr+i); //将返回的指针里的数据拷贝到arr2
}
for(i=0;i<5;i++){
printf("%d\n",arr2[i]);
}
return 0;
}
运行结果

可以看到能够正确地输出结果
总结
可以看到将指针作为返回值并没有想象中那么方便
一般来说想要让指针作为返回值一般是采用两种办法:
指针作为参数,然后将要返回的数据存储到参数里
先声明一个全局变量,然后指针指向的为全局变量
