引言
指针被称为C语言的灵魂,可见指针在C语言中的地位,指针在逆向中也是极其重要的,在查找数据时也是一种非常重要的手段。许多初学者对于一级指针也是学习的一知半解,在这里带大家从反汇编角度了解指针。在这里我们使用的编译器是visual c++ 6.0。
这里所有的知识点都来自于滴水逆向三期视频第36集(指针7),有需要的小伙伴可以自己去学习,当然也可以指出此篇文章的不足。
数据宽度
我们先来看看C语言中不同数据类型是如何存储的:
C语言代码:
char a=100;
short b=100;
int c=100;
我们在正向学习中知道一个char类型占据一个字节,一个short类型占据两个字节,一个int类型占据四个字节;
接下来我们在visual C++ 中观察反汇编,看看他们到底占据几个字节
我们知道在调用一个空函数的时候visual c++为我们默认分配0x40个字节。
反汇编代码:
push ebp
mov ebp,esp
sub esp,4Ch
push ebx
push esi
push edi
lea edi,[ebp-4Ch]
mov ecx,13h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
我们看到编译器为我们分配了0x4c个字节。所以不管是char,short,int类型,编译器为我们分配内存时都是分配4个字节,并不会节省空间,只是在使用时使用只使用1个字节,2个字节或者是4个字节。
在学习多级指针之前带大家看一看一级指针:
一级指针
C语言代码:
int a=10;
int* p;
p=&a;
我们来到反汇编观察:
反汇编代码:
mov dword ptr [ebp-4],0Ah
lea eax,[ebp-4]
mov dword ptr [ebp-8],eax
我们可以观察到一级指针是把一个变量(可以是C语言的各种数据类型,也可以是一个数组的首地址,甚至通过强制转换类型我们可以存储一个整数)的地址存储到一个指针变量中。在计算机中其实并不知道他存储的是什么类型,计算机内存只负责存储,只是C语言规定我们要存储地址。
1. 数据宽度
我们通过C语言sizeof函数来看看指针类型的数据宽度。
C语言代码:
int* a;
printf("%d",sizeof(a));
通过程序输出我们能够知道指针类型占据4个字节空间。
2. 指针++,–,加或减一个整数运算
在这里我们通过数组可以更好地观察指针的运算。
C语言代码:
int number[6]={1,2,3,4,5,6};
int* p=number;
我们来观察反汇编代码:
反汇编代码:
mov dword ptr [ebp-18h],1
mov dword ptr [ebp-14h],2
mov dword ptr [ebp-10h],3
mov dword ptr [ebp-0Ch],4
mov dword ptr [ebp-8],5
mov dword ptr [ebp-4],6
在内存中数组的存储形式是这样的:
数据 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
地址 | ebp-18h | ebp-14h | ebp-10h | ebp-0ch | ebp-8h | ebp-4h |
这是一个int类型的数组,所以每个地址之间相差4个字节。
接下来我们来看看对指针的++操作:
C语言代码:
printf("%x\n",p);
printf("%d\n",*p);
printf("%d\n",*(p+1));
我们可以观察到程序输出:
19ff1c //该行输出的是该数组的首地址,这个地址在每个计算机可能不同
1 //该行输出指针p所指的地址内的数据
2 //我们对指针进行了+1的操作,p+1指向了数组的下一个地址
在这里实际上p+1看似指向了数组的下一个地址,其实这里是在数组的首地址上加了4才指向下一个数据,我们来观察反汇编:
反汇编代码:
mov ecx,dword ptr [ebp-1Ch]
mov edx,dword ptr [ecx+4]
这里首先取出了在p里存储的地址,存储到寄存器ecx中,然后该地址+4,则指向下一个数据。那么在这里我们该怎么当指针加1时,寄存器内地址到底加了多少呢,按照老师讲的方法:我们去掉一个*,* p为int * 类型,去掉一个 * 就是int类型,那么地址就应该加4。我们想想实质是什么呢,在p指向的地址内存储的数据为int类型,那么该地址+4才指向下一个数据,所以应该在该地址的基础上+4指向下一个数据。
同理,如果是char类型或者是short类型的数组,那么当指向该数组的指针+1时,该指针指向的地址+1或者+2才指向下一个数据。
由此我们可以得到当指针+n(一个整数)时,该指针指向的地址应该+(n*该指针指向的地址存储的数据类型所占据的宽度)。
3. 同类型指针做加减法
同类型的指针是可以做加减运算的,我们来看看:
C语言代码:
int* p1=(int*)100;
int* p2=(int*)200;//这里我们通过强制转换类型将100和200分别存入指针p1和p2中,站在编译器的角度,100和200都是地址
printf("%d",p1-p2);
在程序输出窗口我们可以看到输出:
25
那么这个25到底是怎么计算出来的呢,根据老师讲的,这里输出的实际上是100/4。
p1和p2都是int类型,那么在p1和p2所指的地址内存储的数据应该是int类型,就是占据4个字节,指针p1与p2相减,实际上是输出该段地址内一共能存储多少个同类型(指针为int,则为int类型,如果指针为char*类型,则为char类型)的数据。
在这里我们载介绍指针加(减)一个整数的另一种写法:
//C语言代码
int number[6]={1,2,3,4,5,6};
int* p=number;
printf("%d\n",*p);
printf("%d\n",*(p+1));
printf("%d\n",p[1]);
我们可以观察到程序输出框里输出:
1
2
2
我们可以看到*(p+1)和p[1]输出完全相同的值,我们继续查看反汇编:
反汇编代码:
mov eax,dword ptr [ebp-1Ch]
mov ecx,dword ptr [eax+4]
push ecx
p[1]的反汇编代码:
mov edx,dword ptr [ebp-1Ch]
mov eax,dword ptr [edx+4]
push eax
我们能够清楚的看到 * (p+1)和p[1]的反汇编代码完全相同,则p[1]可以代替*(p+1)。
二级指针
为了大家更好地理解,我们二级指针使用short类型的数组。
//C语言代码
short number[5]={1,2,3,4,5};
short* p1=number;
short** p2=&p1;
我们来到反汇编观察:
mov word ptr [ebp-0Ch],offset main+1Ch (0040d41c)
mov word ptr [ebp-0Ah],offset main+22h (0040d422)
mov word ptr [ebp-8],offset main+28h (0040d428)
mov word ptr [ebp-6],offset main+2Eh (0040d42e)
mov word ptr [ebp-4],offset main+34h (0040d434)
lea eax,[ebp-0Ch]
mov dword ptr [ebp-10h],eax
lea ecx,[ebp-10h]
mov dword ptr [ebp-14h],ecx
1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|
ebp-1Ch | ebp-0Ah | ebp-8 | ebp-6 | ebp-4 |
由于是short类型数组,我们可以观察到每个数据之间地址相差2。
通过反汇编我们可以观察到:
指针p1里存的是该数组的首地址,p2里存的是p1的地址,这里p2我们成为二级指针。
1. 数据宽度
通过C语言的sizeof函数我们来看看二级指针的数据宽度:
//C语言代码:
short number[5]={1,2,3,4,5};
short* p1=number;
short** p2=&p1;
printf("%d",sizeof(p2));
在程序输出窗口我们可以看到输出:
4
不管是几级指针,它里面存储的都是地址,所以指针本身都占据4个字节。
2. 二级指针++,–,加或减一个整数运算
C语言代码:
short** p=(short**)100;
printf("%d\n",p);
printf("%d\n",p+1);
在这里我们通过强制类型转换把100转换成short**类型存储到二级指针p中。
在程序输出窗口我们可以看到输出:
//程序输出窗口
100
104
我们再来到反汇编看一看:
mov ecx,dword ptr [ebp-4]
add ecx,4
push ecx
在这里我们看到二级指针+1是在该二级指针指向的地址基础上再加4。看到这里大家可能还是不太明白,接下来带大家到反汇编一探究竟:
3. 二级指针反汇编
1. *(*p2)反汇编:
//C语言代码
short** p2;
printf("%d",*(*p2));
在使用指针时,在变量前面加一个 * 就相当于数据本身减掉一个 * ,比如这里的*(*p2):
我们定义的p2为short**类型, * p2就是short * 类型,在 *short前再加一个 *,就是 *(*p2),为short类型。我们来观察反汇编:
mov eax,dword ptr [ebp-4]
mov ecx,dword ptr [eax]
movsx edx,word ptr [ecx]
我们来看看在反汇编里对 *( *p2 )取值的过程:
1.取出指针变量p2里存的地址,放入eax
2.把eax里存的数据当作地址,取出该地址里存的数据放入ecx
3.把ecx里存的数据当地址,取出该地址内存放的数据放入edx
接下来我们可以在自己编译器里看到是push edx(将edx压入栈),接下来就是调用printf函数的过程了,在这里我们不再做介绍。
2. *( *(p2+1))反汇编:
C语言代码:
short** p2;
printf("%d",*(*(p2+1)));
我们直接从反汇编角度来分析对*(*(p2+1))取值的过程:
//反汇编代码:
mov eax,dword ptr [ebp-4]
mov ecx,dword ptr [eax+4]
movsx edx,word ptr [ecx]
大家可以从下面贴出来的图片中加强理解,蓝色为对二级指针未作任何操作,橙色为*( *(p2+1))
1.取出指针变量p2中存放的数据放入eax
2.将取出的数据+4,将得到的值看作地址,将该地址中的数据放入ecx(二级指针本来指向第一个一级指针,因为二级指针内存放的还是地址,所以对二级指针+1实质上还是在内存上+4(4 *1)个字节,指向第二个一级指针)
3.将ecx中的数据当作地址,取出该地址中存放的数据,放入edx(二级指针内存放的数据+4(4 * 1),指向第二个一级指针,第二个一级指针指向第二个数据)
3. *( *(p2+1)+1)反汇编
//C语言代码:
short** p2;
printf("%d",*(*(p2+1)+1));
我们还是直接来到反汇编窗口来观察:
//反汇编代码:
mov eax,dword ptr [ebp-4]
mov ecx,dword ptr [eax+4]
movsx edx,word ptr [ecx+2]
大家可以从下面这张图中来理解:
该图中,蓝色为未作任何操作时二级指针示意图,较细箭头为给p2+1时的指向,加粗箭头为最终指向
我们来逐步分析对*( *(p2+1)+1)取值的过程:
1.首先取出指针变量p2中存取的数据,放入eax
2.将eax内存放的数据当作地址,在该地址基础上+4,取出该地址内存放的数据,放入ecx(第一次取出的数据,是一个地址,指向第一个一级指针,如图所示,将二级指针+1,实际上就是要指向本来要指向的一级指针(一级指针1)的下一个(一级指针2),由于一级指针占据4个字节,所以要在二级指针内存放的数据+4(4 * 1))
3.将ecx中的值+2,再当作一个地址,取出该地址中存放的数据,放入edx(由于一级指针2本来指向的是数据2(short类型,占据2字节),所以要在一级指针内存放的地址上+2(2 * 1))
4. *( *(p2+2)+3)
这里其实是前三个的运用,为了加深理解,我们来定义char**类型的二级指针。
//C语言代码
char** p2;
printf("%d",*(*(p2+2)+3));
//反汇编代码
mov eax,dword ptr [ebp-4]
mov ecx,dword ptr [eax+8]
movsx edx,byte ptr [ecx+3]
我们继续以画内存图的方式来为大家讲解:
该图中蓝色箭头为二级指针未作任何操作时的指向,较细橙色箭头为对二级指针操作后的指向,较粗箭头为对一级指针操作后的指向。
我们来逐步分析对*( *(p2+2)+3)取值的过程
1.取出二级指针变量p2中的数据,放入eax中
2.将eax中的值+8,将得到的值看作地址,取出该地址中存放的数据,放入ecx(第一次取出的地址指向一级指针,所以对二级指针+2的操作实际上是让改二级指针指向原本指向的一级地址再向后便宜两个一级指针,所以就是二级指针内存放的数据+8(4 * 2))
3.将ecx中的值+3,将得到的值看作地址,取出该地址内存取的数据,放入edx(由于ecx内存取的是一个地址(可以看作一级指针),指向char类型的数据,所以对一级指针+3的操作实际上是对一级指针内存放的数据+3(3 * 1))。
三级指针
我们先来了解一下三级指针:
//C语言代码:
char a=1;
char* p1=&a;
char** p2=&p1;
char*** p3=&p2;
这里我们定义一个char类型的变量a,定义一个char*类型的变量(一级指针)p1存放a的地址,定义一个char * *类型的变量(二级指针)存放p1的地址,再定义一个char * * *类型的变量存(三级指针)放p2的地址。
//反汇编代码:
mov byte ptr [ebp-4],1
lea eax,[ebp-4]
mov dword ptr [ebp-8],eax
lea ecx,[ebp-8]
mov dword ptr [ebp-0Ch],ecx
lea edx,[ebp-0Ch]
mov dword ptr [ebp-10h],edx
根据反汇编分析:
将1存进a[ebp-4]中
将a[ebp-4]的地址存入eax,将eax存入p1[ebp-8]中————>一级指针
将p1[ebp-8]的地址存入ecx,将ecx存入p2[ebp-0ch]—————>二级指针
将p2[ebp-0ch]的地址存入edx,将edx存入p3[ebp-10h]——————>三级指针
1. 数据宽度
我们通过C语言sizeof函数可以得到三级指针所占据的数据宽度:
char*** a;
printf("%d",sizeof(a);
我们可以看到在程序输出框里输出:
4
所以三级指针占据4个字节存储空间,在C语言编译器中,我们有数据类型之分,但是实际在电脑内存中,内存条根本没有数据类型之分,它只负责存储数据。
2. 三级指针++,–,加或减一个整数运算
//C语言代码:
char*** a=(char***)100;
printf("%d",a++);
我们能够观察到在程序输出窗口输出:
104
char ***内存储的是一个char **类型变量的地址,对char ***类型+1,实际上是要char ***指向原本指向数据的下一个数据,而char **类型占据4个字节,所以要在原本char ***基础上+4(4 * 1)。
反汇编代码:
//反汇编代码:
mov dword ptr [ebp-4],64h
mov eax,dword ptr [ebp-4]
add eax,4
我们来分析整个过程:
定义了一个char ***类型的变量,通过强制转换类型将100转换为char ***类型。然后对该变量里存储的值+4(在内存并不认为它是地址,只是编译器和我们认为它是地址,内存只负责存储,在内存眼里所有数据都一样)。
3. 三级指针反汇编
1. *(*p3)反汇编
C语言代码:
char*** p3;
printf("%d",*(*p3));
我们知道在带 *的数据类型前面加 *得到的数据类型就是原本的类型去掉一个 *,所以我们这里取到了一级指针,并没有取到一级指针所指的空间里所存储的数据。
反汇编代码:
mov eax,dword ptr [ebp-4]
mov ecx,dword ptr [eax]
mov edx,dword ptr [ecx]
我们来逐步分析对*(*p3)取值的过程:
1.取出变量pa[ebp-4]内存储的数据,放入eax
2.将eax中的数据当作地址,取出该地址内存储的数据,放入ecx
3.将ecx中的数据当作地址,取出该地址中存储的数据,放入edx
2. *( *(*p3))反汇编
C语言代码:
char*** p3;
printf("%d",*(*(*p3)));
反汇编代码:
mov eax,dword ptr [ebp-4]
mov ecx,dword ptr [eax]
mov edx,dword ptr [ecx]
movsx eax,byte ptr [edx]
我们来逐步分析对*( *(*p3))取值的过程:
1.取出p3[ebp-4]中的数据,放入寄存器eax
2.将寄存器eax中存储的数据当作地址,取出该地址中存储的数据,放入寄存器ecx
3.将寄存器ecx中存储的数据当作地址,取出该地址中存储的数据,放入寄存器edx
4.将寄存器edx中存储的数据当作地址,取出该地址中存储的数据,放入寄存器eax
3. *( *( *( *p3+1)+2)+3)反汇编
C语言代码:
char*** p3;
printf("%d",*(*(*(p3+1)+2)+3));
反汇编代码:
mov eax,dword ptr [ebp-4]
mov ecx,dword ptr [eax+4]
mov edx,dword ptr [ecx+8]
movsx eax,byte ptr [edx+3]
我们来逐步分析对 *( *( *(p3+1)+2)+3)取值的过程:
由于此过程有点繁琐,我才用画内存图的方法带大家了解:
图中,蓝色箭头为未作任何操作时,三级指针的分别指向,较细橙色箭头为+1操作后三级指针指向的变化,较粗橙色箭头为+2操作后二级指针指向变化,绿色箭头为+3操作后一级指针指向的不安话
1.取出p3[ebp-4]中的数据,放入寄存器eax
2.将eax中存放的数据+4后当作地址,取出该地址中存放的数据,放入寄存器ecx(因为给变量p3本身+1,就是要三级指针指向原本指向的下一个数据,而三级指针指向的是二级指针类型,占据4个字节,所以给三级指针+1的操作事实上是给三级指针内存放的数据+4(4 * 1)
3.将寄存器ecx中的值+8后当作地址,取出该地址中存放的数据,放入寄存器edx(给二级指针+2的操作,就是要二级指针指向原本指向数据向再后偏移2个数据,所以事实上就是给二级指针内存放的数据+8(4 * 2)
4.将寄存器edx中存放的值+3后当作地址,取出该地址中存放的数据,放入寄存器eax(给一级指针+3的操作,就是要让指针指向原本指向数据再向后偏移三个数据,而此一级指针指向的数据为char类型,所以给该一级指针+3的操作,事实上是给该一级指针内存放的数据+3(1 * 3)
4. p3[1][2][3]反汇编
C语言代码:
char*** p3;
printf("%d",p3[1][2][3]);
反汇编代码:
mov eax,dword ptr [ebp-4]
mov ecx,dword ptr [eax+4]
mov edx,dword ptr [ecx+8]
movsx eax,byte ptr [edx+3]
我们来与*( *( *(p3+1)+2)+3的反汇编代码做个比较,发现它俩的反汇编代码竟惊人地相同!
在讲二级指针时我们已经讲过这个知识点,我们在这里就不再啰嗦了。
直接给出结论:
*(*(*(*(*(p+m) +n) +i) +j) +k)与p[m][n][i][j][k]可以互换
OK今天我们就学习到这里,当然希望大家可以指出此篇笔记的不足或者错误之处,当然希望我们能够共同学习,共同进步!!!