代码改变世界

从简单的算法初探过程汇编

2012-02-08 17:44  捣乱小子  阅读(3348)  评论(10编辑  收藏  举报

不忽视汇编

  较于我们日常接触的高级语言,诸如c语言,c++,java等等,汇编语言是更接近机器的语言,它的常用操作简单到把一个数值(立即数,寄存器数或者存储器数据)加载到寄存器,正是这样,所以让汇编完成一个程序任务,过程会比较晦涩;高级语言隐藏了很多的机器细节(比如过程(函数)栈帧的初始化,以及过程结束时栈帧的恢复),代码清晰易懂。

  真佩服六七十年代那些大牛们,都是怎么过来的...膜拜膜拜。写一个100以内整数的和,即使有充分的汇编文档,这也足够折腾我一阵子,太恶心了。但是了解汇编的行为方式和其中的一些重要细节,有助于理解计算机软件和硬件的工作方式。我就一个简单的算法来认识一下汇编。

过程汇编前奏

  过程可以理解为c中的函数,当调用者(caller)调用被调用者(be caller)的时候,系统会为被调用者在栈内分配空间,这个空间就称为栈帧。栈的结构大概如下:

image

  程序栈是向低地址生长的栈,与数据结构当中的栈结构类似,有后进先出的性质,寄存器%esp(stack pointer)保存栈顶指针的地址,寄存器%ebp(** pointer)保存帧指针的地址。 程序执行的时候,栈指针可以移动,以便增大或者缩小程序栈的空间,而帧指针是固定的,因为大多数程序栈中存储的数据都是相对于帧指针的(帧指针+偏移量)。

当调用者调用另一个过程的时候:

  • 首先,如果这个被调用过程如果有参数的话,调用的栈帧中会构造这些参数,并存入到调用者的栈帧中(所以上面的图参数n...参数1,就是这个原因了);
  • 将返回地址入栈。返回地址是当被调用过程执行完毕之后,调用者应该继续执行的指令地址;它属于调用者栈帧的部分,形成了调用者栈帧的末尾
  • 到这一步就进入了被调用者的栈帧了,所谓当前栈帧。保存调用者的帧指针,以便在之后找回调用者的程序栈;
  • 最后进入程序执行,一般过程会sub 0xNh %esp来分配当前程序栈的大小,用来存取临时变量啊,暂存寄存器的值啊等等。
  • 如果被调用者又要调用另一个过程,回到第一步即可;
  • 当过程结束之时,会将栈指针,帧指针恢复,经常会在反汇编中看到如下:
    mov         %esp,%ebp  
    pop         %ebp 
    同时,返回地址会被恢复到PC。
  • 这时回到了打调用者应该继续执行的地方。

  上面的文字可以更概括,反汇编一个过程(函数)会有建立(初始化),主体(执行),结束(返回)。之前很容易把栈和堆搞混(不是数据结构里面),找到一个好文章与大家分享:栈和堆的区别。据说被转了无数次了,说明写的不错。 过程调用和返回在汇编语言中分别用call和ret(return)来实现。call和ret的做法不是很透明,

  • call将返回地址入栈,并将PC跳转到被调用过程的起始地址;
  • ret与call相反,从栈中弹出返回地址,并跳转PC。 

具体看图:

image

关于汇编代码格式

  汇编代码最为常见的是ATT和intel汇编代码格式,ATT应该较为古老,但却是GCC,OBJDUMP的默认格式。需要注意的是在带有多个操作数的指令的情况下,列出操作数顺序两者是相反的,所以在思路上很容易混淆。例如实现%esp→%eax,有如下区别。

#intel 
mov eax,esp 
#ATT 
movl %esp,%eax

因为受到书本的影响,所以我习惯在寄存器前加上“%”,并且我更偏好ATT格式的汇编代码。

反汇编具体分析

(下面的程序栈图,我把参数入栈我在标明“参数i=?”,这可能会有点疑惑,如果“参数x=?”这样会更好,:))

  有一个简单程序,先不管它实现了什么功能,看下去,绝对会有收获的。给出的c代码是:

View Code
#include <iostream> 
using namespace std; 

int fun(unsigned int x) 

    if(x == 0
        return 0; 
    unsigned int nx = x>>1; 
    int rv = fun(nx); 
    return (x & 0x01)+rv; 


int main() 

    unsigned int i = 12; 
    fun(i); 
    return 0; 
}

在vs2008下debug查看汇编代码有如下反汇编代码,因为晦涩,所以摘抄了如下:

View Code
004110E6  jmp         fun (4113A0h)  

int fun(unsigned int x) 

004113A0  push        ebp   
004113A1  mov         ebp,esp  
004113A3  sub         esp,0D8h  
004113A9  push        ebx   
004113AA  push        esi   
004113AB  push        edi   
004113AC  lea         edi,[ebp-0D8h]  
004113B2  mov         ecx,36h  
004113B7  mov         eax,0CCCCCCCCh  
004113BC  rep stos    dword ptr es:[edi]  
    if(x == 0
004113BE  cmp         dword ptr [x],0  
004113C2  jne         fun+28h (4113C8h)  
        return 0; 
004113C4  xor         eax,eax  
004113C6  jmp         fun+48h (4113E8h)  
    unsigned int nx = x>>1; 
004113C8  mov         eax,dword ptr [x]  
004113CB  shr         eax,1  
004113CD  mov         dword ptr [nx],eax  
    int rv = fun(nx); 
004113D0  mov         eax,dword ptr [nx]  
004113D3  push        eax   
004113D4  call        fun (4110E6h)  
004113D9  add         esp,4  
004113DC  mov         dword ptr [rv],eax  
    return (x & 0x01)+rv; 
004113DF  mov         eax,dword ptr [x]  
004113E2  and         eax,1  
004113E5  add         eax,dword ptr [rv]  

004113E8  pop         edi   
004113E9  pop         esi   
004113EA  pop         ebx   
004113EB  add         esp,0D8h  
004113F1  cmp         ebp,esp  
004113F3  call        @ILT+315(__RTC_CheckEsp) (411140h)  
004113F8  mov         esp,ebp  
004113FA  pop         ebp   
004113FB  ret               

int main() 

00411420  push        ebp   
00411421  mov         ebp,esp  
00411423  sub         esp,0CCh  
00411429  push        ebx   
0041142A  push        esi   
0041142B  push        edi   
0041142C  lea         edi,[ebp-0CCh]  
00411432  mov         ecx,33h  
00411437  mov         eax,0CCCCCCCCh  
0041143C  rep stos    dword ptr es:[edi]  
    unsigned int i = 12; 
0041143E  mov         dword ptr [i],0Ch  
    fun(i); 
00411445  mov         eax,dword ptr [i]  
00411448  push        eax   
00411449  call        fun (4110E6h)  
0041144E  add         esp,4  
    return 0; 
00411451  xor         eax,eax  

00411453  pop         edi   
00411454  pop         esi   
00411455  pop         ebx   
00411456  add         esp,0CCh  
0041145C  cmp         ebp,esp  
0041145E  call        @ILT+315(__RTC_CheckEsp) (411140h)  
00411463  mov         esp,ebp  
00411465  pop         ebp   
00411466  ret             

上面的代码,在第一句就间接道明了fun的地址。可以看到在call  fun之前会有一段准备:

View Code
    fun(i); 
00411445  mov         eax,dword ptr [i]  
00411448  push        eax   
00411449  call        fun (4110E6h)  
0041144E  add         esp,4 

00411445h的指令就将fun的参数(此时i=6,还记得上面的图吗,参数n-参数1)和返回地址入栈,然后PC跳至004110E6h,此时main的栈帧如下:

image

借助jmp跳至004113A0h,正式进入fun函数。fun内首先保存了帧指针和被调用者保存寄存器和其他相关数据,只有当参数x==0的时候才会终止函数的运行,故在递归调用(注意,是递归调用,而不是调用)fun之前(即call fun之前),有如下:

image

所以,一直递归下去的话:

image

直到x==0,此时会进入if的分支执行步骤。

View Code
    if(x == 0
004113BE  cmp         dword ptr [x],0  
004113C2  jne         fun+28h (4113C8h)  
        return 0; 
004113C4  xor         eax,eax  
004113C6  jmp         fun+48h (4113E8h)

在汇编中,会用到异或xor逻辑运算来对一个寄存器清零(004113C4h地址的指令),由于x==0,PC跳至004113E8h,执行返回。

View Code
004113E8  pop         edi   
004113E9  pop         esi   
004113EA  pop         ebx   
004113EB  add         esp,0D8h  
004113F1  cmp         ebp,esp  
004113F3  call        @ILT+315(__RTC_CheckEsp) (411140h)  
004113F8  mov         esp,ebp  
004113FA  pop         ebp   
004113FB  ret             

在这里把被保存的寄存器值都弹出来,恢复栈归位,留意其中针对%esp和%ebp的操作;执行ret操作,返回,

image

程序继续执行:

View Code
#    int rv = fun(nx); 
#004113D0  mov         eax,dword ptr [nx]  
#004113D3  push        eax   
#004113D4  call        fun (4110E6h)  
004113D9  add         esp,4  
004113DC  mov         dword ptr [rv],eax

rv = 0;

  可以看到,处理器释放了栈上的内存(%esp+4,还记得吗,栈是向低地址增长的),因为在call之前,也就是00411448h地址处,调用者也就是main函数将%eax参数入栈,接着fun退出之后,参数的内存也就理所当然的要释放掉。联想一下,如果参数有很多个,那么call之前就会有多个push,对应的,call之后就会有“add %esp n”的操作将其释放。接着将%eax(在寄存器是用习惯当中,%eax经常被用作返回值寄存器)的值给了rv,如此一来rv就顺理成章地得到了fun的返回值。接下来:

View Code
    return (x & 0x01)+rv; 
004113DF  mov         eax,dword ptr [x]  
004113E2  and         eax,1  
004113E5  add         eax,dword ptr [rv]

%eax←(x&0x01)+rv = 0x01&0x01 + 0 = 1;(提示:从这里开始体会fun的功能)

  简单的将x&0x01+rv后送入%eax(记得吗,%eax经常被用作返回值寄存器),此时可能会有疑问,x是从哪里来的,答案是x存在调用者的栈帧内,而非被调用者的栈帧,因为x是函数的一个参数,dword ptr [x]应该就是对读取了调用者栈帧中的x参数。该是恢复栈的时候了:

View Code
004113E8  pop         edi   
004113E9  pop         esi   
004113EA  pop         ebx   
004113EB  add         esp,0D8h  
004113F1  cmp         ebp,esp  
004113F3  call        @ILT+315(__RTC_CheckEsp) (411140h)  
004113F8  mov         esp,ebp  
004113FA  pop         ebp   
004113FB  ret  

恢复栈帧,执行ret,如图:

image

 

fun又成功返回了,程序继续:

View Code
#    int rv = fun(nx); 
#004113D0  mov         eax,dword ptr [nx]  
#004113D3  push        eax   
#004113D4  call        fun (4110E6h)  
004113D9  add         esp,4  
004113DC  mov         dword ptr [rv],eax

rv = %eax = 1;

又回到了刚才走过的地方,但是数据有异。接下来程序执行return退出:

View Code
    return (x & 0x01)+rv; 
004113DF  mov         eax,dword ptr [x]  
004113E2  and         eax,1  
004113E5  add         eax,dword ptr [rv]

%eax←(x&0x01)+rv = 0x3&0x01 + 1 = 2;又该是ret的时候了,恢复栈:

View Code
004113E8  pop         edi   
004113E9  pop         esi   
004113EA  pop         ebx   
004113EB  add         esp,0D8h  
004113F1  cmp         ebp,esp  
004113F3  call        @ILT+315(__RTC_CheckEsp) (411140h)  
004113F8  mov         esp,ebp  
004113FA  pop         ebp   
004113FB  ret  

栈帧结构如图:

image

还差一次,返回之后程序继续执行:

View Code
#    int rv = fun(nx); 
#004113D0  mov         eax,dword ptr [nx]  
#004113D3  push        eax   
#004113D4  call        fun (4110E6h)  
004113D9  add         esp,4  
004113DC  mov         dword ptr [rv],eax

rv = %eax = 2;

接下来程序return退出(不累赘了):

View Code
return (x & 0x01)+rv; 
004113DF  mov         eax,dword ptr [x]  
004113E2  and         eax,1  
004113E5  add         eax,dword ptr [rv]  
004113E8  pop         edi   
004113E9  pop         esi   
004113EA  pop         ebx   
004113EB  add         esp,0D8h  
004113F1  cmp         ebp,esp  
004113F3  call        @ILT+315(__RTC_CheckEsp) (411140h)  
004113F8  mov         esp,ebp  
004113FA  pop         ebp   
004113FB  ret  

至此,程序完全退出了fun的递归过程,回到了主函数main,main也有自己的栈帧,因为main也是一个函数。下图:

image 

View Code
#    fun(i); 
#00411445  mov         eax,dword ptr [i]  
#00411448  push        eax   
#00411449  call        fun (4110E6h)  
0041144E  add         esp,4  
    return 0; 
00411451  xor         eax,eax

0x0041144E处,add %esp,4,目的是释放一开始入栈的fun的参数,而主函数返回0(return 0),也是用到了异或逻辑运算xor来讲%eax清零。

到这里,相信有点明白了,在递归调用过程中,程序栈是如何变化的,并且上面的函数计算参数i中位的和。

收获

  发现这样一个小小的递归程序,分析起它反汇编如有一种返璞归真的感觉,对理解“递归调用”会更为清晰的思路。纵观上面的分析,递归调用虽然是算法中解决问题常用的方法,但是它对付起庞大递归次数的程序来说(上面因为分析所以选取的递归次数较少),非常消耗内存。 所以在写程序的时候,在时间和空间的消耗抉择上,需要谨慎。通过学习汇编和反汇编代码的分析,将更了解机器的行为,从而写出更为高效的代码。

 

文章有点长,欢迎讨论。

捣乱小子 2012年2月8日星期三