Jun 10, 2024
安全

逆向基础 - 指针

终于来到C语言中极其重要的部分——指针,由于指针的内容相对较多,于是将其拆分为好几个笔记。

逆向基础 - 指针
Posted by
SYSTEM· 100 次阅读
专栏系列逆向基础

什么是指针

一般关于指针的解释都离不开地址。这里先暂且忘记这个概念

指针其实也是一种数据类型,和先前学习的int float等数据类型没有实质上的区别,只不过这个数据类型是在先前学习的所有数据类型后面加上若干个*号,如char *,int *等等,这种数据类型被称为指针


  • 任意类型后面都可以加上*号,使其成为新的指针数据类型

  • *可以是任意多个

指针的声明

指针的声明其实在前面介绍什么是指针的时候就已经讲到了,例子如下:

C
struct S1 {
        int a;
};

void function() {                
        char* a;
        short** b;
        int*** c;
        long**** d;
        _int64***** e;
        float****** f;
        double******* g;
        S1******** s1;
}

可以看到所有的其它数据类型(包括结构体)后面加上若干个*后就是所谓的指针了

推荐的声明方式如上

但也可以这样将*放在变量前面,但不推荐,因为这样相当于将这个数据类型拆开了,不利于理解

C
struct S1 {
    int a;
};

void function() {                
    char *a;
    short **b;
    int ***c;
    long ****d;
    _int64 *****e;
    float ******f;
    double *******g;
    S1 ********s1;
}

指针的赋值

在说指针的赋值之前先看看先前普通变量的赋值

普通变量的赋值貌似是直接使用 变量=值即可,但其实是编译器简化了赋值的步骤,实际上在赋值前本应该加上要赋值类型

例子如下:

C
void function() {                
    int a;
    a=610;
    a=(int)610;
}

现在再来看指针的赋值

C
void function(){                
    char* a;
    a=(char*) 610;
    int** b;
    b=(int**) 610;
}

在要赋的值前面加上指针的类型即可,貌似和普通变量的赋值并无太大差别,此时也注意到这里的指针也和地址没有什么关联

指针的的类型转换这里暂且不提


指针的数据宽度

研究数据宽度方法

先前研究过其它基本变量的数据宽度,会发现char、short、int都是按照4字节来分配的(内存对齐),但实际使用的情况下则是按照其原本类型的数据宽度来赋值或进行其它操作的

如:

C
void function(){                
    char a;
    short b;
    int c;
    a=1;
    b=2;
    c=3;
}

其对应的反汇编代码为:

X86ASM
11:   void function(){
00401010   push        ebp
00401011   mov         ebp,esp
00401013   sub         esp,4Ch
00401016   push        ebx
00401017   push        esi
00401018   push        edi
00401019   lea         edi,[ebp-4Ch]
0040101C   mov         ecx,13h
00401021   mov         eax,0CCCCCCCCh
00401026   rep stos    dword ptr [edi]
12:       char a;
13:       short b;
14:       int c;
15:       a=1;
00401028   mov         byte ptr [ebp-4],1
16:       b=2;
0040102C   mov         word ptr [ebp-8],offset function+20h (00401030)
17:       c=3;
00401032   mov         dword ptr [ebp-0Ch],3
18:   }
00401039   pop         edi
0040103A   pop         esi
0040103B   pop         ebx
0040103C   mov         esp,ebp
0040103E   pop         ebp
0040103F   ret

可以注意到此时提升的堆栈为4Ch,而默认(空函数时)提升的堆栈为40h

X86ASM
00401013   sub         esp,4Ch

于是此时为三个变量分配的空间是:4Ch-40h0xC12=3×4,即为char short int都分配了4个字节


但在这三个变量赋值的时候展现出来的就是其原本的数据类型宽度了

X86ASM
15:       a=1;
00401028   mov         byte ptr [ebp-4],1

在char类型的a中的赋值宽度是 byte,1字节


X86ASM
16:       b=2;
0040102C   mov         word ptr [ebp-8],offset function+20h (00401030)

在short类型的b中的赋值宽度是word,2字节


X86ASM
17:       c=3;
00401032   mov         dword ptr [ebp-0Ch],3

在int类型的c中的赋值宽度是dword,4字节


研究指针数据宽度

于是如法炮制,按照前面的方法来研究指针的数据宽度

在前面的数据类型后添加*,使其成为指针类型

C
void function(){                
        char* a;
        short* b;
        int* c;
        a=(char*)  1;
        b= (short*) 2;
        c=(int*)  3;
}

其对应的反汇编代码为

X86ASM
11:   void function(){
00401010   push        ebp
00401011   mov         ebp,esp
00401013   sub         esp,4Ch
00401016   push        ebx
00401017   push        esi
00401018   push        edi
00401019   lea         edi,[ebp-4Ch]
0040101C   mov         ecx,13h
00401021   mov         eax,0CCCCCCCCh
00401026   rep stos    dword ptr [edi]
12:       char* a;
13:       short* b;
14:       int* c;
15:       a=(char*)  1;
00401028   mov         dword ptr [ebp-4],1
16:       b= (short*) 2;
0040102F   mov         dword ptr [ebp-8],2
17:       c=(int*)  3;
00401036   mov         dword ptr [ebp-0Ch],3
18:   }
0040103D   pop         edi
0040103E   pop         esi
0040103F   pop         ebx
00401040   mov         esp,ebp
00401042   pop         ebp
00401043   ret

直接观察对应的赋值语句:

X86ASM
15:       a=(char*)  1;
00401028   mov         dword ptr [ebp-4],1
16:       b= (short*) 2;
0040102F   mov         dword ptr [ebp-8],2
17:       c=(int*)  3;
00401036   mov         dword ptr [ebp-0Ch],3

可以看到,所有赋值的宽度都为dowrd,说明无论是char*、short__、int\__其数据宽度都为4字节

可以使用同样的方法研究float、double、struct等其它数据类型

并且在这里会注意到,指针类型的赋值和非指针类型的赋值在反汇编中并没有什么区别

总结

无论是什么类型,在其后面加上__(无论加几个\__都一样)后其数据宽度都变为4字节

指针的加减

例子

指针类型也支持加减的操作,但不支持乘和除(编译器决定的),来看例子:

C
#include "stdafx.h"

void function() {                
        char* a;
        short* b;
        int* c;
        a=(char*)  1;
        b= (short*) 2;
        c=(int*)  3;

        a++;
        b++;
        c++;

        printf("a:%d\t b:%d\tc:%d\n",a,b,c);

}

int main(int argc, char* argv[])
{
        function();
        return 0;
}

运行结果

image-20210309164329745
image-20210309164329745

分析

这里会观察到结果并不是想象中的2,3,4;而是2,4,7

细心的小伙伴肯定发现了:

  • 2 = 1 + 1  (char数据宽度为1字节)

  • 4 = 2 + 2  (short数据宽度为2字节)

  • 7 = 3 + 4  (int数据宽度为4字节)

结果是加上了原本各自的数据类型的宽度


拓展例子

前面只是都是一级指针,现在将指针换为二级指针:

C
void function() {                
    char** a;
    short** b;
    int** c;
    a=(char**)  1;
    b= (short**) 2;
    c=(int**)  3;

    a++;
    b++;
    c++;

    printf("a:%d\t b:%d\tc:%d\n",a,b,c);

}

运行结果

image-20210309165051328
image-20210309165051328

分析

此时的结果为:

  • 5= 1 + 4  (char* 数据宽度为4字节)

  • 6= 2 + 4  (short* 数据宽度为4字节)

  • 7 = 3 + 4  (int* 数据宽度为4字节)

结果为加上 去掉一个*后的数据宽度


拓展例子二

前面的加法操作都只增加了1,现在再来查看增加大于1时的情况

C
void function() {                
    char* a;
    short* b;
    int* c;
    a=(char*)  1;
    b= (short*) 2;
    c=(int*)  3;

    a=a+5;
    b=b+5;
    c=c+5;

    printf("a:%d\t b:%d\tc:%d\n",a,b,c);
}

运行结果

image-20210309170142595
image-20210309170142595

分析

此时的结果为:

  • 6= 1 + 5*1 (char 数据宽度为1字节)

  • 12= 2 + 5*2  (short 数据宽度为2字节)

  • 23 = 3 + 5*4  (int 数据宽度为4字节)

结果为加上 去掉一个*后的数据宽度 × 增加的数值

总结

无论是指针的加亦或是减(这里只演示了加法,但减法同理),其加或减的单位为去掉一个*后的数据宽度

也就是实际增减的数值=去掉一个*后的数据宽度 × 增减的数值

指针类型相减

前面提到的指针的加减都是同一个指针里的加减,但指针之间其实也支持相减操作(不支持相加)

但指针之间的加减要求指针的类型必须一致,即char*类型只能和char*类型相加减,不能和char**或其它类型相加减

例子

C
void function() {                
    char* a;
    char* b;
    short* c;
    short* d;
    int* e;
    int* f;

    a=(char*) 200;
    b=(char*) 100;

    c=(short*) 200;
    d=(short*) 100;

    e=(int*) 200;
    f=(int*) 100;

    printf("%d\n",a-b);
    printf("%d\n",c-d);
    printf("%d\n",e-f);
}

运行结果

image-20210309171355894
image-20210309171355894

分析

此时的结果为:

  • 100 =  (200 - 100)/1(char 数据宽度为1字节)

  • 50 =  (200 - 100)/2  (short 数据宽度为2字节)

  • 25 =   (200 - 100)/4 (int 数据宽度为4字节)

结果为相减完后再除以  原本各自的数据宽度


扩展例子

前面只是都是一级指针,现在将指针换为四级指针:

C
void function() {                
    char**** a;
    char**** b;
    short**** c;
    short**** d;
    int**** e;
    int**** f;

    a=(char****) 200;
    b=(char****) 100;

    c=(short****) 200;
    d=(short****) 100;

    e=(int****) 200;
    f=(int****) 100;

    printf("%d\n",a-b);
    printf("%d\n",c-d);
    printf("%d\n",e-f);
}

运行结果

image-20210309171954363
image-20210309171954363

分析

此时的结果为:

  • 25 =  (200 - 100)/4(char*** 数据宽度为4字节)

  • 25 =  (200 - 100)/4  (short*** 数据宽度为4字节)

  • 25 =   (200 - 100)/4 (int*** 数据宽度为4字节)

结果为相减后再除以  去掉一个*后的数据宽度

总结

指针之间的减法,其结果为相减后再除以去掉一个*后的数据宽度

指针之间的比较

指针之间也支持相互比较,但也和上面指针类型相减一样,要求指针类型一致

例子

C
void function(){                
    char**** a;
    char**** b;

    a=(char****) 200;
    b=(char****) 100;

    if (a>b)
    {
            printf("a>b\n");
    }else{
            printf("a<=b\n");
    }
}

运行结果

image-20210309172856531
image-20210309172856531

结论

相同类型的指针之间是支持大小比较的

总结

  • 指针的数据宽度为4字节,或者说:无论是什么类型,在其后面加上__(无论加几个\__都一样)后其数据宽度都变为4字节

  • 指针数值支持进行加减,加减的结果=去掉一个*后的数据宽度 × 增减的数值

  • 指针之间支持减法但不支持加法,其结果为相减后再除以去掉一个*后的数据宽度

  • 指针之间支持比较大小,但要求进行比较的两个指针为相同类型


获取变量的数据类型

在C语言中可以使用下面的方法获取一个变量的数据类型:

代码

C
#include "stdafx.h"
#include <typeinfo>
void function(){                
        char**** a;
        printf("%s\n",typeid(a).name());
}
int main(int argc, char* argv[])
{
        function();
        return 0;
}

运行结果

image-20210309223919194
image-20210309223919194

使用方式

通过上面的例子可以知道使用方式首先要包含一个相关的头文件typeinfo

然后使用该头文件的方法typeid(变量).name()即可获得变量对应的数据类型


取变量地址

在C语言中可以在变量前加上&符号来获取一个变量的地址

首先看看取回来地址的类型

取地址的返回类型

C
#include "stdafx.h"
#include <typeinfo>
void function(){                
        char a;
        short b;
        int c;
        char* d;
        printf("%s\n",typeid(&a).name());
        printf("%s\n",typeid(&b).name());        
        printf("%s\n",typeid(&c).name());
        printf("%s\n",typeid(&d).name());
}

int main(int argc, char* argv[])
{
        function();
        return 0;
}

运行结果

image-20210309224811959
image-20210309224811959

分析结果

不难发现,所有取地址返回的类型都为原本变量的类型后加个*,也契合了本笔记的主题——指针

取地址的内容

通过前面的分析得出了取出的地址类型是一个指针类型,现在要观察其存储的内容

代码

C
#include "stdafx.h"
#include <typeinfo>
//为了方便观察地址 先声明为全局变量
int a;
void function(){
    a=610;
        int* b=&a;
    printf("%x\n",b);
}

int main(int argc, char* argv[])
{
        function();
        return 0;
}

运行结果

image-20210309231812753
image-20210309231812753

反汇编代码

X86ASM
11:       a=610;
00401038   mov         dword ptr [a (00427c48)],262h
12:       int* b=&a;
00401042   mov         dword ptr [ebp-4],offset a (00427c48)
13:       printf("%x\n",b);

通过a的赋值语句可以看到a存储在00427c48这个内存地址中

X86ASM
11:       a=610;
00401038   mov         dword ptr [a (00427c48)],262h

再看下面的指针赋值语句

X86ASM
12:       int* b=&a;
00401042   mov         dword ptr [ebp-4],offset a (00427c48)

这里的offset a是vc6.0为了方便使用者查看生成的,实际上的语句为:

X86ASM
mov dword ptr ss:[ebp-0x4],0x427C48

也就是直接将全局变量a的地址00427c48赋给b


代码二

前面声明的变量为全局变量,现在来看看局部变量的情况:

C
#include "stdafx.h"
#include <typeinfo>
void function(){
    //这里a声明为局部变量
        int a=610;
        int* b=&a;
        printf("%x\n",b);
}

int main(int argc, char* argv[])
{
        function();
        return 0;
}

运行结果二

image-20210309235502045
image-20210309235502045

此时的地址显然就是一个堆栈中的地址,对应了变量存储在堆栈中

反汇编代码二

X86ASM
11:       int a=610;
00401038   mov         dword ptr [ebp-4],262h
12:       int* b=&a;
0040103F   lea         eax,[ebp-4]
00401042   mov         dword ptr [ebp-8],eax
13:       printf("%x\n",b);

可以看到此时是通过lea指令将变量a的地址ebp-4传给eax,然后再将eax赋值给b


取地址中存储数据

前面讲了如何获取一个变量的地址,那么在获取完地址后,再说说如何获取这地址中存储的数据

在C语言中,在一个指针类型的变量前面加上*符号,即可取出该地址里所存储的内容

取地址数据的返回类型

如法炮制,观察取地址数据的返回类型

代码

C
#include "stdafx.h"
#include <typeinfo>
void function() {
    int***a=(int***) 610;
    printf("%s\n",typeid(*a).name());
    printf("%s\n",typeid(**a).name());
    printf("%s\n",typeid(***a).name());
}

int main(int argc, char* argv[])
{
        function();
        return 0;
}

运行结果

image-20210310000600529
image-20210310000600529

分析结果

不难发现,所有取地址数据返回的类型都为原本变量的类型后减个*,可以说是和&取地址正好相反

不同的是对于多级指针,可以一次使用多个*来多次取地址中存储的数据

取地址数据的内容

前面了解了*符号的使用,现在来看个稍微复杂点的例子

代码

C
#include "stdafx.h"
#include <typeinfo>
int a;
void function() {
    a=610;
    int* b=&a;
    int** c=&b;
    int*** d=&c;
    c=*d;
    b=*c;
    a=*b;

    b=**d;
    a=**c;   

    a=***d;
}

int main(int argc, char* argv[])
{
        function();
        return 0;
}

反汇编代码

X86ASM
11:       a=610;
00401038   mov         dword ptr [a (00427c50)],262h
12:       int* b=&a;
00401042   mov         dword ptr [ebp-4],offset a (00427c50)
13:       int** c=&b;
00401049   lea         eax,[ebp-4]
0040104C   mov         dword ptr [ebp-8],eax
14:       int*** d=&c;
0040104F   lea         ecx,[ebp-8]
00401052   mov         dword ptr [ebp-0Ch],ecx
15:       c=*d;
00401055   mov         edx,dword ptr [ebp-0Ch]
00401058   mov         eax,dword ptr [edx]
0040105A   mov         dword ptr [ebp-8],eax
16:       b=*c;
0040105D   mov         ecx,dword ptr [ebp-8]
00401060   mov         edx,dword ptr [ecx]
00401062   mov         dword ptr [ebp-4],edx
17:       a=*b;
00401065   mov         eax,dword ptr [ebp-4]
00401068   mov         ecx,dword ptr [eax]
0040106A   mov         dword ptr [a (00427c50)],ecx
18:
19:       b=**d;
00401070   mov         edx,dword ptr [ebp-0Ch]
00401073   mov         eax,dword ptr [edx]
00401075   mov         ecx,dword ptr [eax]
00401077   mov         dword ptr [ebp-4],ecx
20:       a=**c;
0040107A   mov         edx,dword ptr [ebp-8]
0040107D   mov         eax,dword ptr [edx]
0040107F   mov         ecx,dword ptr [eax]
00401081   mov         dword ptr [a (00427c50)],ecx
21:
22:       a=***d;
00401087   mov         edx,dword ptr [ebp-0Ch]
0040108A   mov         eax,dword ptr [edx]
0040108C   mov         ecx,dword ptr [eax]
0040108E   mov         edx,dword ptr [ecx]
00401090   mov         dword ptr [a (00427c50)],edx

分析反汇编

首先将各变量信息整理出来,方便后面分析:

变量变量地址变量内容/地址的值(十六进制)
a00427c50262(对应十进制为610)
bebp-4 = 0012FF2800427C50
cebp-8 = 0012FF240012FF28
debp-0Ch = 0012FF200012FF24

代码中涉及的变量较多,这里只拿最复杂的 a=***d来作分析,其它留作样例

X86ASM
22:       a=***d;
00401087   mov         edx,dword ptr [ebp-0Ch]
0040108A   mov         eax,dword ptr [edx]
0040108C   mov         ecx,dword ptr [eax]
0040108E   mov         edx,dword ptr [ecx]
00401090   mov         dword ptr [a (00427c50)],edx

1.这里的ebp-0Ch对应的是d的地址,此时就是将d赋值给edx

X86ASM
00401087   mov         edx,dword ptr [ebp-0Ch]
image-20210310002638753
image-20210310002638753

结合内存里的数据可以得到:d的地址ebp-0Ch0012FF20,d=[ebp-0Ch]=0012FF24

这里的代码相当于

X86ASM
00401087   mov         edx,0012FF24h(d)

2.将前面edx地址里存储的数据赋值给eax,此时的[edx]存储的其实就是c

X86ASM
0040108A   mov         eax,dword ptr [edx]
image-20210310002914650
image-20210310002914650

结合内存里的数据可以得到:dedx0012FF24,[edx]0012FF28c

这里的代码相当于

X86ASM
0040108A   mov         eax,0012FF28(c)

3.将前面eax地址里存储的数据赋值给ecx,此时的[eax]存储的其实就是b

X86ASM
0040108C   mov         ecx,dword ptr [eax]
image-20210310003238462
image-20210310003238462

结合内存里的数据可以得到:ceax0012FF28,b=[eax]=00427C50

这里的代码相当于

X86ASM
0040108C   mov         ecx,00427C50(b)

4.将前面ecx地址里存储的数据赋值给edx,此时的[ecx]存储的其实就是a

X86ASM
0040108E   mov         edx,dword ptr [ecx]
image-20210310004636277
image-20210310004636277

结合内存里的数据可以得到:becx00427C50,a=[ecx]262h610

这里的代码相当于

X86ASM
0040108E   mov         edx,262h

5.最后将edx赋值给a

X86ASM
00401090   mov         dword ptr [a (00427c50)],edx

小总结

可以看到,被赋值变量 = * 赋值变量   在汇编中的形式为:mov 被赋值变量,[赋值变量]

如果有多个*,则多执行几次来取值

总结

  • 可以在变量前加上&符号来获取变量地址

  • 取地址返回的类型都为原本变量的类型后加个*,也就是个指针类型

  • 在一个指针类型的变量前面加上*符号,即可取出该地址里所存储的内容

  • 取地址数据返回的类型都为原本变量的类型后减个*,可以说是和&取地址正好相反

  • 对于多级指针,可以一次使用多个*来多次取地址中存储的数据


指针遍历数组

有了先前的基础,再来看看如何用指针遍历数组

代码

C
#include "stdafx.h"
void function(){
        short arr[5]={1,2,3,4,5};

        short* p=&arr[0];
        short* p2=arr;

        if(p==p2){
                printf("equal\n");
        }else{
                printf("not equal\n");
        }

        int i;
        for(i=0;i<5;i++){
                int j=*(p+i);
                printf("addr:%x value:%d\n",p+i,j);
        }        
}
int main(int argc, char* argv[])
{
        function();
        return 0;
}

代码说明

稍微说明的一下代码部分

主要是声明了一个数组,然后用两种方法取得数组的首地址,一种是&arr[0],另一种则直接是arr

后面则是通过循环配合指针遍历数组成员并输出

按顺序依次涉及先前指针笔记的知识点:

  1. 取变量地址

  2. 指针赋值

  3. 指针之间的比较

  4. 取地址中存储数据

  5. 指针的加减


运行结果

image-20210310153516983
image-20210310153516983

可以看到通过&arr[0]和arr取数组首地址得到的结果是一致的

并且能够通过指针来输出数组成员的地址和对应的数据

这里还会观察到数组里的每个地址都相差2(short类型的数据宽度),和先前数组的学习又匹配上了


反汇编代码

X86ASM
11:       short arr[5]={1,2,3,4,5};
0040D7A8   mov         word ptr [ebp-0Ch],offset function+1Ch (0040d7ac)
0040D7AE   mov         word ptr [ebp-0Ah],offset function+22h (0040d7b2)
0040D7B4   mov         word ptr [ebp-8],offset function+28h (0040d7b8)
0040D7BA   mov         word ptr [ebp-6],offset function+2Eh (0040d7be)
0040D7C0   mov         word ptr [ebp-4],offset function+34h (0040d7c4)
12:
13:       short* p=&arr[0];
0040D7C6   lea         eax,[ebp-0Ch]
0040D7C9   mov         dword ptr [ebp-10h],eax
14:       short* p2=arr;
0040D7CC   lea         ecx,[ebp-0Ch]
0040D7CF   mov         dword ptr [ebp-14h],ecx
15:
16:       if(p==p2){
0040D7D2   mov         edx,dword ptr [ebp-10h]
0040D7D5   cmp         edx,dword ptr [ebp-14h]
0040D7D8   jne         function+59h (0040d7e9)
17:           printf("equal\n");
0040D7DA   push        offset string "equal\n" (00422fbc)
0040D7DF   call        printf (0040d710)
0040D7E4   add         esp,4
18:       }else{
0040D7E7   jmp         function+66h (0040d7f6)
19:           printf("not equal\n");
0040D7E9   push        offset string "not equal\n" (00422fb0)
0040D7EE   call        printf (0040d710)
0040D7F3   add         esp,4
20:       }
21:
22:       int i;
23:
24:       for(i=0;i<5;i++){
0040D7F6   mov         dword ptr [ebp-18h],0
0040D7FD   jmp         function+78h (0040d808)
0040D7FF   mov         eax,dword ptr [ebp-18h]
0040D802   add         eax,1
0040D805   mov         dword ptr [ebp-18h],eax
0040D808   cmp         dword ptr [ebp-18h],5
0040D80C   jge         function+0A8h (0040d838)
25:           int j=*(p+i);
0040D80E   mov         ecx,dword ptr [ebp-18h]
0040D811   mov         edx,dword ptr [ebp-10h]
0040D814   movsx       eax,word ptr [edx+ecx*2]
0040D818   mov         dword ptr [j],eax
26:           printf("addr:%x value:%d\n",p+i,j);
0040D81B   mov         ecx,dword ptr [j]
0040D81E   push        ecx
0040D81F   mov         edx,dword ptr [ebp-18h]
0040D822   mov         eax,dword ptr [ebp-10h]
0040D825   lea         ecx,[eax+edx*2]
0040D828   push        ecx
0040D829   push        offset string "addr:%x value%d:\n" (00422f9c)
0040D82E   call        printf (0040d710)
0040D833   add         esp,0Ch
27:       }
0040D836   jmp         function+6Fh (0040d7ff)
28:
29:   }

反汇编分析

由于循环和数组等相关的知识在先前的笔记已经详细学习过了,这里就直接看指针相关的代码

X86ASM
码25:           int j=*(p+i);
0040D80E   mov         ecx,dword ptr [ebp-18h]
0040D811   mov         edx,dword ptr [ebp-10h]
0040D814   movsx       eax,word ptr [edx+ecx*2]
0040D818   mov         dword ptr [j],eax

1.将ebp-18h里的值赋值给ecx,这里的[ebp-18h]其实对应的就是 i

X86ASM
0040D80E   mov         ecx,dword ptr [ebp-18h] (i)

2.将ebp-10h里的值赋值给edx,这里的[ebp-10h]其实对应的是p,即数组首地址

X86ASM
0040D811   mov         edx,dword ptr [ebp-10h] (p)

3.movsx是带符号扩展赋值,将edxecx__2,也就是pi\__数据宽度地址里存储的值赋给eax

X86ASM
0040D814   movsx       eax,word ptr [edx+ecx*2]

为什么要使用movsx指令?

内存对齐的结果,先前的笔记就提到过:char short 在计算时都会转变为dword宽度来进行计算        

相关笔记可以参考:

逆向基础笔记十三 汇编C语言类型转换

逆向基础笔记十八 汇编 结构体和内存对齐


4.将前面暂存在寄存器中的值赋给变量j

X86ASM
0040D818   mov         dword ptr [j],eax

指针翻转数组

翻转数组思想

翻转数组的思想就是从数组两端(数组首部和数组尾部)开始然后逐渐向中间靠拢,相互交换数组中的内容

image-20210310163600879
image-20210310163600879

代码

C
#include "stdafx.h"
void function(){
        int arr[5]={1,2,3,4,5};

        int* begin=&arr[0];
        int* end=begin+4;

        while(begin<end){
                int tmp=*begin;
                *begin=*end;
                *end=tmp;
                begin++;
                end--;
        }

        int i;
        for(i=0;i<5;i++){
                printf("%d\n",arr[i]);
        }        

}
int main(int argc, char* argv[])
{
        function();
        return 0;
}

代码分析

数组翻转的关键代码是:

C
int* begin=&arr[0];
int* end=begin+4;

while(begin<end) {
    int tmp=*begin;
    *begin=*end;
    *end=tmp;
    begin++;
    end--;
}

1.获取数组首地址和尾地址

C
int* begin=&arr[0];
int* end=begin+4;

2.循环直到所有数组成员交换结束

C
while (begin < end) {
}

3.取出begin中的数据放在临时变量中

C
int tmp=*begin;

4.用end里存储的值覆盖begin

C
*begin=*end;

5.将原本备份的begin的变量tmp赋值给end,此时已经完成了交换

C
*end=tmp;

6.继续交换,让指针向数组中间靠拢

C
begin++;
end--;

运行结果

image-20210310165950306
image-20210310165950306

可以看到,数组成功翻转了


反汇编实现翻转数组

前面使用指针实现了数组的翻转,为进一步了解其本质,自己手写汇编代码实现翻转数组

下面的汇编代码中省略了 dword ptr ds:[],默认就是取dword

代码

C
#include "stdafx.h"
void function() {
    int arr[5]={1,2,3,4,5};        
    int len=sizeof(arr)/sizeof(int)-1;        
    __asm{
            xor ecx,ecx
_begin:
            mov eax,len
            sub eax,ecx

            lea edx,[arr+ecx*4]        
            push edx
            mov edx,[edx]
            lea ebx,[arr+4*eax]
            push ebx
            mov ebx,[ebx]
            xchg [arr+ecx*4],ebx
            pop ebx

            mov [ebx],edx
            pop edx
            inc ecx

            cmp edx,ebx
            jb _begin

    }
    int i;
    for(i=0;i<5;i++){
            printf("%d\n",arr[i]);
    }        

}

int main(int argc, char* argv[])
{
        function();
        return 0;
}

运行结果

image-20210310184633838
image-20210310184633838

能够正确地实现相同的功能O(∩_∩)O


反汇编代码分析

C
__asm{
                xor ecx,ecx
_begin:
                mov eax,len
                sub eax,ecx

                lea edx,[arr+ecx*4]        
                push edx
                mov edx,[edx]
                lea ebx,[arr+4*eax]
                push ebx
                mov ebx,[ebx]
                xchg [arr+ecx*4],ebx
                pop ebx

                mov [ebx],edx
                pop edx
                inc ecx

                cmp edx,ebx
                jb _begin

        }

1.将ecx清零,初始化ecx,ecx在这里是作为偏移量来使用的(刚开始为首地址的偏移,后来慢慢往中间靠拢)

X86ASM
xor ecx,ecx

2.声明一个程序段,后续跳转会用到

X86ASM
_begin:

3.将数组的长度减1的值赋给eax,因为数组从0开始,所以要减1

X86ASM
mov eax,len

4.用先前的eax减去ecx获得偏移(刚开始为尾地址的偏移,后来慢慢往中间靠拢)

X86ASM
sub eax,ecx

5.通过数组首地址加上ecx偏移取得地址,刚开始取得的为首地址,相当于edx=begin

X86ASM
lea edx,[arr+ecx*4]        

6.将前面获得的地址edx放入堆栈中

X86ASM
push edx

7.取出edx中的值,这里相当于edx=*begin=tmp

X86ASM
mov edx,[edx]

8.通过数组首地址加上偏移eax取得地址,刚开始取得的为尾地址,相当于ebx=end

X86ASM
lea ebx,[arr+4*eax]

9.将前面获得的地址ebx放入堆栈中

X86ASM
push ebx

10.取出ebx中的值,这里对相当于ebx=*end

X86ASM
mov ebx,[ebx]

11.交换arr+ecx*4(*begin)和ebx(*end)里存储的值,这里相当于*begin=*end

X86ASM
xchg [arr+ecx*4],ebx

12.将先前的push的end的地址恢复到ebx,使得ebx=end

X86ASM
pop ebx

13.这里相当于*end=tmp,此时数组中的两个成员就已经交换完毕了

X86ASM
mov [ebx],edx

14.将先前push的begin的地址恢复到edx,使得edx=begin

X86ASM
pop edx

15.让ecx自增一,这里相当于begin++;end--; 因为这里end的偏移是通过len-begin的偏移得到的

X86ASM
inc ecx

16.比较edx和ebx

jb:jump below,小于则跳转(无符号),这里相当于while(begin

如果begin

X86ASM
cmp edx,ebx
jb _begin

总结

可以通过指针来存储数组的各成员地址,然后用指针来遍历数组,翻转数组

评论交流
暂无评论,欢迎分享您的见解