通俗来讲,在内存中一块连续存储数据的叫数组,数组的每个子元素的宽度都一样,并且只能为通用的数据类型做单位(char,short,int等等)
让我们先定义一个数组,然后赋值:
char arr1[2] = { 0 };
arr1[0] = 1;
arr1[1] = 2;
arr1[2] = 3;
让我们先见识一下反汇编
char arr1[3] = { 0 };
013D4DC2 33 C0 xor eax,eax
013D4DC4 66 89 45 F4 mov word ptr [arr1],ax
013D4DC8 88 45 F6 mov byte ptr [ebp-0Ah],al
arr1[0] = 10;
013D4DCB B8 01 00 00 00 mov eax,1
013D4DD0 6B C8 00 imul ecx,eax,0
013D4DD3 C6 44 0D F4 0A mov byte ptr arr1[ecx],0Ah
arr1[1] = 20;
013D4DD8 B8 01 00 00 00 mov eax,1
013D4DDD C1 E0 00 shl eax,0
013D4DE0 C6 44 05 F4 14 mov byte ptr arr1[eax],14h
arr1[2] = 30;
013D4DE5 B8 01 00 00 00 mov eax,1
013D4DEA D1 E0 shl eax,1
013D4DEC C6 44 05 F4 1E mov byte ptr arr1[eax],1Eh
一个小小的数组,为什么变成了这么多东西??(vs2017下)
先别慌,我们来一条一条的解析指令!
xor eax,eax //清除eax寄存器的数据
--eax-ax-al 0x00000000
mov word ptr [arr1],ax //根据数组的宽度来讲,我们要存储两个数组,所以第一次先使用ax寄存器(0x0000)
-- cc cc cc -> 00 00 cc
mov byte ptr [ebp-0Ah],al //ax寄存器+al(0x00),这样我们就使用2+1的宽度填充了3 的宽度的0
-- 00 00 cc -> 00 00 00
//第一次把cccccc变成0000cc,第二次把0000cc变成000000,完成填充0,初始化数组
mov eax,1
imul ecx,eax,0
mov byte ptr arr1[ecx],0Ah
//这个指令看起来有点复杂,继续解析
//-把1放到eax寄存器里,imul是什么?我们先观察一下
影响 OF、CF 标志位
第一种指令格式:IMUL r/m ;单操作数;
如果参数是 r8/m8, 将把 AL 做乘数, 结果放在 AX;
如果参数是 r16/m16, 将把 AX 做乘数, 结果放在 EAX;
如果参数是 r32/m32, 将把 EAX 做乘数, 结果放在 EDX:EAX
看了半天也没看懂,反正imul ecx,eax,0,乘来乘去最后乘了0,就一直是0
再观察一下shl指令,逻辑左移指令,通过左移来计算偏移!
后面你就会知道为什么需要通过imul和shl指令来计算eax或者ecx的值
但是这条指令为什么看起来怪怪的?
mov byte ptr arr1[ecx],0Ah //ecx为0
不要紧,这是编译器的锅,反汇编的显示就是这样,让我们换一个软件来显示这些opcode
C6 44 0D F4 0A mov byte ptr ss:[ebp+ecx-C],0a //ebp+0-c,其实就是ebp-c的地方存了第一个
C6 44 05 F4 14 mov byte ptr ss:[ebp+ecx-C],14 //ebp+1-c
让我来展示一张堆栈结构图
比如我们的ebp是401020的时候,第一行操作数组的汇编代码的意思就是:
mov byte ptr ss:[ebp+ecx-C],0a
401020的ebp+0-c。其实就是401014。在401014的地方存储10
401020的ebp+1-c。其实就是401015。在401015的地方存储20
...
再看看全局数组
arr1[0] = 10;
00204DB8 B8 01 00 00 00 mov eax,1
00204DBD 6B C8 00 imul ecx,eax,0
00204DC0 C6 81 48 A1 20 00 0A mov byte ptr arr1 (020A148h)[ecx],0Ah
arr1[1] = 20;
00204DC7 B8 01 00 00 00 mov eax,1
00204DCC C1 E0 00 shl eax,0
00204DCF C6 80 48 A1 20 00 14 mov byte ptr arr1 (020A148h)[eax],14h
arr1[2] = 30;
00204DD6 B8 01 00 00 00 mov eax,1
00204DDB D1 E0 shl eax,1
00204DDD C6 80 48 A1 20 00 1E mov byte ptr arr1 (020A148h)[eax],1Eh
我们的数组被存储在基址0x20A148h的位置,再看到imul和shl指令的时候,我们已经恍然大悟
mov word ptr ds:[0x20a148+eax],14//eax=0
mov word ptr ds:[0x20a148+eax],14//eax=1
和我们的堆栈操作何其相似?
最后对arr[10]等大的数组进行拆分发现:
ebp+eax(0)-14 | ebp+eax(1)-14 | ebp+eax(2)-14 | ebp+eax(3)-14 |
ebp+eax(0)-c | ebp+eax(1)-10 | ebp+eax(2)-10 | ebp+eax(3)-10 |
ebp+eax(0)-c | ebp+eax(1)-c | ebp+eax(2)-c | ebp+eax(3)-c |
…
先上代码再解析:
#include "pch.h"
#include
#include
void HelloWord()
{
printf("Hello World");
getchar();
}
void Fun()
{
int arr[5] = { 1,2,3,4,5 };
arr[7] = (int)HelloWord;
}
int main()
{
Fun();
return 0;
}
void Fun()
{
01334630 55 push ebp
01334631 8B EC mov ebp,esp
01334633 81 EC DC 00 00 00 sub esp,0DCh
01334639 53 push ebx
0133463A 56 push esi
0133463B 57 push edi
0133463C 8D BD 24 FF FF FF lea edi,[ebp-0DCh]
01334642 B9 37 00 00 00 mov ecx,37h
01334647 B8 CC CC CC CC mov eax,0CCCCCCCCh
0133464C F3 AB rep stos dword ptr es:[edi]
0133464E B9 08 C0 33 01 mov ecx,offset _0340B6FD_consoleapplication3.cpp (0133C008h)
01334653 E8 B0 CB FF FF call @__CheckForDebuggerJustMyCode@4 (01331208h)
int arr[5] = { 1,2,3,4,5 };
01334658 C7 45 E8 01 00 00 00 mov dword ptr [arr],1
0133465F C7 45 EC 02 00 00 00 mov dword ptr [ebp-14h],2
01334666 C7 45 F0 03 00 00 00 mov dword ptr [ebp-10h],3
0133466D C7 45 F4 04 00 00 00 mov dword ptr [ebp-0Ch],4
01334674 C7 45 F8 05 00 00 00 mov dword ptr [ebp-8],5
arr[7] = (int)HelloWord;
0133467B B8 04 00 00 00 mov eax,4
01334680 6B C8 07 imul ecx,eax,7
01334683 C7 44 0D E8 A2 13 33 01 mov dword ptr arr[ecx],offset HelloWord (013313A2h)
}
让我们来看一看堆栈图:
当我们在使用arr[7]的时候,实际上就是写入了ebp+4,众所周知。ebp+4储存返回地址,而当我们使用arr[7]的时候,实际上就是修改了整个函数的返回地址!,如果我们使用arr[6]即可读取ebp栈底的值!