art of disassembly----chapter01----lesson9--Opcodes and Mnemonics---01

翻译 2011年12月29日 17:47:13

在本LESSON中,我将把Opcode (Opcodes)翻译为操作码!!!


        本章节讨论了80X86指令集的低级实现,描述了INTEL工程师怎么样决定用数值来编码指令以及当他们设计CPU时,所做的权衡。同时,展示了设计时所做努力的历史被景以便你更好的理解他们不能不做的一些在设计上的妥协。

        指令设计的重要性

        在本小节, 我们将探讨CPU设计最有趣而又重要的方面之一:CPU指令集的设计。CPU指令集的架构是CPU工程师在一开始设计时就必须认真考虑的问题之一。高速缓存、多管道等等这些特性是很容易被重新嫁接到CPU上的,一旦最初的CPU设计不符合要求。但是,要改变CPU的指令集可不是一个简单的事情,一旦CPU开始生产,人们开始以些指令集编写软件。因此,对CPU指令集的选择必须非常的谨慎。

       你可能试图在你的指令集中加入你所能够想到的指令(称为"kitchen sink"),以这种的方法来实现你的指令集设计。这样的设计在很多方面都是有问题的,我们将在下面的小节中再来讨论。好的CPU设计在于选择出可抛弃的指令而不是选择可保留的指令。在指令集中包含尽可能说的指令说起来当然是很容易了,但是难点在于:一旦你意识到不可能把这么多东西放在一个小小的芯片上时,弃用哪些指令将使你纠结。

      尴尬的事实1:硅是有限的。面临的第一个问题是:每一个特性就会消耗掉一定数目的晶体管,而在一个CPU上所能集成的晶体管的数目是有限有的。这意味着在一个CPU上是没有足够的晶体管来支持你的所谓的所有的特性。比如,早期的8086CPU使用少于30,000个晶体管,奔腾3使用超过800W个晶体管。这两者的数值差异反映了1978 1998两个年份的半导体技术的不同。

      尴尬的事实2:价格。在当今,虽然在一个CPU上使用百万数量级的晶体管已经不是一个问题了,但是使用的晶体管越多,CPU的价格就越是昂贵。比如,奔腾4要价几百美金。而一个带30,000个晶体管的CPU只要价几美元。为节约成本,忽略一些特性以使用更少的晶体管就非常重要的。

     尴尬的事实3:可扩展性。以"kitchen sink"方法来实现CPU设计的另一个问题在于:在设计CPU之初你是很难去预期到人们所想要使用的所有特性。例如:INTEL 的MMX 和SIMD 指令添加到奔腾系列处理器上以增强多媒体编程。在1978年,人们是很难预期到使用这些指令的需要。

     尴尬的事实4:向上的兼容性。这几乎是可扩展性的反面。CPU感觉一条指令很重要但事实却是这条指令使少使用。例如:80X86系列CPU的LOOP指令在当然高性能的编程中很少使用到,80X86上的ENTER指令也是一样的。当使用"kitchen sink "这种方法来设计CPU时,你经常可以发现程序基本上是不使用一部分指令的。然后不幸的是,你几乎不可能在下一个CPU版本中去移除这些指令,这会使得使用了这些指令的旧程序出现崩溃。一般来说,当你在你的指令集中加了某条指令,你不得不一直支持它即使你的CPU版本改变了。除非是真的非常非常少的程序在使用这条指令同时你也乐意让它们无法运行,否则你得自动的在软件中模拟这些指令,由此可见,移除指令是一个非常艰难的事情。

    尴尬的事实5:复杂性。一个新的CPU是否流行是很容易衡量的,只要看看有多少人为这种类型的CPU写软件就可知道了。很多CPU很快就消亡了因为没人为这种特定的CPU写软件。因为CPU的设计师必须为使用这些指令的汇编程序员及编译器写作者考虑。“kitchen sink”这种实现方法看起来对这程序员有吸引力。事实是,没有人愿意去学习一个复杂的系统。如果你的CPU指令覆盖了方方面面,这也许对熟悉了CPU的人有吸引力。但是遗憾的是无乎无人有心去学习这样的一个复杂的系统。

    以上所述的以"kitchen sink"实现所带来的种种问题有一个普遍的解决方案:在开始时就设计简单的指令集并为扩张预留空间。这是80X86系列处理器如此流行的主要原因之一。INTEL最初只设计一个简单的CPU并在随后不断的扩张指令集来添加新的特性。

   


     基本的指令设计目标

     一个典型的Von Neumann架构的CPU,计算机将数值型的CPU指令进行解码,把这些数值存放在内存中。指令的解码是指令集设计最主要的任务之一,是需要好好考虑的。

    解码一条指令,我们必须为每一条指令指定一个独一无二的数值型操作码(numeric opcode value)(这是很显然的,两条不同的指令是不能共享同一个数值的,否当CPU试图去解码操作码时,它是无法分辨两条指令的)。对于一个N个位的数据,所能的有2n个不同的操作码,所以去解码一个指令m(m为一个数值),它所对应的操作码有log2(m)位长

    解析操作码比为每一条指令指定一个独一无二的数值要来得复杂一些。记住,我们必须利用现有的硬件来弄清楚每一条指令的作用并操作其它的硬件来完成指令所代表的特定操作。假如你有一个7位的操作码,这将对应有128条不同的指令。对每一条指令进行单独的解码需要 seven-line to 128-line的解码器。假设我们的指令有特定的模式,我们就可以通过以三个更小的解码器来代替这个大解码器来减小硬件。

   如果你有128个互不相同的指令,除了对它们进行单独的解码你几乎不能再干什么了。然后,在大多数的架构中,各条指令并不是完全的相互独立的。例如:在80X86CPU中,"mov (eax,ebx)"和"mov (ecx,edx)"有不同的操作码(different opcodes)(因为这两条是不同的指令)。但这两条指令之间并非没有联系。它们都是从一个寄存器移动数据到另一个寄存器中。实现上,它们两者唯一的不同在于源操作数及目的操作数。这意味着,操作码可以进行更细微的分解。在一个操作码之中,我们可以以一个更小的子操作码来编码指令(如mov),用这个操作码中的其它位串来编码操作数。

   比如:我们现在只有8条指令,每条指令有两个操作数,每个操作数只是4个数值之中的一个,那么我们可以把操作码分成三个域按3,2,2位的形式进行编码。这样的编码形式仅仅需要3个不同的解码器就可以分辨本条指令的含义。 这个小小的例子道出了CPU指令集设计的一个很重要的方面:让操作码的解析尽可能的简单。而要实现这个要求只要把操作码分解成几个不同的位域,这几个位域分别包含正确执行此指令的部分信息。位域越小,硬件来解码和执行就越是简单.

                            

         CPU设计的另一个重要的目标在于使得指令的长度尽可能的在一个合理的范围内。不必要的长指令消耗额外的内存空间并影响CPU整体的性能。因此,指令应该尽可能紧凑,这个带来的另一个好处在于程序使用更少的内存空间。

         如果我们用N位来编码2N个不同的指令,这看起来没有余地来选择指令的长度。必须得用n位来编码这2n条指令。但同时,你也可能使用多于n位来编码这些指令,信不信由你,这种方法是减小一个应该程序大小的秘诀。

        在讨论用长指令来产生小程序之前,我们先小小的偏离主题一下。首要一点是:我们没法为操作码长度选择任意的位长。假如我们的CPU具备从内存中读字节的能力,那么操作码的长度因应该是8位长的整数倍。但如果CPU没法从内存中读取一整个字节(大部分复杂指令集的CPU从内存中一次性读取32位或64位数据)那么,操作码的长度就必须是CPU一次能从内存中读取的最小位长。试图缩短这个数据总线的下限是毫无意义的。既然我们现在在讨论80X86的架构,那么我们的操作码长度会是8位的整数倍。

       另一点要考虑的是指令操作数的尺寸。一些CPU设计者(特别是 RICS 设计师)把所有操作数都包含到操作码中。而其它一些CPU设计师(CICS设计师)并不把立即数、地址偏移这类操作数作为操作码的一部分(虽然他们通常将寄存器操作数作为操作码一部分而编码)。在此,我们将不把立即数、地址偏移作为操作码的一部分。

      用8位的操作码你只能编码256条不同的指令。即使我们不把操作数作为操作码的一部分,仅有256条指令也是很少的。这里并不是说你可以用8位的操作码,大部分8位的处理器如8086都是使用8位的操作码,而是现代的处理器趋向于有大大超过256指令。下一步是使用2bytes的操作码了。用2bytes的操作码,我们可以编码65536条指令,只是说我们指令变长了。

    如果减小指令的尺寸是一个很重要的设计目标的话,我们可以使用数据压缩理论来减小指令的平均尺寸。基本的思想如下:我们可以分析为这个CPU写的程序,在一大堆应用程序中去设计每一条指令使用的频繁程度,然从以最频繁使用的最少使用的顺序作个列表。然后在我们的指令集中,把最频繁使用的那些指令用1byte来编码,次频繁的用2bytes来编码,以此类推。这样一来,虽然我们最长的指令可以达到3 4 bytes但是在应用程序中大部分会是1到2字节,如此操作码的平均长度就在1到2字节之间。这把我们选用2字节来编码每条指令所生成的程序大小要小得多。


     虽然使用可变多的指令可以让我们得到更小程序,但是这是有代价的。首先,解码这些指令更加的复杂。在解码一条指令之前,CPU必须先解码这条指令的长度。这个额外的步骤消耗时间并可能影响CPU的整体性能。另一个问题是:这使得在管道解码多条指令变得非常困难(由于没法得到在指令队列中各个指令的分界线)这两原因再加上其它一些原因解释了为什么大部分流行的RICS架构避免使用可变成长的指令。然后,对我们而言,为了节省内存空间我们就使用变长的指令来实现。

       在真正选择指令来实现你的CPU之前,现在先来打算打算未来吧。毫无疑问,你要去发现将来在哪些场合将会使用的指令,并为这些指令保留一些操作码。如果你使用上图所示来编码你的指令,那么这将是一个好主意:保存一块64个不同指令的单字节操作码,一半的双字节指令操作码,一半的三字节指令操作码。特别的,保留单字节的操作码看起来相当的奢侈,但是历史证明这是非常有远见的。

       第二步是选择你要实现的指令。虽然我们为将来的扩展需要保留了近一半的操作码,但我们也不必实现所有剩下的操作码。我们可以选择预留一定数目的操作码而不并实现。好的实现不在于我们能否很快的用掉所有的操作码而在于即使存在一些限制(如硅)我们有一整套完全而又考虑良好的指令集.。我们要知道,新添一条指令要比移除一条指令简单得多。在这次讨论得到的结论就是:一开始简单的设计要比复杂的设计好得多。

      第一步骤是选择一些一般的指令类型。作为第一次尝试,你应该把你选择的指令限制在那些常用的范围之内。要得到这方面的参考最好的去处就是去看看其它处理器的指令集。例如,大部分的处理器都会实现如下的一些指令:

               数据移动指令(eg mov)

               算术及逻辑运算指令(eg add,sub,and ,or ,not)

               比较指令

               一系列的条件转移指令

               输入\输出指令

               其他各种指令

      你的目标就如同最初指令集的CPU设计师一样,选择一套合理的指令集以便程序员可以高效率的来写程序。(指令集中指令应该尽可能的少)。这个决定是非常具有战略意义的。CPU设计师的工作不在于设计出最好的指令集而在于设计一套在现有局限下理想的指令集。

      一旦你决定了哪些指令将出现在你最初的指令集里头,下一步就是将操作码映射给这些指令了。第一步是以指令的相同属性来对这些指令进行分类成组。例如:add 指令和sub指令处理相同的操作数类型,则应该把这两个指令放在同一组里。另一方面,not指令和neg指令都只需一个操作数,所以他们也应该在同一组里,

      一旦你把指令分门别类成组后,下一步就是来对它们进行编码了。一个典型的编码规则是:用几个位来确实指令是属于哪个组的,几个位和确定在组中的哪条指令,几个类来确实指令允许的操作数类型(寄存器、内存、立即数)。编码这些信息所需要的位对指令的尺寸有直接的影响,没法考虑到指令使用的频繁程度。例如:如果用两个bit来选择一个组,用四个bit来选择组内的一条指令,六个bit来指令操作数的类型,你肯定是无法用8bit的操作码来映射这条指令了。另一方面,如果你想做的只是将八个通用寄存器中的一个压入堆栈,你可以用四个bit来选择push 三个bit来选择寄存器。

       编码操作数通常是一个大问题,因为很多指令都允许大量的操作数。例如,80X86的MOV指令就需要2bytes的操作码。然而,INTEL注意到mov(mov disp eax   ) 或mov eax,disp 非常的常用,INTEL就有这条指令的另一个版本,这个版本只用一个字节从而来减小大量使用这条指令的程序大小。要注意的是,INTEL并未移除这些指令的两个字节版本,只是说有两条不同的指令可以实现这个动作,一条指令使用1字节,而另一条需要两个字节。

      为了进一步讨论,我们将使用一个例子。下节,我们将小小的实战一下设计一个简单指令集。


ART和Dalvik的比较

ART是什么?他和Dalvik是什么关系?
  • u012481172
  • u012481172
  • 2016年05月03日 15:03
  • 705

深入理解ART虚拟机—虚拟机的启动

看art虚拟机也有一段时间了,是时候写点什么出来了。早先看art的时候,发现不是太能理解,所以就恶补了一下dalvik虚拟机,所以有了之前的dalvik系列,等再次回头看art的时候,确实轻松了不少。...
  • threepigs
  • threepigs
  • 2016年10月11日 13:50
  • 3808

Dalvik和ART简介

1、classes.dex文件初识     我们先把QQ_236.apk后缀改为QQ_236.zip,然后解压,发现有一个classes.dex文件,这个classes.dex是java源码编译后生成...
  • guoqifa29
  • guoqifa29
  • 2015年07月02日 14:33
  • 1460

深入理解ART虚拟机—ART的函数运行机制

前面两篇文章介绍了ART的启动过程,而在启动之后,我们感兴趣的就是ART是怎么运行的。回顾一下虚拟机系列的前面几篇文章,我们可以理一下思路: 一,apk以进程的形式运行,进程的创建是由zygote。 ...
  • threepigs
  • threepigs
  • 2016年11月04日 16:09
  • 2041

ART和Dalvic对比

ART和Dalvic是我最近要接入热修复的时候,查看AndFix源码才发现的(后知后觉很严重了),既然已经落后了,那就去官网去查查这是什么东东。Android Runtime(缩写为ART),是一种在...
  • baidu_17508977
  • baidu_17508977
  • 2016年12月14日 15:25
  • 430

系统学习机器学习之神经网络(五) --ART

原文:http://blog.sina.com.cn/s/blog_7671b3eb0100xnk1.html 分析的方法主要有两种—统计聚类法和神经网络法。统计聚类法包括系统聚类法、动态聚类法、模糊...
  • App_12062011
  • App_12062011
  • 2016年12月05日 11:19
  • 2301

Android art模式解析

Android art模式解析 本文主要针对android系统art模式下面从安装apk到运行apk的一个过程,主要有一下几个方面: Art虚拟机介绍 安装时dex文件转化为oat文件 oat文...
  • xwl198937
  • xwl198937
  • 2015年12月14日 19:12
  • 2753

JVM、DVM(Dalvik VM)和ART虚拟机对比

本文在于帮助大家快速的有一定深度的了解Android虚拟机。如果读者期望更加深入的了解相关的内容,可以根据文末给出的参考资料继续往下学习。如果觉得文中内容有什么错误,欢迎读者朋友指正,同时如需要转载请...
  • evan_man
  • evan_man
  • 2016年09月02日 15:31
  • 3443

Dalvik和ART运行时环境的区别

Dalvik和ART运行时环境的区别在此,我并没有打算深入的学习Dalvik和ART两种方式的实现原理,只是想知道他俩的区别。之前,也是零零散散的看过,并没有总结成文字。在此,总结下。Dalvik以下...
  • watermusicyes
  • watermusicyes
  • 2016年01月16日 09:19
  • 10449

ART和Dalvik区别

Art上应用启动快,运行快,但是耗费更多存储空间,安装时间长,总的来说ART的功效就是"空间换时间"。 ART: Ahead of Time Dalvik: Just in Time ...
  • feelinghappy
  • feelinghappy
  • 2017年08月09日 11:57
  • 135
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:art of disassembly----chapter01----lesson9--Opcodes and Mnemonics---01
举报原因:
原因补充:

(最多只允许输入30个字)