数组的负数索引值完全不阻碍寻址。例如,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
现在,栈内地址与变量值的对应关系是:
数组fakearray[]的地址是0x001dfbd3,它比数组array[]的地址0x001dfbd4落后了一个字节 (栈是逆向增长的存储结构)。
虽然这确实是某种意义上的hack,但是它确实不很靠谱(编译结果可能和预期相差甚远)。因此,本书不建议在生产环境下使用这种代码。即使如此,本例仍然不失为典型的演示程序。