从简单的算法初探汇编语言

开发 开发工具 算法
汇编语言是更接近机器的语言,它的常用操作简单到把一个数值(立即数,寄存器数或者存储器数据)加载到寄存器。我们今天就要从简单的算法谈起,看看什么是汇编语言。

  不忽视汇编

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

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

  过程汇编前奏

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

  程序栈是向低地址生长的栈,与数据结构当中的栈结构类似,有后进先出的性质,寄存器%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。

  具体看图:

关于汇编代码格式

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

  1.   #intel  
  2.   moveax,esp  
  3.   #ATT  
  4.   movl %esp,%eax 

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

  反汇编具体分析

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

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

  1.   #include <iostream>  
  2.   using namespace std  
  3.   intfun(unsigned intx)  
  4.   {  
  5.   if(x == 0)  
  6.   return 0  
  7.   unsigned intnx = x>>1  
  8.   intrv = fun(nx)  
  9.   return (x &0x01)+rv  
  10.   }  
  11.   intmain()  
  12.   {  
  13.   unsigned inti = 100  
  14.   fun(i)  
  15.   return 0  
  16.   } 

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

  1.   004110E6 jmp fun (4113A0h)  
  2.   intfun(unsigned intx)  
  3.  {  
  4.   004113A0 push ebp  
  5.   004113A1 mov ebp,esp  
  6.   004113A3 sub esp,0D8h  
  7.   004113A9 push ebx  
  8.   004113AA push esi  
  9.   004113AB push edi  
  10.   004113AC lea edi,[ebp-0D8h]  
  11.   004113B2 mov ecx,36h  
  12.   004113B7 mov eax,0CCCCCCCCh  
  13.   004113BC repstos dword ptr es:[edi]  
  14.   if(x == 0)  
  15.   004113BE cmp dword ptr [x],0  
  16.   004113C2 jne fun+28h (4113C8h)  
  17.   return 0  
  18.   004113C4 xor eax,eax  
  19.   004113C6 jmp fun+48h (4113E8h)  
  20.   unsigned intnx = x>>1  
  21.   004113C8 mov eax,dword ptr [x]  
  22.   004113CB shr eax,1  
  23.   004113CD mov dword ptr [nx],eax  
  24.   intrv = fun(nx)  
  25.   004113D0 mov eax,dword ptr [nx]  
  26.   004113D3 push eax  
  27.   004113D4 call fun (4110E6h)  
  28.   004113D9 add esp,4  
  29.   004113DC mov dword ptr [rv],eax  
  30.   return (x &0x01)+rv  
  31.   004113DF mov eax,dword ptr [x]  
  32.   004113E2 and eax,1  
  33.   004113E5 add eax,dword ptr [rv]  
  34.   }  
  35.   004113E8 pop edi  
  36.   004113E9 pop esi  
  37.   004113EA pop ebx  
  38.   004113EB add esp,0D8h  
  39.   004113F1 cmp ebp,esp  
  40.   004113F3 call @ILT+315(__RTC_CheckEsp) (411140h)  
  41.   004113F8 mov esp,ebp  
  42.   004113FA pop ebp  
  43.   004113FB ret  
  44.   intmain()  
  45.   {  
  46.   00411420 push ebp  
  47.   00411421 mov ebp,esp  
  48.   00411423 sub esp,0CCh  
  49.   00411429 push ebx  
  50.   0041142A push esi  
  51.   0041142B push edi  
  52.   0041142C lea edi,[ebp-0CCh]  
  53.   00411432 mov ecx,33h  
  54.   00411437 mov eax,0CCCCCCCCh  
  55.   0041143C repstos dword ptr es:[edi]  
  56.   unsigned inti = 12  
  57.   0041143E mov dword ptr [i],0Ch  
  58.   fun(i)  
  59.   00411445 mov eax,dword ptr [i]  
  60.   00411448 push eax  
  61.   00411449 call fun (4110E6h)  
  62.   0041144E add esp,4  
  63.   return 0  
  64.   00411451 xor eax,eax  
  65.   }  
  66.   00411453 pop edi  
  67.   00411454 pop esi  
  68.   00411455 pop ebx  
  69.   00411456 add esp,0CCh  
  70.   0041145C cmp ebp,esp  
  71.   0041145E call @ILT+315(__RTC_CheckEsp) (411140h)  
  72.   00411463 mov esp,ebp  
  73.   00411465 pop ebp  
  74.  00411466 ret 

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

  1.   fun(i)  
  2.   00411445 mov eax,dword ptr [i]  
  3.   00411448 push eax  
  4.   00411449 call fun (4110E6h)  
  5.   0041144E add esp,4 

 

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

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

  图

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

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

  1.   if(x == 0)  
  2.   004113BE cmp dword ptr [x],0  
  3.   004113C2 jne fun+28h (4113C8h)  
  4.   return 0  
  5.   004113C4 xor eax,eax  
  6.   004113C6 jmp fun+48h (4113E8h) 

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

  1.   004113E8 pop edi  
  2.   004113E9 pop esi  
  3.   004113EA pop ebx  
  4.   004113EB add esp,0D8h  
  5.   004113F1 cmp ebp,esp  
  6.   004113F3 call @ILT+315(__RTC_CheckEsp) (411140h)  
  7.   004113F8 mov esp,ebp  
  8.   004113FA pop ebp  
  9.   004113FB ret 

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

程序继续执行:

  1.   # intrv = fun(nx)  
  2.   #004113D0 mov eax,dword ptr [nx]  
  3.   #004113D3 push eax  
  4.   #004113D4 call fun (4110E6h)  
  5.   004113D9 add esp,4  
  6.   004113DC mov dword ptr [rv],eax  
  7.   rv = 0; 

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

  1.   return (x &0x01)+rv  
  2.   004113DF mov eax,dword ptr [x]  
  3.   004113E2 and eax,1  
  4.   004113E5 add eax,dword ptr [rv]  
  5.   %eax←(x&0x01)+rv = 0x01&0x01 + 0 = 1;(提示:从这里开始体会fun的功能) 

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

  1.   004113E8 pop edi  
  2.   004113E9 pop esi  
  3.   004113EA pop ebx  
  4.   004113EB add esp,0D8h  
  5.   004113F1 cmp ebp,esp  
  6.   004113F3 call @ILT+315(__RTC_CheckEsp) (411140h)  
  7.   004113F8 mov esp,ebp  
  8.   004113FA pop ebp  
  9.   004113FB ret 

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

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

  1.   # intrv = fun(nx)  
  2.   #004113D0 mov eax,dword ptr [nx]  
  3.   #004113D3 push eax  
  4.   #004113D4 call fun (4110E6h)  
  5.   004113D9 add esp,4  
  6.   004113DC mov dword ptr [rv],eax  
  7.   rv = %eax = 1; 

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

  1.   return (x &0x01)+rv  
  2.   004113DF mov eax,dword ptr [x]  
  3.   004113E2 and eax,1  
  4.   004113E5 add eax,dword ptr [rv]  
  5.   %eax←(x&0x01)+rv = 0x3&0x01 + 1 = 2;又该是ret的时候了,恢复栈:  
  6.   004113E8 pop edi  
  7.   004113E9 pop esi  
  8.   004113EA pop ebx  
  9.   004113EB add esp,0D8h  
  10.   004113F1 cmp ebp,esp  
  11.   004113F3 call @ILT+315(__RTC_CheckEsp) (411140h)  
  12.   004113F8 mov esp,ebp  
  13.   004113FA pop ebp  
  14.   004113FB ret 

   栈帧结构如图:

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

  1.   # intrv = fun(nx)  
  2.   #004113D0 mov eax,dword ptr [nx]  
  3.   #004113D3 push eax  
  4.   #004113D4 call fun (4110E6h)  
  5.   004113D9 add esp,4  
  6.   004113DC mov dword ptr [rv],eax  
  7.   rv = %eax = 2; 

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

  1.   return (x &0x01)+rv  
  2.   004113DF mov eax,dword ptr [x]  
  3.   004113E2 and eax,1  
  4.   004113E5 add eax,dword ptr [rv]  
  5.   004113E8 pop edi  
  6.   004113E9 pop esi  
  7.   004113EA pop ebx  
  8.   004113EB add esp,0D8h  
  9.   004113F1 cmp ebp,esp  
  10.   004113F3 call @ILT+315(__RTC_CheckEsp) (411140h)  
  11.   004113F8 mov esp,ebp  
  12.   004113FA pop ebp  
  13.   004113FB ret 

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

  1.   # fun(i)  
  2.   #00411445 mov eax,dword ptr [i]  
  3.   #00411448 push eax  
  4.   #00411449 call fun (4110E6h)  
  5.   0041144E add esp,4  
  6.   return 0  
  7.   00411451 xor eax,eax 

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

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

  收获

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

  文章有点长,欢迎讨论。

原文链接:http://www.cnblogs.com/daoluanxiaozi/archive/2012/02/08/2340530.html

【编辑推荐】

  1. 汇编语言:从机器语言到高级语言的进化
  2. 详解汇编语言开发环境搭建方法
  3. JavaScript是Web的汇编语言:疯狂,亦或只是精神错乱?
  4. 向Brendan致敬-那段华丽的JavaScript历史
  5. 一位反JavaScript主义者的觉醒
责任编辑:彭凡 来源: 博客园
相关推荐

2011-01-14 14:08:17

Linux汇编语言

2011-01-14 14:39:32

Linux汇编语言

2011-01-14 14:15:11

Linux汇编语言

2011-01-04 17:08:10

汇编语言

2011-01-14 14:22:50

Linux汇编语言

2021-06-11 10:02:39

语言编程开发

2010-11-09 09:51:52

汇编语言

2018-01-11 14:58:40

2011-01-14 13:44:45

Linux汇编语言

2023-11-23 08:25:40

开发人员SmaliAndroid

2021-04-27 07:59:11

内联汇编 C 语言 asm 关键字

2011-07-21 09:59:26

JavaScript

2017-01-12 22:36:30

2023-06-01 16:27:34

汇编语言函数

2009-02-27 08:45:27

Unix入门

2013-05-13 11:25:02

WAFWeb应用防火墙WAF绕过

2020-11-18 09:30:29

图片懒加载前端浏览器

2021-03-25 13:05:56

网络安全寄存器汇编语言

2020-12-18 08:49:11

相对跳转绝对跳转指令

2011-05-17 14:11:06

Dijkstra
点赞
收藏

51CTO技术栈公众号