翻译的不是很好,很多长句,觉的翻不出,就按自己理解的意思翻译出来了。有一些术语,也不知道自己翻译的对不对,要是大家觉的有什么更好词语,大家就在后面帖出来,或是跟版主说说,请他帮忙修改一下。 

六  反汇编导航

  在本章与随后的几个章节中,我们将简单地介绍IDA Pro交互操作的核心部分如何导航与如何操作。本章的重点将放在如何导航上,我们将演示如何利用IDA在反汇编世界中进行逻辑性的导航。迄今为止,我们已经演示了如何简单的将一些逆向工程的工具与IDA相结合使用来显示反汇编后的代码。在反汇编中熟练地导航是掌握IDA所必须的一项技能。静态反汇编列表除了提供向上向下翻页外,没有提供其他任何的导航功能。即使使用最好的文本编辑器,比如dead listing,也难以进行导航,它们所能提供的最好的导航也不过是一个内置的,类似grep的文本搜索功能。正如你将看到的,IDA的数据库基础提供了十分出色的导航功能。
 
  基本IDA导航

  在你刚开始接触IDA之初,你将会十分满意IDA所提供的导航功能。除了提供你已经熟悉的与你所使用的文本编辑器或文本处理软件一样的标准文本搜索功能,IDA还开发和显示一个全面的交叉引用的列表,这个列表就如同一个web页面的超链接一样。结论就是,在大多数的情况下,导航到所感兴趣的位置只需要进行一次鼠标双击。
  双击导航
  当一个程序被反汇编,这个程序中的每一个位置被赋于一个虚拟地址。这样,我们能够通过提供一个虚拟地址的值,导航到一个程序中任何一个我们所感兴趣的位置。但很不幸的是,在我们脑中记忆大量的虚拟地址不是一件简单的事情。这促使了早期的程序员为程序中的他们想引用的位置赋于了符号名字,这样使得事情变得简单了许多。为程序中的相应位置赋与符号名字与为程序机器码赋与助记符是不同的(这样助记符使得程序更容易读写,也更容易记忆)。
  正如我们先前所讨论的,IDA通过检查二进制文件的符号表,或是通过检查一个二进制文件中一个位置是如何引用的自动地生成一个名字,在分析阶段自动产生了符号名字。不仅仅是它的符号,在反汇编窗口中显示的任何名字都有可能像web页面中的超链接用来进行导航。这些名字和超链接之间的区别是,这些名字在任何时候都不会显示高亮来标识它能够用来导航,并且IDA中需要双击而不是像超链接只需要单击。我们已经在在各个子窗口中使用符号名字,例如导入,导出和函数窗口。对于这些窗口,双击一个名字都会使得反汇编视图跳转到相应的引用位置。这是一个简单的双击导航的例子。在下面的列表中,每一个符号用①来标识一个命名的导航目标。双击当中的任何一个都会使得IDA重新重定向的选择的位置。
 
 
For navigational purpose,IDA treats two additinal display entities as navigational targets.首先,交叉引用(如②所示)被认为是导航目标。交叉引用一般来说被格式化为一个名字和一个十六进制的偏移量。前一个列表中的loc_40134E右边的交叉引用指向这么一个位置,这个位置位于sub_4012E4 后4D16或7710字节处。双击交叉引用的文字能够跳转显示引用的位置(在这个例子中是00401331)。交叉引用将在第九章有更为详细的介绍。
  第二种类型的显示实体,在导航中使用了十六进制。如果一个显示的十六进制值是这个二进制文件中的合法的虚拟地址的值,当双击这个十六进制,将会使得反汇编窗口重新显示选择的虚拟地址的内容。在下面的列表中,双击任意一个被标记③的值,都将跳转显示新的位置的内容,因为每一个值都是这个二进制文件中的合法的虚拟地址。而双击标记为④,则没有任何反应。


 
  最后一个有关双击导航的事项是有光IDA信息窗口,这个窗口最常用来显示信息。当导航到一个目标,正如先前所描述的,在窗口中显示为第一个条目,当双击这个信息,将跳转到相应的目标。

  正如这个信息窗口摘要所显示的,被⑤所标识的两条信息能够被用来导航在这两条信息开头所显示的地址。双击其他任何的信息,包括那些包含⑥的信息,将没有任何反应。
地址跳转
有的时候,你知道你想要导航的位置的精确地址,而这个时候在反汇编窗口中并没有相应位置的符号名字来让你进行双击导航。在这种情况下,你有几种选择。首先,也是最基本的办法是使用反汇编窗口的滚动条上下滚动窗口,直到相对应的位置进入到视野当中。这种办法,只有在你知道你想要导航的位置的虚拟地址的时候才是可行的,因为反汇编窗口中的内容依据虚拟地址线性呈现的。如果你只知道一个命名位置的名字,例如一个名为foobar的子过程,那么通过使用滚动条来寻找相应的位置就像大海捞针一般。这个时候,你可以先在名字窗口中进行排序,然后找到相应的符号名字,双击导航到相应的位置。第三种办法是使用IDA的搜索功能,你可以在搜索菜单中找到。
  跳转到已知位置的最简单的方法是使用“跳转到地址”对话框,正如下图所示。


 
  当反汇编窗口处于激活状态下,可以通过“跳转”->“跳转到地址”,或是使用“G”快捷键来激活“跳转到地址”对话框。如果你将这个窗口看成是“Go”对话框,这将有助于你记忆相应的快捷键。这样,你只需要输入指定的地址(十六进制地址的值或是符号名字),点击确定按钮,就能够立即跳转并显示相应的位置。输入的值能够被对话框记录,并能够通过点击下拉列表,在随后的操作中再次被使用。这种历史记录的功能使得返回到先前访问过的位置变得十分容易。
  导航历史记录
  如果你将IDA的文档导航功能与网页浏览器中的文档导航功能相比较,你就会发现,符号名字与地址和超链接在功能上是一致的,通过它们都能够很容易地跳转到一个新的位置。IDA与传统网页浏览器共有的另外一个特性,是能够基于你流览反汇编代码的顺序来进行向前向后导航。每一次你流览反汇编窗口中代码一个新的位置,你的当前位置都会被追加到一个记录位置的链表上。有两个菜单操作能够遍历这个链表。首先,Jump->Jump to Previous Poistion表示立即跳转到紧临当前位置之前流览的位置。这与网页流览器中的后退按钮在概念上完全一样。这个是操作的快捷键是ESC,这是你应当牢记的最为常用的快捷键之一。但要注意的是,当是除了反汇编窗口以外的窗口处于活动状态的时候,使用ESC将会使得这些活动的窗口关闭。你可以通过View->Open将这些无意关闭的窗口再次打开。后退导航在你跟踪一个有着很多层的函数调用链的时候,或是你想会到你在反汇编窗口中最初的位置的时候,将十分有用。
  Jump->Jump to Next Position是一个相对应的操作,能够将反汇编窗口所显示的位置在位置链表中向前移动,就如同网页流览器中的向前按钮。这个操作的相对应的快捷键是CTRL-ENTER,它没有向后流览的快捷键ESC的使用频率高。最后,正如下图所示,两个使用频率很高的按钮,提供我们所熟悉的向前向后导航的功能。


 
  每一个按钮都与一个下拉历史记录列表相对应,这样你就能够立即访问到流览历史列表中的任何一个位置,而不需要通过对整个历史列表进行追溯。

栈帧

  因为IDA Pro是一个底层分析工具,所以它的一些特性和显示需要用户能够对被编译过的语言的底层细节有一定的了解,这些细节很大一部分体现在生成机器语言和管理被高级语言使用的内存的规则上。为了能够更好的理解
  其中的一个底层的概念是栈帧。栈帧是一块连续的内存,分配在程序的运行时栈上,指定给一个特定调用的函数。程序员一般将一些执行语句封装成为一个单元,称为函数(也被称为过程,方法或是子程序。)在一些情况下,这是当前使用语言的一个要求。在大多数的情况,使用函数进行编程被认为是一个很好的编程习惯。
  当一个函数没有被执行的时候,它一般来说不需要内存或是只需要很小的内存。当一个函数被调用的时候,它会因为多种原因需要分配内存。首先,调用者需要以参数的形式将信息传入到函数中,而这些参数需要保存在一定的地方,使得函数能够访问到。第二,被调用的函数在执行它的任务的时候需要临时的存储空间。这些临时空间一般是通过程序员申明本地变量来进行分配的,这些变量只能够在函数中被使用,一但函数调用完毕,这些变量将不能够再被访问。
编译器利用栈帧,使得函数参数和函数中局部变量的分配与释放对程序员来说是透明的。编译器在将控制权转移给函数本身之前,插入一定的代码将函数的参数压入栈帧中,并分配足够的内存空间用来存放函数中的局部变量。函数栈帧的结构特点,使得函数的返回地址也应该被保存在这个新的栈帧中。使用栈帧的一个好处是使得递归变为可能,因为对函数的每一次的递归调用,这个函数都被分配一个新的自己的栈帧,这样就巧妙的将没一次的调用与前一次调用相隔离开。下面是当一个函数被调用的时候具体发生的步骤:
1.  调用函数将被调用函数所要求的参数,根据相应的函数调用约定,保存在一定的位置中。如果参数是被放置在运行时栈中,这个操作会改变程序的栈指针。
2.  调用函数将控制权移交给调用函数。这个通常用一条指令来实现,比如x86中的CALL或是MIPS中的JAL。函数的返回地址一般是保存在程序的栈中,或是CPU中的寄存器中。
3.  如果有必要,调用函数会设置栈帧指针,并保存调用函数希望保存不变的寄存器的值。
4.  被调用函数为自己的局部变量分配内存空间。这个通常是通过修改栈顶指针的值,在运行时栈中保留出一块空间。
5.  被调用函数执行自己的任务,通常会返回一个值。在执行自己任务的时候,被调用函数有可能需要访问由调用函数传入的参数。如果调用函数返回一个值,这个值通常是被保存在一个指定的寄存器中。
6.  一旦调用函数完成自己的操作,为此函数局部变量分配的栈上空间都将被释放。这通常是步骤四的逆向执行。
7.   在步骤三中保存的寄存器的值被恢复。这包含了对调用函数的栈帧指针寄存器的恢复。
8.  被调用函数将控制权交返给调用函数。典型的指令包括x86中的RET和MIPS中的JR。根据使用的函数调用约定的不同,这个操作也可能承担着从程序栈上清除先前传入的参数的任务
9.  一旦调用函数再次获得控制权,它也许需要将先前的参数从栈上清除。在这种情况下,对栈的修改需要将栈帧指针的值恢复到步骤一之前所拥有的值。
步骤三与步骤四在函数调用之初都会被一同调用,以至于这两个操作被统称为prologue。同样地,步骤六到步骤八在函数调用的最后总是被频繁的一同调用,于是它们组成了函数的epilogue。除了步骤五,它代表着函数的实体,其它的所有操作组成在一起被称为函数调用。

调用约定
  在对栈帧有了初步的了解之后,我们可以来进一步的了解它们的结构。下面的这个例子是建立在x86架构之上的,并且其行为与Mcrosoft Visual C/C++或是gcc/g++编译器相关。创建一个栈帧的最重要的一个步骤是调用函数如何向栈中传递函数参数。调用函数必须能够精确的存贮这些参数,使得被调用函数能够访问到它们,否则,将出现严重的错误。函数通过选择一个特定的调用约定,来表明它们希望以一个特定的方式接收它们的参数。
  一个调用约定精确的指明了一个调用函数如何传递被调用函数所需的参数。调用约定可能说明使用指定的寄存器,或是程序的栈上,或是二者皆有,来保存函数的参数。另一个重要的概念是,当一个被调用函数完成执行任务以后,先前传入栈上的参数该由谁负责清除。有的调用约定指明由调用函数负责将栈上的参数清除,然而有的调用约定指明由被调用函数负责清除栈上的参数。遵从相应的调用约定是保证程序栈指针完整性所必不可少的。

C调用约定
对于x86架构上的大多数C编译器来说,默认的调用约定被称为C调用约定。C/C++程序中的_cdecl修饰符会使得编译器使用C调用约定。从这里开始,我们使用cdecl来代表C调用约定。C调用约定规定调用函数将参数从右到左依次压入栈中,并且由调用函数负责将栈上的参数清除。
将参数从右到左依次入栈的结果是,当参数被调用的时候,函数最左边(第一个)的参数将位于栈顶。这使得第一个参数能够很容易的就被访问到,而不用关心被调用函数究竟需要几个参数。这就使得C调用约定很适合用于那些参数个数不确定的函数。(例如printf)
调用函数负责清除栈上的参数的规定,使得在被调用函数返回后,你常常能够看到对程序栈指针进行调整的指令。对于参数不确定的函数,调用函数很适合对栈进行修改,因为调用函数能够精确的知道传递参数的个数,并且进行精确的修改,而对于被调用函数,它永远也不能够事先知道将要传入函数的参数的个数,这使得被调用函数难以对栈指针进行调整。
在下面的一些例子中,我们假定被调用函数是以下的函数原型。

 
这个函数我们假定它默认的调用约定为C调用约定,因此这四个函数参数将从右至左依次压入栈中,并且由调用函数负责清除栈上的参数。编译器将为调用此函数生成以下代码。


 
从①开始的四个push操作使得程序栈指针(ESP)增加16字节(在32位架构上4*size(int)),当demo_cdecl函数返回的时候,在②处进行了相应的逆向操作。如果demo_cdecl被调用50次,那么每次函数调用后都会有与②相似的修改操作。下面的这个例子遵循的还是C调用约定,但是在demo_cdecl调用返回之后,调用函数函数并没有显示的对栈空间进行清除。
 

在这个例子中,编译器在函数demo_cdecl的prologue阶段,在栈的顶部为函数参数预先分配了内存空间。当demo_cdecl的函数参数被传入栈中,并没有对栈指针进行修改,因此也不需要对在demo_cdecl返回的时候也不需要对栈指针进行修改。GNU编译器(gcc和g++)利用这种技术将函数参数传递至栈上。但要注意的是,两种方法都使得当函数被调用的时候,栈指针指向函数最左边的参数。

标准调用约定

标准这个词在这里似乎有点使用不当,因为这个名字是Microsoft为自己的调用约定所起的。这种调用约定使用_stdcall修饰符来进行申明,正如以下所示:


 
为了防止大家对标准这个词产生误解,我们在本书的以下内容都将此调用约定称为stdcall调用约定。
与C调用约定相同,stdcall调用约定也要求函数参数从右到左此次传入栈中。不同的是,stdcall调用约定规定当被调用函数执行结束返回的时候,由被调用函数负责清除栈上的指针。对于需要完成这一任务的函数来说,此函数必须知道在栈上有多少函数参数。这个只适应于那些参数个数固定的函数。这导致了一些函数参数不固定的函数,例如printf,无法使用stdcall调用约定。例如,demo_stdcall函数需要传入三个整数类型的参数,需要在栈上占用12字节的空间(在32位架构上4*size(int))。一个x86的编译器,能够利用RET指令的特殊形式,同时将返回地址从栈上弹出,并且将栈指针加上12用来清除栈上的参数。在demo_stdcall的例子中,我们可以在下面看到用来返回到调用函数的指令。


 
使用stdcall调用约定的最大好处是,在每个函数执行返回后,不需要代码来清理栈上的参数,这使得程序变得更下,执行起来更快。依照惯例,Microsoft的所有的DLL导出的固定参数个数的函数都使用stdcall调用约定。

X86的快速调用约定
Fsatcall调用约定是stdcall调用约定的一种变形,fastcall将函数参数中的两个传递至CPU的寄存器中,而不是程序栈上。Microsoft Visual C/C++和GNU gcc/g++(版本4.3及其以后)编译器能够识别函数申明中的fastcall修饰符。当函数被指定使用fastcall调用约定,前两个参数将被分别传入ECX和EDX寄存器中。其他的参数将和stdcall一样,从从右到左依次压入栈中。与stdcall相同,fastcall调用约定也要求被调用函数负责清除栈上的参数。以下显示如何使用fastcall修饰符。

 
当调用demo_fastcall的时候,编译器将会生成以下的代码。


 
可以注意到,当demo_fastcall调用返回后,没有对栈指针进行调整,这是因为demo_fastcall本身负责对参数y和z进行了清除。我们还应当清楚,因为有两个参数被传递至寄存器上,我们只需要从栈上清除8字节的空间,尽管参数有四个参数。

C++调用约定
C++类中的非静态函数与其它的标准函数的不同之处,在于它们需要一个this指针,指向调用它们的对象。调用函数的对象的地址必须由调用者提供,并且当调用对象非静态成员函数的时候,对象的指针以参数的形式传递给被调用函数。C++标准本身并没有规定this指针该如何传递给非静态成员函数,因此各个不同的编译器有着自己的传递this指针的技术。
Microsoft Visual C++编译器提供一个thiscall的调用约定,这个调用约定将this指针保存再ECX寄存器中,并且要求非静态成员函数负责清理栈上参数。GNU的g++编译器隐式的将this指针当作非静态成员函数的第一个参数,除此之外,其它方面与C调用约定一致。对于g++编译后的代码,this指针在调用非静态成员函数之前被放置在栈的顶部,并且由调用者在函数返回后负责清理栈上的参数。更多有关C++编译的信息将在第八章介绍。

其它的调用约定
如果想要详细介绍当前所有的调用约定的话,将需要单独的一本书来叙述。调用约定常常是和语言,编译器和CPU相关的。而且根据你所研究方面的不同要求,你也有可能遇见一些非主流编译器生成的代码。另外一些特殊的情况也需要注意,比如代码优化,自定义的汇编语言代码,系统调用。
当一个函数导出被其他的程序员所使用(例如库函数),很重要的一点是这个函数需要遵循主要的调用约定,使得程序员能够很容易的使用这些函数。从另一个方面来说,如果一个函数只是用于内部使用,那么这个函数的调用约定可以只被使用这个函数的程序所了解。在一些情况下,为了产生更快的代码,最优化情况下的编译器会选择一些备用的调用约定。比如Microsoft Visual C++使用/GL编译选项,GNU gcc/g++使用regparm关键字。
系统调用是一种特殊的函数调用,用于请求一个操作系统的服务。为了使操作系统内核完成用户的请求,系统调用常常使得线程从用户态转换为内核态。系统调用的初始化方法在不同的操作系统和CPU上是不同的。例如,Linux x86的系统调用使用int 0x80指令进行初始化,然而其它的x86系统可能使用sysenter指令。在许多的x86系统(Linux是一个例外)上,系统调用的参数被存储在运行时栈上,而系统调用号在初始化系统调用之前被存储在EAX寄存器上。Linux的系统调用将参数存储在寄存器上,当系统调用的参数的个数多于可用的寄存器,这些参数将会被传递至程序栈上。

局部变量布局
  与调用约定规定函数参数如何传入不同,并没有规定函数的局部变量该以怎样的方式进行布局。编译器面临的第一个任务是计算出一个函数的局部变量所需要的空间总数。第二个任务是确定这些变量是能够存储在寄存器上,还是必须分配在程序栈上。这些变量的空间究竟应该如何分配,与调用函数和被调用函数没有什么联系。值得注意的一点是,仅仅从函数的原代码上,我们是无法确定这个函数的局部变量的分布情况的。

栈帧例子
  下面这个函数在32位 x86架构上的计算机上被编译。
 
 
  通过计算,我们知道这个函数的局部变量所需要的最小的空间是76字节(三个四字节的整数变量和一个64字节的数组)。这个函数可以用C调用约定或是标准调用约定,这两种调用约定下的栈帧是一样的。下图将显示调用demo_stackframe的时候,其栈帧的一种可能实现,这里假定没有使用帧指针寄存器这个栈帧在demo_stackframe的入口处使用一行prologue建立起这个栈帧。
 

  Offset列指定了基地址+偏移量的地址,这些地址能够用来引用栈帧中任意的局部变量和参数。


 
  使用栈指针来引用所有的局部变量的函数需要编译器做更多的工作,因为栈指针频繁被修改,使得编译器要保证,当对栈帧中的所有变量进行引用时,相应的偏移量要一直是正确的。


 
通过图6-3中的偏移量可以看出①处的push指令将局部变量y准确的压入栈中。当我们初次看到②处的push指令的时候,我们也许会怀疑这里错误的将局部变量y再次的压入了栈中。但是,因为我们是在与一个基于ESP的栈帧打交道,处于①处的push指令改变了ESP寄存器,因此图6-3中的所有的偏移量在ESP改变之后必须进行临时调整。①处的指令执行完之后,局部变量z的新的偏移量变为了[ESP+4],这正如②处所准确引用的一样。当处理使用栈指针引用栈帧中的变量的函数时,你必须密切注意对栈指针的任何改动,并且对改动后的变量的偏移量进行相应的调整。使用栈指针来引用栈帧中所有变量的一个好处是,使得其它余下的寄存器能够用于其它地方。
一旦demo_stackframe执行结束,它需要返回到调用函数中。RET指令能够用来将栈顶上的返回地址弹出赋值给EIP。在返回地址被弹出之前,栈上的局部变量需要从栈上清处。这样才使得,当ret指令执行的时候,栈指针能够准确的指向保存的返回地址。对于这个指定的函数,相应的epilogue如下。


 
我们可以通过指定一个寄存器用做栈帧指针,并在函数入口处对栈帧指针进行一些设置,来使得计算局部变量的偏移量的工作变得更为简单。在x86的程序上,EBP(扩展基础指针)寄存器常常被用来当作栈指针。在默认情况下,绝大多数的编译器使用一个帧指针来生成代码,虽然它们都有可选的编译选项,即使用栈指针来做为代替。例如GNU gcc/g++编译器提供 fomit-frame-pointer编译选项,它使得编译器生成不依赖于固定帧指针寄存器的代码。
为了看看demo_stackframe在使用了一个指定帧指针之后的栈帧的布局究竟是怎样的,我们需要以下新的prologue代码。


 
③处的push指令将当前被调用函数使用的EBP值保存起来。C调用约定和标准调用约定允许一个函数改变EAC,ECX和EDX的值,但不允许函数对其它寄存器进行改变。因此,如果我们想把EBP用做一个帧指针,我们必须在修改它之前保存它的当前值,然后必须在从被调用函数返回到调用函数之前将EBP恢复到原来的值。如果有其它任何的寄存器需要在调用函数中进行保存操作(例如ESI,EDI),编译器会在保存EBP值的时候来进行保存,或是延迟保存直到局部变量空间被分配。在一个栈帧中,并没有为这些保存的寄存器的空间指定标准的存储位置。
一旦EBP被保存,它可以被修改指向当前栈的位置。这是通过④处的一个MOV指令来实现的,它将栈指针的当前值复制到EBP中。最后,如同在一个非使用EBP的栈帧中一样,局部变量的空间在⑤处被分配。最后的栈帧布局如下图所示:


 
  在使用了一个专门的帧指针之后,所有的变量的偏移量可以通过使用帧指针寄存器来进行计算获得。在绝大多数的情况下(虽然不是要求的),正偏移量被用来访问函数参数,而负偏移量被用来访问布局变量。在有了一个专门的帧指针之后,栈指针能够自由的进行改变,而不用担心帧中的任意变量的偏移量会受到影响。对函数bar的调用现在如以下面所示:

 
  ⑥处的push指令对栈指针的修改。对紧接下来的push指令中对局部变量z的访问并没有任何的影响。
  最后,由于对帧指针的使用,在函数结束之后,使得epilogue会有一些不同,因为调用函数的帧指针在函数返回之前必须被恢复。在帧指针恢复到先前值之前,必须将局部变量从栈上清除,由于当前帧指针指向先前帧指针,于是这一切操作变得容易。X86程序利用了EBP做为帧指针,下列代码显示了典型的epilogue:

 
  由于以上的操作是如此的频繁,使得x86架构提供一个leave指令,用来完成相同的任务。


 
  尽管使用的寄存器的名字和指令在不同的处理器架构上有所不同,但是创建栈帧的基本过程是一致。不管机器架构如何,你也许想熟悉典型的prologue和epilogue过程,使得你能够很快的去分析函数中一些更为有趣的代码。

IDA栈视图
  栈帧很明显是一个运行时的概念,也就是说,如果程序不运行,栈不存在,栈帧也就无法存在。虽然以上概念是正确的,但这并不意味着当你使用像IDA这样的工具进行静态分析的时候,你能够忽视栈帧的概念。为所有函数建立起栈帧的代码也存在于二进制文件中。通过对这些代码进行详细的分析,即使函数没有运行,我们也能够对任何函数的栈帧结构有一个详细的了解。在分析之初,通过对push或pop和其它一些任何可以对栈指针产生修改的指令,例如加减常数,进行分析,IDA对一个函数的栈指针的行为进行着很深入的监视。这样分析的首要目的是确定分配在函数栈帧上的局部变量空间的准确值。其它的目的包括确定在一个函数中是否使用了特定的帧指针(例如通过识别出push ebp/mov ebp,esp代码序列),和识别出一个函数栈帧中所有的对变量的内存引用。例如,如果IDA注意到下面在demo_stackframe中的指令:

 
IDA将会注意到函数的第一个参数被加载进EAX寄存器中(参看 Figue 6-4)。通过对栈帧结构的详细分析,IDA能够分清对函数参数的内存引用(处于保存的返回地址之下的值)和对局部变量的引用(处于保存的返回地址之上的值)。IDA还使用一些额外的步骤来确定栈帧中的哪些内存位置是被直接引用的。例如,虽然Figure 6-4中的栈帧有96字节的大小,但是实际上只有7个变量被我们引用到(四个局部变量和三个函数参数)。
  对函数行为的理解归结起来就是对函数所操作的数据类型的理解。当阅读一个反汇编列表的时候,查看函数栈帧的细目表是理解函数所操作数据的时机之一。IDA提供两种不同的函数栈帧视图:摘要视图和详细视图。为了理解这两种视图,我们将使用下面的这个版本的demo_stackframe,我们将会使用gcc进行编译。


 
  在这个例子中,我们为变量y和z提供初始值,防止编译器给出在函数bar中使用未初始化的变量的警告信息。并且,一个字符被保存为数组的第一个元素,我们选择不初始化变量x。相应的IDA对这个函数的反汇编分析如下。

 
  在这个列表中我们将提及许多的知识点,使得我们能够熟悉IDA的反汇编记号。我们从①处开始,通过对函数的prologue进行分析,IDA认为这个函数使用EBP寄存器做为帧指针。通过②处的代码,我们了解到gcc在这个栈帧中为局部变量分配了120字节的空间。这个包括了③处为函数bar传入的两个总共为8字节的参数,但这仍然远大于我们先前估计的76字节,这表明编译器为了在栈帧中保证某种特殊的字节对齐,会为局部变量分配额外的内存空间。从④处开始,IDA提供了一个摘要性的栈视图,它将栈帧中被直接引用的变量列出来,并且标上这个变量的大小和它们与帧指针之间的偏移量。
  IDA根据它们与被保存的返回地址之间的相对距离,为每个局部变量赋予了一个名字。局部变量处于保存的返回地址之上,而函数参数位于保存的返回地址之下。局部变量使用var_为前缀,用其与保存的帧指针之间距离的十六进制的形式做为后缀来命名。在这个例子中,局部变量var_c是一个4字节大小的变量,并且位于保存的帧指针([ebp-0ch])12个字节之上。函数参数的命名规则是使用arg_做为前缀,加上一个表示当前参数与第一个参数之间距离的十六进制形式做为后缀。这样,第一个四字节的参数将被命名为arg_0,随后的参数将被命名为arg_4,arg_8,arg_c,以此类推。在这个特殊的例子中,arg_0没有被列出,是因为在这个函数中没有使用到参数a。因为IDA没有发现有任何的内存引用到[ebp+8](第一个参数的位置),所以arg_0没有被在摘要内存视图中列出。在对摘要内存视图快速流览后,我们发现IDA对许多的栈中位置都没有进行命名,这是因为在程序代码中没有对这些位置进行直接的引用。

注意
:只有当栈中的变量在函数中被直接引用,IDA才会自动为这些变量命名。
  IDA的反汇编列表与我们先前所显示的栈帧分析之间的一个重要不同之处是,在反汇编列表之中我们看不到像[ebp-22]这样形式的内存引用。相反,IDA在反汇编列表中,将所有的不变的偏移量用符号名字所替代。这使得IDA保持着其产生更高阶的反汇编的目标。使用符号名字也比数字常量更为容易。事实上,正如我们后面将看到的,IDA允许我们修改栈上任何的变量的名字为我们所期望的名字,这也使得这些名字更容易记忆。摘要栈视图负责将IDA生成的名字映射到相应的栈帧偏移量上。例如,反汇编窗口中的显示的内存引用[ebp+arg_8],被[ebp+16]或[ebp+10h]所代替。如果你喜欢数字型的偏移量,IDA将很高兴的为你显示。正如Figure6-5所示,右键点击⑤处的arg_8会弹出相应的菜单,从这个菜单中我们可以改变数据的显示格式。


 
  在这个特殊的例子中,因为我们手中有源代码可以进行比较,所以我们可以通过一系列的反汇编代码中的变量的线索,将IDA生成的变量名字映射到源代码中使用的实际变量的名字。
1.  首先,demo_stackframe需要传入三个参数:a,b和c。这个三个参数与arg_0,arg_4和arg_8分别一一对应。(因为arg_0没有被直接引用,所以在反汇编代码中没有被列出来)。
2.  局部变量x通过参数c来初始化。也就是说var_c与x相对应,因为它是用arg_8来初始化的。⑥
3.  相类似的,局部变量y是通过参数b来初始化的。也就是说var_5c与y相对应,因为它是用arg_4来初始化的。⑦
4.  局部变量z与var_60相对应,因为它用值10进行初始化在⑧。
5.  64字节大小的字符数组buffer从var_58处开始,并且buffer[0]用A(ASCII 0X41)初始化。
6.  Bar的两个函数参数被复制到栈中,而不是被压入栈中。这是当前版本gcc(3.4或更高版本)的一钟典型做法。我们所看到的局部变量var_74和var_78,实际上是编译器在调用函数之前在栈中为函数参数预留的内存空间。

文本搜索
IDA的文本搜索就意味着对反汇编列表视图进行子字符串搜索。文本搜索通过Search->Text或ALT-T来激活,Figure 6-7就是打开的文本搜索对话框。文本搜索有许多直观的选项用来指定文本搜索执行的细节。正如图中所示,类POSIX正则表达式被启用。Identifier在某种程度上是误用的名字。实际上它是将搜索结果限定在一个完整的单词或是词组,包括操作码或是常量值。一个对401116的Identifier搜索,将无法匹配到符号名loc_401116。
你也许会注意到,用于引用var_78和var_74的记号,与对其它变量引用的记号有所不同,例如var_60。IDA使用下列的记号引用var_60:[ebp+var_60]。通过查看摘要栈视图,我们直到这个记号与[ebp-60h]是等价的。对于这个特定的函数,IDA将符号名字var_60与数值-60视为等价。用于引用var_78的记号相比起来则更为复杂:[esp+78h+var_78]。这种情况下的细微变化,是由于var_78是通过ESP进行引用的,而不是EBP。结果是,IDA需要使用“fudge factor”来利用符号名字var_78。[esp+var_78]的地址也许是不正确的,因为正如底层机器语言指令所定义的,它与[esp-78h]而不是与[esp]相当。为了能够利用符号变量名字,常数78h必须被加上,使得相应的数学计算的结果是正确的,产生实际上的结果[esp+78h-78h],这就正确的产生了[esp]。这种小技巧是很值得记住的,因为当一个函数不使用帧指针,其中的所有的变量都通过栈指针的偏移量来进行引用的时候,是需要这种小技巧的。在这种情况下,IDA根据与保存的返回地址的偏移量而不是保存的帧指针的偏移量来生成变量名,因为如果是基于保存的帧指针进行引用,就会与基于栈指针的引用一样,需要进行相同类型的偏移量调整。
除了摘要栈视图,IDA还提供了一个详细显示栈帧中每个被分配字节的详细栈帧视图。你可以通过双击一个栈帧中的任意一个变量名来激活详细视图。双击先前列表中的var_c能够显示出如Figure 6-6中所示的栈帧视图(ESC关闭窗口)。

 
因为详细视图记录了栈中的每个字节,所以它比摘要视图占据了更多的空间,摘要视图只列出了被引用的变量。Figure 6-6所显示的栈帧的部分跨越32字节,这个只是整个栈帧的一小部分。可以注意到,对于那些在函数中没有被直接引用的字节并没有被赋予名字。例如,参数a,也就是arg_0,在demo_stackframe中从来没有被引用。由于没有内存引用用来分析,IDA选择对这些栈帧中的相应的的字节不做任何操作。从另外一方面来说,arg_4在反汇编列表中的⑦处被直接引用,它的内容被装载进32位的EAX寄存器。基于32位的数据被移动的事实,IDA能够推断出arg_4是一个4字节大小的变量,并且以此为其标记(db定义了一个字节的存储空间,dw定义了两个字节的存储空间,也被称为一个字,dd定义4个字节的存储空间,也被称为一个双字)。
在Figure 6-6中所示的两个特别的值是“ s”和“ r”(二者都以一个空格开头)。这些虚构的变量是IDA用来表示保存的返回地址(“ r”)和保存的寄存器的值(“ s”在这个例子中代表EBP)。这些值被包含在栈视图中是为了完整性,因为栈帧中的每个字节都将被计算。
栈帧视图使得你能够查看编译器内部工作细节。在Figure 6-5中,你能够很清楚的看到编译器在保存的帧指针”s”和局部变量x(var_c)之间查入额外的八字节。这些字节占据了栈帧中从-00000001到-00000008的偏移量。此外,对摘要视图中所列的局部变量相联系的偏移量做一些数学计算,你就会发现编译器为字符数组var_58分配了76字节(而不是源代码中所说的64字节)。除非你恰巧是一个编译器作者,或者你想深入的研究gcc的源代码,那么你所能够做的只是猜测为什么这些额外的字节以这种方式被分配。在大多数的情况下,你会将原因归结于这些额外的字节是用于字节对齐,并且这些额外字节的存在对程序的运行并不会产生任何的影响。毕竟,如果一个程序员申请64字节,而最终给予了76字节,这个程序的运行通常不会有任何的不同,特别是当这个程序员不使用任何多于64字节的东西。从另一方面来说,如果你恰巧是一个exploit开发人员,你就会知道有可能溢出这个特殊的缓冲区。在第八章中,我们会再次回到栈帧视图中,使用它处理一些更为复杂的数据类型,例如数组和结构。

搜索数据库
  IDA使得你能够很容易的就导航到你已知的东西,并且设计了许多数据视图用来总结一些特定类型的信息(名字,字符串,导出表等等),使得你能够很容易的找到相应的信息。但究竟是IDA的什么特性使得你能够在数据库中进行更为一般的搜索?你过你花点时间看看Search菜单的内容,你就会发现这个菜单有一长串的选项。其中大部分的选项是引导你到一些种类的下一个匹配项。Search->Next Code将光标移动到下一个包含指令的位置。你也许也想熟悉Jump菜单中的选项。对于其中的大多数选项,你被提供一个地址的列表由你选择,使得你能够从一个位置快速的导航到另一个位置。虽然这些封装的搜索特性十分的有用,但还是有两种多用途的搜索值得详细的介绍:文本搜索和二进制搜索。

二进制搜索
  如果你需要搜索特定的二进制内容,例如一段已知的字节,文本搜索并无法实现此功能,你需要使用IDA的二进制搜索功能。尽管文本搜索功能搜索反汇编窗口,但你可以认为二进制搜索功能只搜索十六进制视图窗口的内容部分。根据搜索串是如何指定的,十六进制的形式或是ASCII的形式都能够被搜索到。二进制搜索能够通过Search->Sequence of Bytes, 或是ALT-B来激活。Figure6-8显示了二进制搜索对话框。为了能够搜索一串十六进制字节,搜索串需要被指定为用空格分割,两个十六进制为一组的形式,例如CA FE BA BE,即使对大小写敏感的选项被激活,其小写的形式与其大写形式完全一样。
  为了能够搜索被嵌入的字符串的数据(非常有效的搜索十六进制视图窗口中ASCII部分),你必须将搜索串用引号引用起来。当你搜索的字符串是Unicode版本的时候,将Unicode字符串的选项激活。
  大小写敏感的选项可能令人有点困惑。对于字符串搜索,这个选项是十分直观明白的,当这个选项没有被选择的时候,搜索“hello”也能够成功的搜索到“HELLO”。如果你进行十六进制搜索,并且大小写敏感的选项不选择,那么事情会变的有些有趣。当你进行一次大小写不敏感的搜索,搜索串是 E9 41 C3,你也许会奇怪的注意到你的搜索结果是E9 61 C3。这两个字符串都是匹配的结果,因为0x41是A的ASCII值,而0x61是a的ASCII值。所以,即使你已经指定了十六进制的搜索,0x41还是会被认为与0x61是相等的,因为你没有说明是大小写敏感搜索。

 
注意:当你进行十六进搜索的时候,如果你想得到精确的匹配结果,确保你已经指定了大小写敏感。这对于你在搜索特定的操作码而不是ASCII文本的时候是十分重要的。
  如果你想搜索下一个匹配到的二进制数据,你只需要CTRL-B或是Search->Next Sequence of Bytes。最后,你不需要一定在十六进制视图窗口中进行你的二进制搜索。当反汇编视图窗口处于激活状态的时候,IDA允许你指定二进制搜索标准,当搜索到相应的匹配结果的时候,反汇编窗口会跳转到满足搜索标准的匹配结果的位置。
本章小结
  本章的目的是为你能够有效的在反汇编代码中进行导航提供一些必要的技能。你与IDA交互的大多数操作都与我们已经讨论过的操作相关。当你能够安全有效的进行导航之后,下一步你将学习的是如何修改IDA的数据库,以满足你的特殊需求。在下一章节中,做为帮助我们理解学习二进制代码的内容和行为的一种办法,我们将学习如何对反汇编代码进行最基本的修改。