【问题标题】:Instruction decoding when instructions are length-variable指令长度可变时的指令解码
【发布时间】:2012-01-02 11:54:06
【问题描述】:

以下是一些指令及其对应的编码:

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 字节块的形式从内存中读取数据。但是,指令可以具有可变长度。微处理器如何解码这些指令?为什么它们的长度不是固定的以方便执行?

【问题讨论】:

  • 仅供参考:它们实际上通常一次获取 16 (Core2) 或 32 (K10) 字节。
  • @harold,指令长度是否受处理器字长(处理器架构)的限制,在这种情况下,现代处理器的最大长度是 32 位或 64 位(不是字节)?
  • @DenysS。不一定,也不是在 x86 中。这更像是一个 RISC 功能。 x86 指令目前被限制为 15 个字节(这是一个奇数,实际上很难达到),这个限制曾经更低(但它总是大于字长)。
  • @harold,好的,但是字长必须限制指令操作数和寄存器的大小,对吧? (我从 wiki 得到的)否则是什么意思呢?
  • @DenysS。确实,通用寄存器与字长一样宽(或者,好吧,无论如何,这已经足够了)并且指针大小和“任何地方的最大立即参数”仍然匹配那些(尽管在 64 位模式下,唯一的 64 位立即数如果您想将地址计数为立即数,那么任何地方都是对通用寄存器的简单加载和对偏移量的特殊加载,ALU 操作中的立即数被卡在 32 位)。不过现在也有 256 位向量寄存器,您可以在 16 位模式下使用 32 位寄存器,因此 x86 是“字长”含义的一个糟糕示例。

标签: assembly cpu-architecture microprocessors


【解决方案1】:

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

CISC instruction sets 像 x86 一样被设计为由microcode 按顺序(逐步)解码。 (您可以将微码视为 CISC 指令的一种解释器)这是 80 年代初期设计 x86 时的最新技术。

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

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

【讨论】:

  • x86 的描述有点不对劲。 AMD CPU 在引入行时使用指令 length (又名边界)注释 L1i 高速缓存,但实际提取/解码从对齐的 16 或 32 字节提取块开始(取决于微架构)。缓存解码导致解码的微指令缓存(Sandybridge 和 Zen 系列)仅在执行到达尚未解码的指令时发生,在常规解码期间动态构建微指令缓存行。请参阅 realworldtech.com/sandy-bridgeagner.org/optimize(特别是 Agner 的 microarch pdf)
【解决方案2】:

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

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

例如,对于 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 指令分支到它,因此反汇编器需要查看 bx,在执行中向后查看分支中使用的寄存器和希望你在那里找到一个负载,并希望它是从中加载的 .text 段。

【讨论】:

    【解决方案3】:

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

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


    减少指令大小

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


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

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

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

    现代 x86 处理器具有所谓的 uop(微操作)缓存,可将解码后的指令缓存到处理器更易于管理(并且类似于 RISC)的内容中。

    【讨论】:

    • 8086 有 256 个操作码是正确的。它没有#UD 非法指令错误,因此每个字节序列都解码为something,但其中一些没有记录。 (并且通常解码与另一个具有类似位模式的操作码相同,尽管一些像 SALC 是唯一的并且仍然没有记录。)因此没有任何 记录 指令或编码可以丢弃。 186 甚至能够引入一些新的 1 字节操作码指令,例如 push imm8push imm16 不需要 0F 转义字节;直到后来 186 用完更多的编码空间之后才需要它。
    【解决方案4】:

    你重新发明了RISC

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

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

    良好的观察力。


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

    【讨论】:

    • +1 有一个警告:你提到 RISC 却没有提到 CISC。为不知情的人折腾一些 Wiki 链接。
    • 添加了 Roger、RISC 和 CISC 链接。
    • ARM 等现代 RISC CPU 也有 decode instructions into micro-ops。而且大多数现代 RISC 架构都有替代的可变长度指令格式来增加代码密度
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-06-27
    • 2014-08-07
    • 2020-08-04
    • 2012-07-14
    • 2019-04-26
    • 2019-03-11
    • 1970-01-01
    相关资源
    最近更新 更多