滴水逆向联盟

标题: 汇编语言的准备知识-给初次接触汇编者(5) [打印本页]

作者: 随便注册    时间: 2014-2-27 12:46
标题: 汇编语言的准备知识-给初次接触汇编者(5)
  3. 数组

  不管是多少维的数组,在内存中总是把所有的元素都连续存放,所以在内存中总是一维的。例如,int i_array[2][3]; 在内存确定了一个地址,从该地址开始的12个字节用来存贮该数组的元素。所以变量名i_array对应着该数组的起始地址,也即是指向数组的第一个元素。存放的顺序一般是i_array[0][0],[0][1],[0][2],[1][0],[1][1],[1][2] 即最右边的下标变化最快。当需要访问某个元素时,程序就会从多维索引值换算成一维索引,如访问i_array[1][1],换算成内存中的一维索引值就是1*3 1=4.这种换算可能在编译的时候就可以确定,也可能要到运行时才可以确定。无论如何,如果我们把i_array对应的地址装入一个通用寄存器作为基址,则对数组元素的访问就是一个计算有效地址的问题:

  ; i_array[1][1]=0x16

  lea ebx,xxxxxxxx ;i_array 对应的地址装入ebx
  mov edx,04 ;访问i_array[1][1],编译时就已经确定
  mov word ptr [ebx edx*2], 16 ;

  当然,取决于不同的编译器和程序上下文,具体实现可能不同,但这种基本的形式是确定的。从这里也可以看到比例因子的作用(还记得比例因子的取值为1,2,4或8吗?),因为在目前的系统中简单变量总是占据1,2,4或者8个字节的长度,所以比例因子的存在为在内存中的查表操作提供了极大方便。

  4. 结构和对象

  结构和对象的成员在内存中也都连续存放,但有时为了在字边界或双字边界对齐,可能有些微调整,所以要确定对象的大小应该用sizeof操作符而不应该把成员的大小相加来计算。当我们声明一个结构变量或初始化一个对象时,这个结构变量和对象的名字也对应一个内存地址。举例说明:

  struct tag_info_struct
  {
  int age;
  int sex;
  float height;
  float weight;
  } marry;

  变量marry就对应一个内存地址。在这个地址开始,有足够多的字节(sizeof(marry))容纳所有的成员。每一个成员则对应一个相对于这个地址的偏移量。这里假设此结构中所有的成员都连续存放,则age的相对地址为0,sex为2, height 为4,weight为8。

  ; marry.sex=0;

  lea ebx,xxxxxxxx ;marry 对应的内存地址
  mov word ptr [ebx 2], 0
  ......

  对象的情况基本相同。注意成员函数具体的实现在代码段中,在对象中存放的是一个指向该函数的指针。
5. 函数调用  

  一个函数在被定义时,也确定一个内存地址对应于函数名字。如:  

  long comb(int m, int n)  
  {  
  long temp;  
  .....  

  return temp;  
  }  

  这样,函数comb就对应一个内存地址。对它的调用表现为:  

  CALL xxxxxxxx ;comb对应的地址。这个函数需要两个整型参数,就通过堆栈来传递:  

  ;lresult=comb(2,3);  

  push 3  
  push 2  
  call xxxxxxxx  
  mov dword ptr [yyyyyyyy], eax ;yyyyyyyy是长整型变量lresult的地址  

  这里请注意两点。第一,在C语言中,参数的压栈顺序是和参数顺序相反的,即后面的参数先压栈,所以先执行push 3. 第二,在我们讨论的32位系统中,如果不指明参数类型,缺省的情况就是压入32位双字。因此,两个push指令总共压入了两个双字,即8个字节的数据。然后执行call指令。call 指令又把返回地址,即下一条指令(mov dword ptr....)的32位地址压入,然后跳转到xxxxxxxx去执行。  

  在comb子程序入口处(xxxxxxxx),堆栈的状态是这样的:  

  03000000 (请回忆small endian 格式)  
  02000000  
  yyyyyyyy <--ESP 指向返回地址  

  前面讲过,子程序的标准起始代码是这样的:  

  push ebp ;保存原先的ebp  
  mov ebp, esp;建立框架指针  
  sub esp, XXX;给临时变量预留空间  
  .....  

  执行push ebp之后,堆栈如下:  

  03000000  
  02000000  
  yyyyyyyy  
  old ebp <---- esp 指向原来的ebp  

  执行mov ebp,esp之后,ebp 和esp 都指向原来的ebp. 然后sub esp, xxx 给临时变量留空间。这里,只有一个临时变量temp,是一个长整数,需要4个字节,所以xxx=4。这样就建立了这个子程序的框架:  

  03000000  
  02000000  
  yyyyyyyy  
  old ebp <---- 当前ebp指向这里  
  temp  

  所以子程序可以用[ebp 8]取得第一参数(m),用[ebp C]来取得第二参数(n),以此类推。临时变量则都在ebp下面,如这里的temp就对应于[ebp-4].  

  子程序执行到最后,要返回temp的值:  

  mov eax,[ebp-04]  
  然后执行相反的操作以撤销框架:  

  mov esp,ebp ;这时esp 和ebp都指向old ebp,临时变量已经被撤销  
  pop ebp ;撤销框架指针,恢复原ebp.  

  这是esp指向返回地址。紧接的retn指令返回主程序:  

  retn 4  

  该指令从堆栈弹出返回地址装入EIP,从而返回到主程序去执行call后面的指令。同时调整esp(esp=esp 4*2),从而撤销参数,使堆栈恢复到调用子程序以前的状态,这就是堆栈的平衡。调用子程序前后总是应该维持堆栈的平衡。从这里也可以看到,临时变量temp已经随着子程序的返回而消失,所以试图返回一个指向临时变量的指针是非法的。  

  为了更好地支持高级语言,INTEL还提供了指令Enter 和Leave 来自动完成框架的建立和撤销。Enter 接受两个操作数,第一个指明给临时变量预留的字节数,第二个是子程序嵌套调用层数,一般都为0。enter xxx,0 相当于:  

  push ebp  
  mov ebp,esp  
  sub esp,xxx  

  leave 则相当于:  

  mov esp,ebp  
  pop ebp  

  =============================================================  

ok 完结,看完相信大家的汇编语言已经有了一定的能力了,祝大家天天进步





欢迎光临 滴水逆向联盟 (http://dtdebug.com/) Powered by Discuz! X3.2