第52章 数组与负数索引

数组的负数索引值完全不阻碍寻址。例如,array[-1]实际上表示数组array起始地址之前的存储空间!

这种技术的用途相当有限。笔者认为除了本章范例的这种场景之外,应该没有什么领域用得上这项技术了。众所周知,在表示数组的第一个元素时,C/C++使用的数组下标是0,而部分其他编程语言(FORTRAN等)使用的数组下标可能是1。在移植代码的时候,可能会忽视这种问题。此时借助负数索引值就可以用查找C/C++数组中的第一个元素。

#include <stdio.h>

int main()
{
        int random_value=0x11223344;
        unsigned char array[10];
        int i;
        unsigned char *fakearray=&array[-1];

        for (i=0; i<10; i++)
                array[i]=i;

        printf ("first element %d\n", fakearray[1]);
        printf ("second element %d\n", fakearray[2]);
        printf ("last element %d\n", fakearray[10]);

        printf ("array[-1]=%02X, array[-2]=%02X, array[-3]=%02X, array[-4]=%02X\n",
                array[-1],
                array[-2],
                array[-3],
                array[-4]);
};

指令清单52.1 非优化的MSVC 2010下的程序

 1  $SG2751 DB      'first element %d', 0aH, 00H
 2  $SG2752 DB      'second element %d', 0aH, 00H
 3  $SG2753 DB      'last element %d', 0aH, 00H
 4  $SG2754 DB      'array[-1]=%02X, array[-2]=%02X, array[-3]=%02X, array[-4'
 5          DB      ']=%02X', 0aH, 00H
 6
 7  _fakearray$ = -24      ; size = 4
 8  _random_value$ = -20   ; size = 4
 9  _array$ = -16          ; size = 10
10  _i$ = -4               ; size = 4
11  _main  PROC
12         push      ebp
13         mov       ebp, esp
14         sub       esp, 24
15         mov       DWORD PTR _random_value$[ebp], 287454020 ; 11223344H
16         ; set fakearray[] one byte earlier before array[]
17         lea       eax, DWORD PTR _array$[ebp]
18         add       eax, -1 ; eax=eax-1
19         mov       DWORD PTR _fakearray$[ebp], eax
20         mov       DWORD PTR _i$[ebp], 0
21         jmp       SHORT $LN3@main
22         ; fill array[] with 0..9
23  $LN2@main:
24         mov       ecx, DWORD PTR _i$[ebp]
25         add       ecx, 1
26         mov       DWORD PTR _i$[ebp], ecx
27  $LN3@main:
28         cmp       DWORD PTR _i$[ebp], 10
29         jge       SHORT $LN1@main
30         mov       edx, DWORD PTR _i$[ebp]
31         mov       al, BYTE PTR _i$[ebp]
32         mov       BYTE PTR _array$[ebp+edx], al
33         jmp       SHORT $LN2@main
34  $LN1@main:
35         mov       ecx, DWORD PTR _fakearray$[ebp]
36         ; ecx=address of fakearray[0], ecx+1 is fakearray[1] or array[0]
37         movzx     edx, BYTE PTR [ecx+1]
38         push      edx
39         push      OFFSET $SG2751 ; 'first element %d'
40         call      _printf
41         add       esp, 8
42         mov       eax, DWORD PTR _fakearray$[ebp]
43         ; eax=address of fakearray[0], eax+2 is fakearray[2] or array[1]
44         movzx     ecx, BYTE PTR [eax+2]
45         push      ecx
46         push      OFFSET $SG2752 ; 'second element %d'
47         call      _printf
48         add       esp, 8
49         mov       edx, DWORD PTR _fakearray$[ebp]
50         ; edx=address of fakearray[0], edx+10 is fakearray[10] or array[9]
51         movzx     eax, BYTE PTR [edx+10]
52         push      eax
53         push      OFFSET $SG2753 ; 'last element %d'
54         call      _printf
55         add       esp, 8
56         ; subtract 4, 3, 2 and 1 from pointer to array[0] in order to find values before array[]
57         lea       ecx, DWORD PTR _array$[ebp]
58         movzx     edx, BYTE PTR [ecx-4]
59         push      edx
60         lea       eax, DWORD PTR _array$[ebp]
61         movzx     ecx, BYTE PTR [eax-3]
62         push      ecx
63         lea       edx, DWORD PTR _array$[ebp]
64         movzx     eax, BYTE PTR [edx-2]
65         push      eax
66         lea       ecx, DWORD PTR _array$[ebp]
67         movzx     edx, BYTE PTR [ecx-1]
68         push      edx
69         push      OFFSET $SG2754 ; 'array[-1]=%02X, array[-2]=%02X, array[-3]=%02X, array[-4]=%02 X'
70         call      _printf
71         add       esp, 20
72         xor       eax, eax
73         mov       esp, ebp
74         pop       ebp
75         ret       0
76 _main   ENDP

数组array[]有10个字节型数据元素,它们的值依次是0到9。我们还构造数组fakearray[]和相应的指针,使fakearrary的数组指针地址比array[]数组的地址提前一个字节,确保fakearray[1]的地址和array[0]对齐。但是我们还是很好奇,在array[0]之前的负值索引元素的地址到底是什么。为此,我们在这里在array[]数组的地址之存储了一个双字常数0x11223344。只要不进行优化编译,编译器就会按照变量声明的顺序来分配其存储空间。因此,我们可以在编译后的可执行文件里验证这个存储关系:双字常数random_value正好排列于array[]数组之前。

运行这个刚编译出来的可执行文件,可以看到程序运行的结果是:

first element 0
second element 1
last element 9
array[-1]=11, array[-2]=22, array[-3]=33, array[-4]=44

请注意x86平台的小端字节序现象。

在调试工具OllyDbg的栈窗口里,我们可以看到栈内数据的排布如下:

指令清单52.2 非优化的MSVC2010编译结果

CPU Stack
Address    Value
001DFBCC   /001DFBD3 ; fakearray pointer
001DFBD0   |11223344 ; random_value
001DFBD4   |03020100 ; 4 bytes of array[]
001DFBD8   |07060504 ; 4 bytes of array[]
001DFBDC   |00CB0908 ; random garbage + 2 last bytes of array[]
001DFBE0   |0000000A ; last i value after loop was finished
001DFBE4   |001DFC2C ; saved EBP value
001DFBE8   \00CB129D ; Return Address

现在,栈内地址与变量值的对应关系是:

虽然这确实是某种意义上的hack,但是它确实不很靠谱(编译结果可能和预期相差甚远)。因此,本书不建议在生产环境下使用这种代码。即使如此,本例仍然不失为典型的演示程序。