C++反汇编与逆向分析技术揭秘(第2版)
上QQ阅读APP看书,第一时间看更新

2.5 地址、指针和引用

在C++中,地址标号使用十六进制表示,取一个变量的地址使用“&”符号,只有变量才存在内存地址,常量(见2.6节)没有地址(不包括const定义的伪常量)。例如,对于数字100,我们无法取出它的地址。取出的地址是一个常量值,无法再对其取地址了。

指针的定义使用“TYPE*”格式,TYPE为数据类型,任何数据类型都可以定义指针。指针本身也是一种数据类型,用于保存各种数据类型在内存中的地址。指针变量同样可以取出地址,所以会出现多级指针。

引用的定义格式为“TYPE&”,TYPE为数据类型。在C++中是不可以单独定义的,并且在定义时就要进行初始化。引用表示一个变量的别名,对它的任何操作本质上都是在操作它所表示的变量。详细讲解见2.5.3节。

2.5.1 指针和地址的区别

在32位应用程序中,地址是一个由32位二进制数字组成的值;在64位应用程序中,地址是一个由64位二进制数字组成的值。为了便于查看,转换成十六进制数字显示出来,用于标识内存编号。指针是用于保存这个编号的一种变量类型,它包含在内存中,所以可以取出指针类型变量在内存中的位置——地址。由于指针保存的数据都是地址,所以无论什么类型的指针,32位程序都占据4字节的内存空间,64位程序都占据8字节的内存空间,如图2-9所示。

图2-9 地址和指针

指针可以根据指针类型对地址对应的数据进行解释。而一个地址值无法单独解释数据,对于图2-9中0x0135FE04这个地址值,仅凭借它本身无法说明该地址处对应数据的信息。如果是在一个int类型的指针中保存这个地址,就可以将其看作int类型数据的起始地址,向后数4字节到0x0135FE08处,将0x0135FE04~0x0135FE08中的数据按整型存储方式解释,详见2.5.2节。

指针和地址之间的不同点如表2-3所示。

表2-3 指针和地址之间的不同点

指针和地址之间的共同点如表2-4所示。

表2-4 指针和地址之间的共同点

2.5.2 各类型指针的工作方式

在C++中,任何数据类型都有对应的指针类型。我们从前面的学习中了解到,指针保存的都是地址,为什么还需要类型作为修饰呢?因为我们需要用类型去解释这个地址中的数据。每种数据类型所占的内存空间不同,指针只保存了存放数据的首地址,而没有指明该在哪里结束。这时就需要根据对应的类型来寻找解释数据的结束地址。同一地址使用不同类型的指针进行访问,取出的内容就会不一样,如代码清单2-4所示。

代码清单2-4 不同类型指针访问同一地址

// C++源码
#include <stdio.h>
int main(int argc, char* argv[]) {
  int n = 0x12345678;
  int *p1 = &n;
  char *p2 = (char*)&n;
  short *p3 = (short*)&n;
  printf("%08x \r\n", *p1);
  printf("%08x \r\n", *p2);
  printf("%08x \r\n", *p3);
  return 0;
}

//x86_vs对应汇编代码讲解
00401000  push    ebp
00401001  mov     ebp, esp
00401003  sub     esp, 10h                      ;申请局部变量
00401006  mov     dword ptr [ebp-4], 12345678h  ;n=0x12345678
0040100D  lea     eax, [ebp-4]
00401010  mov     [ebp-8], eax                  ;p1=&n
00401013  lea     ecx, [ebp-4]
00401016  mov     [ebp-0Ch], ecx                ;p2=&n
00401019  lea     edx, [ebp-4]
0040101C  mov     [ebp-10h], edx                ;p3=&n
0040101F  mov     eax, [ebp-8]                  ;取出地址
00401022  mov     ecx, [eax]                    ;取4字节内容
00401024  push    ecx ;参数2 *p1
00401025  push    offset a08x                   ;参数1 "%08x \r\n"
0040102A  call    sub_4010A0                    ;调用printf函数
0040102F  add     esp, 8                        ;平衡栈
00401032  mov     edx, [ebp-0Ch]                ;取出地址
00401035  movsx   eax, byte ptr [edx]           ;取1字节内容,高位符号扩展成4字节
00401038  push    eax ;参数2 *p2
00401039  push    offset a08x_0                 ;参数1 "%08x \r\n"
0040103E  call    sub_4010A0                    ;调用printf函数
00401043  add     esp, 8                        ;平衡栈
00401046  mov     ecx, [ebp-10h]                ;取出地址
00401049  movsx   edx, word ptr [ecx]           ;取2字节内容,高位符号扩展成4字节
0040104C  push    edx                           ;参数2 *p3
0040104D  push    offset a08x_1                 ;参数1 "%08x \r\n"
00401052  call    sub_4010A0                    ;调用printf函数
00401057  add     esp, 8                        ;平衡栈
0040105A  xor     eax, eax
0040105C  mov     esp, ebp
0040105E  pop     ebp
0040105F  retn

//x86_gcc对应汇编代码讲解
00401510  push    ebp
00401511  mov     ebp, esp
00401513  and     esp, 0FFFFFFF0h               ;对齐栈
00401516  sub     esp, 20h
00401519  call    ___main                       ;调用初始化函数
0040151E  mov     dword ptr [esp+10h], 12345678h;n=0x12345678
00401526  lea     eax, [esp+10h]
0040152A  mov     [esp+1Ch], eax                ;p1=&n
0040152E  lea     eax, [esp+10h]
00401532  mov     [esp+18h], eax                ;p2=&n
00401536  lea     eax, [esp+10h]
0040153A  mov     [esp+14h], eax                ;p3=&n
0040153E  mov     eax, [esp+1Ch]                ;取出地址
00401542  mov     eax, [eax]                    ;取4字节内容
00401544  mov     [esp+4], eax                  ;参数2 *p1
00401548  mov     dword ptr [esp], offset a08x  ;参数1 "%08x \r\n"
0040154F  call    _printf                       ;调用printf函数
00401554  mov     eax, [esp+18h]                ;取出地址
00401558  movzx   eax, byte ptr [eax]           ;取1字节内容
0040155B  movsx   eax, al                       ;高位符号扩展成4字节
0040155E  mov     [esp+4], eax                  ;参数2 *p2
00401562  mov     dword ptr [esp], offset a08x  ;参数1 "%08x \r\n"
00401569  call    _printf                       ;调用printf函数
0040156E  mov     eax, [esp+14h]                ;取出地址
00401572  movzx   eax, word ptr [eax]           ;取2字节内容,高位0扩展成4字节
00401575  cwde                                  ;高位符号扩展成4字节
00401576  mov     [esp+4], eax                  ;参数2 *p3
0040157A  mov     dword ptr [esp], offset a08x  ;参数1 "%08x \r\n"
00401581  call    _printf                       ;调用printf函数
00401586  mov     eax, 0
0040158B  leave
0040158C  retn

//x86_clang对应汇编代码讲解
00401000  push    ebp
00401001  mov     ebp, esp
00401003  push    esi
00401004  sub     esp, 30h
00401007  mov     eax, [ebp+0Ch]
0040100A  mov     ecx, [ebp+8]
0040100D  mov     dword ptr [ebp-8], 0
00401014  mov     dword ptr [ebp-0Ch], 12345678h;n=0x12345678
0040101B  lea     edx, [ebp-0Ch]
0040101E  mov     [ebp-10h], edx                ;p1=&n
00401021  mov     esi, edx
00401023  mov     [ebp-14h], esi                ;p2=&n
00401026  mov     [ebp-18h], edx                ;p3=&n
00401029  mov     edx, [ebp-10h]                ;取出地址
0040102C  mov     edx, [edx]                    ;取4字节内容
0040102E  lea     esi, a08x
00401034  mov     [esp], esi                    ;参数1 "%08x \r\n"
00401037  mov     [esp+4], edx                  ;参数2 *p1
0040103B  mov     [ebp-1Ch], eax
0040103E  mov     [ebp-20h], ecx
00401041  call    sub_401090                    ;调用printf函数
00401046  mov     ecx, [ebp-14h]                ;取出地址
00401049  movsx   ecx, byte ptr [ecx]           ;取1字节内容,高位符号扩展成4字节
0040104C  lea     edx, a08x
00401052  mov     [esp], edx                    ;参数1 "%08x \r\n"
00401055  mov     [esp+4], ecx                  ;参数2 *p2
00401059  mov     [ebp-24h], eax
0040105C  call    sub_401090                    ;调用printf函数
00401061  mov     ecx, [ebp-18h]                ;取出地址
00401064  movsx   ecx, word ptr [ecx]           ;取出2字节的内容,高位符号扩展成4字节
00401067  lea     edx, a08x
0040106D  mov     [esp], edx;                   ;参数1 "%08x \r\n"
00401070  mov     [esp+4], ecx                  ;参数2 *p3
00401074  mov     [ebp-28h], eax
00401077  call    sub_401090                    ;调用printf函数
0040107C  xor     ecx, ecx
0040107E  mov     [ebp-2Ch], eax
00401081  mov     eax, ecx
00401083  add     esp, 30h
00401086  pop     esi
00401087  pop     ebp
00401088  retn

//x64_vs对应汇编代码讲解
0000000140001000  mov     [rsp+10h], rdx
0000000140001005  mov     [rsp+8], ecx
0000000140001009  sub     rsp, 48h
000000014000100D  mov     dword ptr [rsp+20h], 12345678h; n=0x12345678
0000000140001015  lea     rax, [rsp+20h]
000000014000101A  mov     [rsp+28h], rax        ;p1=&n
000000014000101F  lea     rax, [rsp+20h]
0000000140001024  mov     [rsp+30h], rax        ;p2=&n
0000000140001029  lea     rax, [rsp+20h]
000000014000102E  mov     [rsp+38h], rax        ;p3=&n
0000000140001033  mov     rax, [rsp+28h]        ;取出地址
0000000140001038  mov     edx, [rax]            ;参数2取出4字节内容 *p1
000000014000103A  lea     rcx, a08x             ;参数1 "%08x \r\n"
0000000140001041  call    sub_1400010E0         ;调用printf函数
0000000140001046  mov     rax, [rsp+30h]        ;取出地址
000000014000104B  movsx   eax, byte ptr [rax]   ;取出1字节的内容,高位符号扩展成4字节
000000014000104E  mov     edx, eax              ;参数2  *p2
0000000140001050  lea     rcx, a08x_0           ;参数1 "%08x \r\n"
0000000140001057  call    sub_1400010E0         ;调用printf函数
000000014000105C  mov     rax, [rsp+38h]        ;取出地址
0000000140001061  movsx   eax, word ptr [rax]   ;取出2字节内容,高位符号扩展成4字节
0000000140001064  mov     edx, eax              ;参数2 *p3
0000000140001066  lea     rcx, a08x_1           ;参数1 "%08x \r\n"
000000014000106D  call    sub_1400010E0         ;调用printf函数
0000000140001072  xor     eax, eax
0000000140001074  add     rsp, 48h
0000000140001078  retn

//x64_gcc对应汇编代码讲解
0000000000401550  push    rbp
0000000000401551  mov     rbp, rsp
0000000000401554  sub     rsp, 40h
0000000000401558  mov     [rbp+10h], ecx
000000000040155B  mov     [rbp+18h], rdx
000000000040155F  call    __main                ;调用初始化函数
0000000000401564  mov     dword ptr [rbp-1Ch], 12345678h;n=0x12345678
000000000040156B  lea     rax, [rbp-1Ch]
000000000040156F  mov     [rbp-8], rax          ;p1=&n
0000000000401573  lea     rax, [rbp-1Ch]
0000000000401577  mov     [rbp-10h], rax        ;p2=&n
000000000040157B  lea     rax, [rbp-1Ch]
000000000040157F  mov     [rbp-18h], rax        ;p3=&n
0000000000401583  mov     rax, [rbp-8]          ;取出地址
0000000000401587  mov     eax, [rax]            ;取出4字节内容
0000000000401589  mov     edx, eax              ;参数2 *p1
000000000040158B  lea     rcx, Format           ;参数1 "%08x \r\n"
0000000000401592  call    printf                ;调用printf函数
0000000000401597  mov     rax, [rbp-10h]        ;取出地址
000000000040159B  movzx   eax, byte ptr [rax]   ;取出1字节内容,高位0扩展成4字节
000000000040159E  movsx   eax, al               ;高位符号扩展成4字节
00000000004015A1  mov     edx, eax              ;参数2 *p2
00000000004015A3  lea     rcx, Format           ;参数1 "%08x \r\n"
00000000004015AA  call    printf                ;调用printf函数
00000000004015AF  mov     rax, [rbp-18h]        ;取出地址
00000000004015B3  movzx   eax, word ptr [rax]   ;取出2字节内容,高位0扩展成4字节
00000000004015B6  cwde                          ;高位符号扩展成4字节
00000000004015B7  mov     edx, eax              ;参数2 *p3
00000000004015B9  lea     rcx, Format           ;参数1 "%08x \r\n"
00000000004015C0  call    printf                ;调用printf函数
00000000004015C5  mov     eax, 0
00000000004015CA  add     rsp, 40h
00000000004015CE  pop     rbp
00000000004015CF  retn

//x64_clang对应汇编代码讲解
0000000140001000  sub     rsp, 68h
0000000140001004  mov     dword ptr [rsp+64h], 0
000000014000100C  mov     [rsp+58h], rdx
0000000140001011  mov     [rsp+54h], ecx
0000000140001015  mov     dword ptr [rsp+50h], 12345678h; n=0x12345678
000000014000101D  lea     rdx, [rsp+50h]
0000000140001022  mov     [rsp+48h], rdx        ;p1=&n
0000000140001027  mov     rax, rdx
000000014000102A  mov     [rsp+40h], rax        ;p2=&n
000000014000102F  mov     [rsp+38h], rdx        ;p3=&n
0000000140001034  mov     rax, [rsp+48h]        ;取出地址
0000000140001039  mov     edx, [rax]            ;参数2取出4字节内容 *p1
000000014000103B  lea     rcx, a08x             ;参数1 "%08x \r\n"
0000000140001042  call    sub_140001090         ;调用printf函数
0000000140001047  mov     rcx, [rsp+40h]        ;取出地址
000000014000104C  movsx   edx, byte ptr [rcx];参数2取出1字节内容,高位符号扩展成4字节
000000014000104F  lea     rcx, a08x             ;参数1 "%08x \r\n"
0000000140001056  mov     [rsp+34h], eax
000000014000105A  call    sub_140001090         ;调用printf函数
000000014000105F  mov     rcx, [rsp+38h]        ;取出地址
0000000140001064  movsx   edx, word ptr [rcx]   ;参数2取出2字节内容,高位符号扩展成4字节
0000000140001067  lea     rcx, a08x             ;参数1 "%08x \r\n"
000000014000106E  mov     [rsp+30h], eax
0000000140001072  call    sub_140001090         ;调用printf函数
0000000140001077  xor     edx, edx
0000000140001079  mov     [rsp+2Ch], eax
000000014000107D  mov     eax, edx
000000014000107F  add     rsp, 68h
0000000140001083  retn

代码清单2-4中使用了3种方式对变量n的地址进行解释。变量n在内存中的数据为“78 56 34 12”,首地址从“78”开始。指针p1为int类型,以int类型在内存中占用的空间大小和排列方式对地址进行解释,然后取出数据。int类型占4字节内存空间,以小尾方式排列,取出内容为“12345678”,是一个十六进制的数字。同理,p2、p23将会按照它们的指针类型对地址数据进行解释。指针取内容的操作分为两个步骤:先取出指针中保存的地址信息,然后针对这个地址取内容,这是一个间接寻址的过程,也是识别指针的重要依据。该示例运行结果如图2-10所示。

图2-10 各类型指针解释地址的结果

通过代码清单2-4中指针取内容的过程可得出结论,不同类型的指针对地址的解释都取自其自身指针类型。

指针都支持哪些运算符号呢?在C++中,所有指针类型都只支持加法和减法。指针是用来保存数据地址、解释地址的,因此只有加法与减法才有意义,其他运算对于指针而言没有任何意义。

指针加法用于地址偏移,但并不像数学中的加法那样简单。指针加1后,指针内保存的地址值并不一定会加1,运算结果取决于指针类型,如指针类型为int,地址值将会加4,这个4是根据类型大小所得到的值。C++为什么要用这种烦琐的地址偏移方法呢?因为当指针中保存的地址为数组首地址时,为了能够利用指针加1后访问到数组内下一成员,所以加的是类型长度,而非数字1,如代码清单2-5所示。

代码清单2-5 各类型指针的寻址方式

// C++ 源码
#include <stdio.h>
int main(int argc, char* argv[]) {
  char ary[5] = {(char)0x01, (char)0x23, (char)0x45, (char)0x67, (char)0x89};
  int *p1 = (int*)ary;
  char *p2 = (char*)ary;
  short *p3 = (short*)ary;
  p1 += 1;
  p2 += 1;
  p3 += 1;
  return 0;
}

//x86_vs对应汇编代码讲解
00401000  push    ebp
00401001  mov     ebp, esp
00401003  sub     esp, 18h
00401006  mov     eax, ___security_cookie
0040100B  xor     eax, ebp
0040100D  mov     [ebp-4], eax                    ;缓冲区溢出检查代码
00401010  mov     byte ptr [ebp-0Ch], 1           ;ary[0]=1
00401014  mov     byte ptr [ebp-0Bh], 23h         ;ary[1]=0x23
00401018  mov     byte ptr [ebp-0Ah], 45h         ;ary[2]=0x45
0040101C  mov     byte ptr [ebp-9], 67h           ;ary[3]=0x67
00401020  mov     byte ptr [ebp-8], 89h           ;ary[4]=0x89
00401024  lea     eax, [ebp-0Ch]
00401027  mov     [ebp-10h], eax                  ;p1=(int*)ary
0040102A  lea     ecx, [ebp-0Ch]
0040102D  mov     [ebp-14h], ecx                  ;p2=(char*)ary
00401030  lea     edx, [ebp-0Ch]
00401033  mov     [ebp-18h], edx                  ;p3=(short*)ary
00401036  mov     eax, [ebp-10h]
00401039  add     eax, 4
0040103C  mov     [ebp-10h], eax                  ;p1 += 1
0040103F  mov     ecx, [ebp-14h]
00401042  add     ecx, 1
00401045  mov     [ebp-14h], ecx                  ;p2 += 1
00401048  mov     edx, [ebp-18h]
0040104B  add     edx, 2
0040104E  mov     [ebp-18h], edx                  ;p3 += 1
00401051  xor     eax, eax
00401053  mov     ecx, [ebp-4]
00401056  xor     ecx, ebp
00401058  call    @__security_check_cookie@4      ;缓冲区溢出检查代码
0040105D  mov     esp, ebp
0040105F  pop     ebp
00401060  retn

//x86_gcc对应汇编代码讲解
00401510  push    ebp
00401511  mov     ebp, esp
00401513  and     esp, 0FFFFFFF0h                 ;对齐栈
00401516  sub     esp, 20h
00401519  call    ___main                         ;调用初始化函数
0040151E  mov     byte ptr [esp+0Fh], 1           ;ary[0]=1
00401523  mov     byte ptr [esp+10h], 23h         ;ary[1]=0x23
00401528  mov     byte ptr [esp+11h], 45h         ;ary[2]=0x45
0040152D  mov     byte ptr [esp+12h], 67h         ;ary[3]=0x67
00401532  mov     byte ptr [esp+13h], 89h         ;ary[4]=0x89
00401537  lea     eax, [esp+0Fh]
0040153B  mov     [esp+1Ch], eax                  ;p1 = (int*)ary
0040153F  lea     eax, [esp+0Fh]
00401543  mov     [esp+18h], eax                  ;p2 = (char*)ary
00401547  lea     eax, [esp+0Fh]
0040154B  mov     [esp+14h], eax                  ;p3 = (short*)ary
0040154F  add     dword ptr [esp+1Ch], 4          ;p1 += 1
00401554  add     dword ptr [esp+18h], 1          ;p2 += 1
00401559  add     dword ptr [esp+14h], 2          ;p3 += 1
0040155E  mov     eax, 0
00401563  leave
00401564  retn

//x86_clang对应汇编代码讲解
00401000  push    ebp
00401001  mov     ebp, esp
00401003  push    ebx
00401004  push    edi
00401005  push    esi
00401006  sub     esp, 20h
00401009  mov     eax, [ebp+0Ch]
0040100C  mov     ecx, [ebp+8]
0040100F  xor     edx, edx
00401011  lea     esi, [ebp-15h]
00401014  mov     dword ptr [ebp-10h], 0
0040101B  mov     edi, ds:dword_40D150
00401021  mov     [ebp-15h], edi                  ;ary[]= {1, 0x23, 0x45, 0x67}
00401024  mov     bl, ds:byte_40D154
0040102A  mov     [ebp-11h], bl                   ;ary[4] = 0x89
0040102D  mov     edi, esi
0040102F  mov     [ebp-1Ch], edi                  ;p1 = (int*)ary
00401032  mov     [ebp-20h], esi                  ;p2 = (char*)ary
00401035  mov     [ebp-24h], esi                  ;p3 = (short)*ary
00401038  mov     esi, [ebp-1Ch]
0040103B  add     esi, 4
0040103E  mov     [ebp-1Ch], esi                  ;p1 += 4;
00401041  mov     esi, [ebp-20h]
00401044  add     esi, 1
00401047  mov     [ebp-20h], esi                  ;p2 += 1;
0040104A  mov     esi, [ebp-24h]
0040104D  add     esi, 2
00401050  mov     [ebp-24h], esi                  ;p3 += 2;
00401053  mov     [ebp-28h], eax
00401056  mov     eax, edx
00401058  mov     [ebp-2Ch], ecx
0040105B  add     esp, 20h
0040105E  pop     esi
0040105F  pop     edi
00401060  pop     ebx
00401061  pop     ebp
00401062  retn

//x64_vs对应汇编代码讲解
0000000140001000  mov     [rsp+10h], rdx          ;保存argv到预留栈空间
0000000140001005  mov     [rsp+8], ecx            ;保存argc到预留栈空间
0000000140001009  sub     rsp, 38h
000000014000100D  mov     rax, cs:__security_cookie
0000000140001014  xor     rax, rsp
0000000140001017  mov     [rsp+20h], rax          ;缓冲区溢出检查代码
000000014000101C  mov     byte ptr [rsp+18h], 1   ;ary[0] = 1
0000000140001021  mov     byte ptr [rsp+19h], 23h ;ary[1] = 0x23
0000000140001026  mov     byte ptr [rsp+1Ah], 45h ;ary[2] = 0x45
000000014000102B  mov     byte ptr [rsp+1Bh], 67h ;ary[3] = 0x67
0000000140001030  mov     byte ptr [rsp+1Ch], 89h ;ary[4] = 0x89
0000000140001035  lea     rax, [rsp+18h]
000000014000103A  mov     [rsp], rax              ;p1 = (int*)ary
000000014000103E  lea     rax, [rsp+18h]
0000000140001043  mov     [rsp+8], rax            ;p2 = (char*)ary
0000000140001048  lea     rax, [rsp+18h]
000000014000104D  mov     [rsp+10h], rax          ;p3 = (short*)ary
0000000140001052  mov     rax, [rsp]
0000000140001056  add     rax, 4
000000014000105A  mov     [rsp], rax              ;p1 += 1
000000014000105E  mov     rax, [rsp+8]
0000000140001063  inc     rax
0000000140001066  mov     [rsp+8], rax            ;p2 += 1
000000014000106B  mov     rax, [rsp+10h]
0000000140001070  add     rax, 2
0000000140001074  mov     [rsp+10h], rax          ;p3 += 1
0000000140001079  xor     eax, eax
000000014000107B  mov     rcx, [rsp+20h]
0000000140001080  xor     rcx, rsp                ; StackCookie
0000000140001083  call    __security_check_cookie ;缓冲区溢出检查代码
0000000140001088  add     rsp, 38h
000000014000108C  retn

//x64_gcc对应汇编代码讲解
0000000000401550  push    rbp
0000000000401551  mov     rbp, rsp
0000000000401554  sub     rsp, 40h
0000000000401558  mov     [rbp+10h], ecx
000000000040155B  mov     [rbp+18h], rdx
000000000040155F  call    __main                  ;调用初始化函数
0000000000401564  mov     byte ptr [rbp-1Dh], 1   ;ary[0] = 1
0000000000401568  mov     byte ptr [rbp-1Ch], 23h ;ary[1] = 0x23
000000000040156C  mov     byte ptr [rbp-1Bh], 45h ;ary[2] = 0x45
0000000000401570  mov     byte ptr [rbp-1Ah], 67h ;ary[3] = 0x67
0000000000401574  mov     byte ptr [rbp-19h], 89h ;ary[4] = 0x89
0000000000401578  lea     rax, [rbp-1Dh]
000000000040157C  mov     [rbp-8], rax            ;p1 = (int*)ary
0000000000401580  lea     rax, [rbp-1Dh]
0000000000401584  mov     [rbp-10h], rax          ;p2 = (char*)ary
0000000000401588  lea     rax, [rbp-1Dh]
000000000040158C  mov     [rbp-18h], rax          ;p3 = (short*)ary
0000000000401590  add     qword ptr [rbp-8], 4    ;p1 += 1
0000000000401595  add     qword ptr [rbp-10h], 1  ;p2 += 1
000000000040159A  add     qword ptr [rbp-18h], 2  ;p3 += 1
000000000040159F  mov     eax, 0
00000000004015A4  add     rsp, 40h
00000000004015A8  pop     rbp
00000000004015A9  retn

//x64_clang对应汇编代码讲解
0000000140001000  sub     rsp, 38h
0000000140001004  xor     eax, eax
0000000140001006  lea     r8, [rsp+1Fh]
000000014000100B  mov     dword ptr [rsp+34h], 0
0000000140001013  mov     [rsp+28h], rdx
0000000140001018  mov     [rsp+24h], ecx
000000014000101C  mov     ecx, cs:dword_14000D2C0
0000000140001022  mov     [rsp+1Fh], ecx          ;ary[]= {1, 0x23, 0x45, 0x67}
0000000140001026  mov     r9b, cs:byte_14000D2C4
000000014000102D  mov     [rsp+23h], r9b          ;ary[4] = 0x89
0000000140001032  mov     rdx, r8
0000000140001035  mov     [rsp+10h], rdx          ;p1 = (int*)ary
000000014000103A  mov     [rsp+8], r8             ;p2 = (char*)ary
000000014000103F  mov     [rsp], r8               ;p3 = (short*)ary
0000000140001043  mov     rdx, [rsp+10h]
0000000140001048  add     rdx, 4
000000014000104C  mov     [rsp+10h], rdx          ;p1 += 1
0000000140001051  mov     rdx, [rsp+8]
0000000140001056  add     rdx, 1
000000014000105A  mov     [rsp+8], rdx            ;p2 += 1
000000014000105F  mov     rdx, [rsp]
0000000140001063  add     rdx, 2
0000000140001067  mov     [rsp], rdx              ;p3 += 1
000000014000106B  add     rsp, 38h
000000014000106F  ret

代码清单2-5演示了对不同类型指针进行加1偏移得到的结果。它们偏移后的地址都是由指针类型决定的,以指针保存的地址作为寻址[首地址],加上[偏移量],最终得到[目标地址]。偏移量的计算方式为指针类型长度乘以移动次数,因此得出指针寻址公式如下所示。

type *p; // 这里用 type 泛指某类型的指针
// 省略指针赋值代码
p+n 的目标地址 = 首地址 + sizeof( 指针类型 type) * n

对于偏移量为负数的情况,此公式同样适用。套用公式,得到的地址值会小于首地址,这时指针是在向后寻址。所以指针可以做减法操作,但乘法与除法对于指针寻址而言是没有意义的。两指针做减法操作是在计算两个地址之间的元素个数,结果为有符号整数,进行减法操作的两指针必须是同类指针。可用于两指针中的地址比较,也可用于其他场合,比如求数组元素个数,其计算公式如下所示。

type *p, *q; // 这里用type泛指某类型的指针
// 省略指针赋值代码
p-q = ((int)p - (int)q) / sizeof(指针类型type)

另外,两指针相加也是没有意义的。将指针访问公式与指针寻址公式结合后,可针对所有类型的指针进行操作。在实际运用中要灵活使用,同时也要谨慎操作,以免将指针指向意料之外的地址,错误地修改地址中的数据,造成程序崩溃。

在代码清单2-5中,指针p1加1后取出的内容就是数组ary以外的数据。ary数组只有5字节数据长度,而p1将访问到数组的第4项,对其取内容后得到的数据是以ary数组的第4项为起始地址的4字节数据。分析出的结果如图2-11所示。

图2-11 字符数组ary的内存信息

2.5.3 引用

引用类型在C++中被描述为变量的别名。C++为了简化操作,对指针的操作进行了封装,产生了引用类型。引用类型实际上就是指针类型,只不过用于存放地址的内存空间对使用者而言是隐藏的。下面通过示例来揭开这个谜底,如代码清单2-6所示。

代码清单2-6 引用类型揭秘

// C++ 源码
#include <stdio.h>
void add(int &ref){
  ref++;
}
int main(int argc, char* argv[]) {
  int n = 0x12345678;
  int &ref = n;
  add(ref);
  return 0;
}

//x86_vs对应汇编代码讲解
00401020  push    ebp
00401021  mov     ebp, esp
00401023  sub     esp, 8
00401026  mov     dword ptr [ebp-4], 12345678h;n=0x12345678
0040102D  lea     eax, [ebp-4]                ;eax=&n
00401030  mov     [ebp-8], eax                ;[ebp-8]存放n的地址,int& ref = n
00401033  mov     ecx, [ebp-8]
00401036  push    ecx                         ;参数1传递n的地址
00401037  call    sub_401000                  ;调用add函数
0040103C  add     esp, 4                      ;平衡栈
0040103F  xor     eax, eax
00401041  mov     esp, ebp
00401043  pop     ebp
00401044  retn

//x86_gcc对应汇编代码讲解
00401523  push    ebp
00401524  mov     ebp, esp
00401526  and     esp, 0FFFFFFF0h             ;对齐栈
00401529  sub     esp, 20h
0040152C  call    ___main                     ;调用初始化函数
00401531  mov     dword ptr [esp+18h], 12345678h;n=0x12345678
00401539  lea     eax, [esp+18h]              ;eax=&n
0040153D  mov     [esp+1Ch], eax              ;[esp+1Ch]存放n的地址,int& ref = n
00401541  mov     eax, [esp+1Ch]
00401545  mov     [esp], eax                  ;参数1传递n的地址int *
00401548  call    __Z3addRi                   ;调用函数add(int &)
0040154D  mov     eax, 0
00401552  leave
00401553  retn

//x86_clang对应汇编代码讲解
00401020  push    ebp
00401021  mov     ebp, esp
00401023  sub     esp, 18h
00401026  mov     eax, [ebp+0Ch]
00401029  mov     ecx, [ebp+8]
0040102C  mov     dword ptr [ebp-4], 0
00401033  mov     dword ptr [ebp-8], 12345678h;n=0x12345678
0040103A  lea     edx, [ebp-8]                ;edx=&n
0040103D  mov     [ebp-0Ch], edx              ;[ebp-0Ch]存放n的地址,int& ref = n
00401040  mov     edx, [ebp-0Ch]
00401043  mov     [esp], edx                  ;参数1传递n的地址
00401046  mov     [ebp-10h], eax
00401049  mov     [ebp-14h], ecx
0040104C  call    sub_401000                  ;调用add函数
00401051  xor     eax, eax
00401053  add     esp, 18h
00401056  pop     ebp
00401057  retn

//x64_vs对应汇编代码讲解
0000000140001020  mov     [rsp+10h], rdx
0000000140001025  mov     [rsp+8], ecx
0000000140001029  sub     rsp, 38h
000000014000102D  mov     dword ptr [rsp+20h], 12345678h;n=0x12345678
0000000140001035  lea     rax, [rsp+20h]      ;rax=&n
000000014000103A  mov     [rsp+28h], rax      ;[rsp+28h]存放n的地址,int& ref = n
000000014000103F  mov     rcx, [rsp+28h]      ;参数1传递n的地址
0000000140001044  call    sub_140001000       ;调用add函数
0000000140001049  xor     eax, eax
000000014000104B  add     rsp, 38h
000000014000104F  retn

//x64_gcc对应汇编代码讲解
000000000040156A  push    rbp
000000000040156B  mov     rbp, rsp
000000000040156E  sub     rsp, 30h
0000000000401572  mov     [rbp+10h], ecx
0000000000401575  mov     [rbp+18h], rdx
0000000000401579  call    __main              ;调用初始化函数
000000000040157E  mov     dword ptr [rbp-0Ch], 12345678h;n=0x12345678
0000000000401585  lea     rax, [rbp-0Ch]      ;rax=&n
0000000000401589  mov     [rbp-8], rax        ;[rbp-8]存放n的地址,int& ref = n
000000000040158D  mov     rax, [rbp-8]
0000000000401591  mov     rcx, rax            ;参数1传递n的地址,int *
0000000000401594  call    _Z3addRi            ;调用函数add(int &)
0000000000401599  mov     eax, 0
000000000040159E  add     rsp, 30h
00000000004015A2  pop     rbp
00000000004015A3  retn

//x64_clang对应汇编代码讲解
0000000140001020  sub     rsp, 48h
0000000140001024  mov     dword ptr [rsp+44h], 0
000000014000102C  mov     [rsp+38h], rdx
0000000140001031  mov     [rsp+34h], ecx
0000000140001035  mov     dword ptr [rsp+30h], 12345678h;n=0x12345678
000000014000103D  lea     rdx, [rsp+30h]      ;rdx=&n
0000000140001042  mov     [rsp+28h], rdx      ;[rsp+28h]存放n的地址,int& ref = n
0000000140001047  mov     rcx, [rsp+28h]      ;参数1传递n的地址
000000014000104C  call    sub_140001000       ;调用add函数
0000000140001051  xor     eax, eax
0000000140001053  add     rsp, 48h
0000000140001057  retn

在图2-10中可以看出,引用类型的存储方式和指针是一样的,都是使用内存空间存放地址值。所以,在C++中,除了引用是通过编译器实现寻址,而指针需要手动寻址外,引用和指针没有太大区别。指针虽然灵活,但如果操作失误将产生严重的后果,而使用引用则不存在这种问题。因此,C++极力提倡使用引用类型,而非指针。

引用类型也可以作为函数的参数类型和返回类型使用。因为引用实际上就是指针,所以它同样会在参数传递时产生一份备份,如代码清单2-7所示。

代码清单2-7 引用类型作为函数参数

//Add函数实现
void add(int &ref){
  ref++;
}

//x86_vs对应汇编代码讲解
00401000  push    ebp
00401001  mov     ebp, esp
00401003  mov     eax, [ebp+8]            ;取出参数ref的内容放入eax
00401006  mov     ecx, [eax]
00401008  add     ecx, 1
0040100B  mov     edx, [ebp+8]
0040100E  mov     [edx], ecx              ;对eax做取内容操作,间接访问实参
00401010  pop     ebp
00401011  retn

//x86_gcc对应汇编代码讲解
00401510  push    ebp
00401511  mov     ebp, esp
00401513  mov     eax, [ebp+8]            ;取出参数ref的内容放入eax
00401516  mov     eax, [eax]
00401518  lea     edx, [eax+1]
0040151B  mov     eax, [ebp+8]
0040151E  mov     [eax], edx              ;对eax做取内容操作,间接访问实参
00401520  nop
00401521  pop     ebp
00401522  retn

//x86_clang对应汇编代码讲解
00401000  push    ebp
00401001  mov     ebp, esp
00401003  push    eax
00401004  mov     eax, [ebp+8]
00401007  mov     ecx, [ebp+8]            ;取出参数ref的内容放入ecx
0040100A  mov     edx, [ecx]
0040100C  add     edx, 1
0040100F  mov     [ecx], edx              ;对ecx做取内容操作,间接访问实参
00401011  mov     [ebp-4], eax
00401014  add     esp, 4
00401017  pop     ebp
00401018  retn

//x64_vs对应汇编代码讲解
0000000140001000  mov     [rsp+8], rcx
0000000140001005  mov     rax, [rsp+8]    ;取出参数ref的内容放入rax
000000014000100A  mov     eax, [rax]
000000014000100C  inc     eax
000000014000100E  mov     rcx, [rsp+8]
0000000140001013  mov     [rcx], eax      ;对rax做取内容操作,间接访问实参
0000000140001015  retn

//x64_gcc对应汇编代码讲解
0000000000401550  push    rbp
0000000000401551  mov     rbp, rsp
0000000000401554  mov     [rbp+10h], rcx
0000000000401558  mov     rax, [rbp+10h]  ;取出参数ref的内容放入rax
000000000040155C  mov     eax, [rax]
000000000040155E  lea     edx, [rax+1]
0000000000401561  mov     rax, [rbp+10h]
0000000000401565  mov     [rax], edx      ;对rax做取内容操作,间接访问实参
0000000000401567  nop
0000000000401568  pop     rbp
0000000000401569  retn

//x64_clang对应汇编代码讲解
0000000140001000  push    rax
0000000140001001  mov     [rsp], rcx      ;保存参数1 ref到局部变量空间
0000000140001005  mov     rcx, [rsp]      ;取出参数ref的内容放入rcx
0000000140001009  mov     eax, [rcx]
000000014000100B  add     eax, 1
000000014000100E  mov     [rcx], eax      ;对rcx做取内容操作,间接访问实参
0000000140001010  pop     rax
0000000140001011  retn

在代码清单2-7中,通过对参数加1的方式修改实参数据。从汇编代码中可以看出,引用类型的参数也占用内存空间,其中保存的数据是一个地址值。取出这个地址中的数据并加1,再将加1后的结果放回地址,如果没有源码对照,指针和引用都一样难以区分。在反汇编下,没有引用这种数据类型。