12

以下是一些说明及其相应的编码:

55                      push   %ebp
89 e5                   mov    %esp,%ebp
83 ec 18                sub    $0x18,%esp
a1 0c 9f 04 08          mov    0x8049f0c,%eax
85 c0                   test   %eax,%eax
74 12                   je     80484b1 <frame_dummy+0x21>
b8 00 00 00 00          mov    $0x0,%eax
85 c0                   test   %eax,%eax
74 09                   je     80484b1 <frame_dummy+0x21>
c7 04 24 0c 9f 04 08    movl   $0x8049f0c,(%esp)

今天的微处理器通常是 32 位或 64 位的,我猜它们通常以 4 字节或 8 字节块的形式从内存中读取数据。但是,指令可以具有可变长度。微处理器如何解码这些指令,为什么它们的长度不是固定的以便于执行?

4

4 回答 4

11

有一个固定的指令长度有很好的理由,实现简单是最重要的。这就是为什么许多处理器确实具有固定的指令长度,例如RISC 处理器和许多早期计算机。

像 x86 这样的CISC 指令集旨在通过微码按顺序(逐步)解码。(您可以将微码视为 CISC 指令的一种解释器)这是 80 年代初设计 x86 时的最新技术。

现在这是一个问题,因为微码已经死了。x86 指令现在被分解成更小的 µ-ops,与 RISC 指令不同。但要做到这一点,必须首先解码 x86 指令。当前的 CPU 每个周期最多可解码 4 条指令。因为没有时间依次解码一条又一条指令,所以这很简单。当从指令缓存中引入一条线时,许多解码器并行解码这条线。每个可能的字节偏移都有一个指令解码器。解码后,每条指令的长度是已知的,处理器决定哪些解码器实际提供有效指令。这很浪费,但非常快。

可变指令大小会带来更多的麻烦,例如,一条指令可以跨越两个高速缓存行,甚至是内存中的两个页面。所以你的观察是正确的。今天没有人会设计像 x86 这样的 CISC 指令集。然而,一些 RISC 最近引入了第二种指令大小以获得更紧凑的代码:MIPS16、ARM-Thumb等。

于 2011-11-21T00:08:23.753 回答
7

编辑:希望使它更具可读性。

硬件不会将内存视为一长串无组织的字节。所有处理器,固定或可变字长,都有特定的引导方法。通常是处理器内存/地址空间中的已知地址,其中包含引导代码第一条指令的地址或第一条指令本身的地址。从那里开始,对于每条指令,当前指令的地址都是开始解码的位置。

例如,对于 x86,它必须查看第一个字节。根据该字节的解码,它可能需要读取更多的操作码字节。如果指令需要地址、偏移量或其他某种形式的立即数,那么这些字节也在那里。处理器很快就知道这条指令中有多少字节。如果解码显示该指令包含 5 个字节并且它从地址 0x10 开始,则下一条指令位于 0x10+5 或 0x15。这种情况永远持续下去。无条件分支,取决于处理器可以有各种形式,你不要假设指令后面的字节是另一条指令。有条件或无条件的分支为您提供了另一条指令或一系列指令在内存中开始的线索。

请注意,今天的 X86 在解码指令时绝对不会一次获取一个字节,会发生合理大小的读取,一次可能是 64 位,处理器会根据需要从中提取字节。当从现代处理器读取单个字节时,内存总线仍然会进行全尺寸读取,并且要么将所有这些位呈现在总线上,而内存控制器只提取它之后的位,或者它可能会保留该数据. 您将看到一些处理器,您可能在背靠背地址上有两个 32 位读取指令,但在内存接口上只发生一个 64 位读取。

我强烈建议您编写反汇编程序和/或模拟器。对于固定长度的指令,这很容易,您只需从头开始并在遍历内存时进行解码。固定字长反汇编程序可能有助于了解解码指令,这是此过程的一部分,但它不会帮助您理解遵循可变字长指令以及如何在不失对齐的情况下分离它们。

MSP430 作为第一个反汇编器是一个不错的选择。有 gnu 工具 asm 和 C 等(还有 llvm )。从汇编程序开始,然后是 C 或使用一些预制的二进制文件。他们的关键是您必须像处理器一样遍历代码,从重置向量开始并逐步完成。当您解码一条指令时,您知道它的长度并知道下一条指令在哪里,直到您遇到无条件分支。除非程序员故意留下陷阱来欺骗反汇编程序,否则假设所有分支条件或无条件都指向有效指令。一个下午或一个晚上是完成整个事情或至少获得概念所需要的。您不一定需要完全解码指令,也不必让它成为一个成熟的反汇编程序,只需要解码到足以确定指令的长度并确定它是否是分支以及如果是在哪里。作为 16 位指令,如果您愿意,您可以一次构建所有可能的指令位组合及其长度的表,这可能会节省一些时间。您仍然必须通过分支解码。

有些人可能会使用递归,而我使用内存映射向我显示指令的开头是哪些字节,哪些字节/字是指令的一部分,但不是第一个字节/字以及我尚未解码的字节。我首先获取中断和复位向量,并使用它们来标记指令的起点。然后进入一个循环解码指令以寻找更多起点。如果在没有其他起点的情况下通过,那么我已经完成了那个阶段。如果在任何时候我发现一个指令起点位于指令中间,则存在需要人工干预才能解决的问题。例如,拆卸旧的视频游戏 rom,你可能会看到这个,手写的汇编程序。编译器生成的指令往往非常干净和可预测。如果我用一个干净的指令内存映射和剩下的东西来解决这个问题,(假设数据)我可以通过一次知道指令在哪里,然后解码并打印出来。可变字长指令集的反汇编程序永远无法找到每条指令。如果指令集具有例如跳转表或其他某种运行时计算地址来执行,那么您不会在没有实际执行代码的情况下找到所有这些。

那里有许多现有的仿真器和反汇编器,如果您想尝试跟随而不是自己编写,我自己也有一些http://github.com/dwelch67

可变和固定字长各有利弊。Fixed 确实具有优势,易于阅读,易于解码,一切都很好,但考虑到 ram,尤其是缓存,您可以在与 ARM 相同的缓存中塞入更多的 x86 指令。另一方面,ARM 可以更轻松地解码,更少的逻辑、更少的功耗等更物有所值。从历史上看,内存很昂贵,逻辑很昂贵,一个字节就是它的工作原理。单字节操作码将您限制为 256 条指令,因此扩展为一些需要更多字节的操作码,更不用说立即数和地址,使其无论如何都成为可变字长。保持反向兼容性数十年,您最终会到达现在的位置。

为了增加所有这些混乱,例如 ARM 现在有一个可变字长指令集。Thumb 有一个可变字指令,即分支,但您可以轻松地将其解码为固定长度。但是他们创建了真正类似于可变字长指令集的 thumb2。此外,许多/大多数支持 32 位 ARM 指令的处理器也支持 16 位拇指指令,因此即使使用 ARM 处理器,您也不能简单地按字对齐数据并随时解码,您必须使用可变字长。更糟糕的是,ARM 到/从拇指的转换是通过执行来解码的,你通常不能简单地拆卸并从拇指中找出手臂。混合模式编译器生成的分支通常涉及加载带有要分支的地址的寄存器,然后使用 bx 指令分支到它,

于 2011-11-20T21:05:26.107 回答
4

我无法回答它们是如何被解码的,但我可以回答为什么它们是可变长度的。

可变长度的原因既是由于希望保持较小的代码大小以及不可预见的指令集扩展。


减少指令大小

一些指令(本质上)需要更多的空间来编码。如果所有指令都设置为足够大的固定长度以容纳这些指令,那么指令代码中将浪费大量空间。可变长度指令允许将指令“压缩”到更小的尺寸。


(不可预见的)指令集扩展

另一个原因是指令集扩展。最初,x86 只有 256 个操作码。(1 字节) 然后需要添加更多指令,因此他们丢弃了一条指令并将其操作码用作新操作码的转义字符。结果是较新的指令更长。但这是扩展指令集和保持向后兼容性的唯一方法。

至于处理器如何解码这些,这是一个复杂的过程。对于每条指令,处理器需要找到长度并从那里解码。这导致了一个固有的顺序解码过程,这是一个常见的性能瓶颈。

现代 x86 处理器具有所谓的 uop(微操作)缓存,它将解码的指令缓存到处理器更易于管理(和类似 RISC)的东西中。

于 2011-11-20T19:40:47.907 回答
2

你重新发明了RISC

嗯,您对经典 x86 (参见 CISC)的反对正是促使 RISC CPU 架构的设计者创建简单、对齐、固定大小的指令集架构的原因。

事实证明,如今的 x86 实际上确实将用户可见的 ISA 转换为更类似于 RISC 的微操作流,该流位于内部缓存中。

良好的观察力。


笔记。
1. 微操作只是一种可用的技术。在一般情况下,只要指令的解码和对齐发生在一个或多个流水线阶段,实际花费的时间不会被添加到平均指令执行时间中。如果分支预测正在工作并且流水线保持满状态,则解码和对齐指令所需的额外时间由与实际指令操作并行执行的逻辑来处理。今天的设计人员可以使用数百万个门,他们可以将大量逻辑用于解码复杂的 x86 ISA。
2.你提到了内存总线宽度;事实证明,内存路径通常也大于 32 位或 64 位。架构字长仅指 ALU 和指针大小。内存和缓存接口的实际宽度通常是架构字大小的 2 倍或 4 倍。

于 2011-11-20T19:42:03.557 回答