指令长度可变时的指令解码

scd*_*dmb 11 assembly microprocessors

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

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)
Run Code Online (Sandbox Code Playgroud)

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

Mac*_*ser 8

具有固定指令长度的原因非常充分,实现简单性是最重要的.这就是为什么许多处理器确实具有固定的指令长度,如RISC处理器和许多早期计算机.

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

现在这是一个问题,因为微码已经死了.x86指令现在分为更小的μ-ops,与RISC指令不同.但要这样做,必须首先解码x86指令.目前的CPU每个周期最多可解码4条指令.因为没有时间按顺序解码一个接一个的指令,所以这只是通过暴力行为.当从指令高速缓存引入一行时,许多解码器并行解码该行.每个可能的字节偏移处的一个指令解码器 在解码之后,每条指令的长度是已知的,并且处理器决定哪些解码器实际提供有效指令.这很浪费,但速度很快.

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

  • 对 x86 的描述稍有偏差。当引入行时,AMD CPU 使用指令*长度*(也称为边界)注释 L1i 缓存,但实际的读取/解码从对齐的 16 或 32 字节读取块(取决于微体系结构)开始。仅当执行到达尚未解码的指令时,才会将解码结果缓存在已解码的 uop 缓存(Sandybridge 和 Zen 系列)中,从而在常规解码期间动态构建 uop 缓存线。请参阅 https://www.realworldtech.com/sandy-bridge/ 和 https://agner.org/optimize/(特别是 Agner 的 microarch pdf) (2认同)

old*_*mer 7

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

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

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

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

我强烈建议您编写反汇编程序和/或模拟器.对于固定长度的指令,它非常简单,您只需从头开始并在通过内存时进行解码.固定字长反汇编程序可能有助于学习解码指令,这是此过程的一部分,但它不会帮助您理解跟随变量字长指令以及如何分离它们而不会失去对齐.

MSP430是第一个反汇编器的不错选择.有gnu工具asm和C等(并且llvm就此而言).从汇编程序开始,然后是C或者使用一些预先制作的二进制文件.它们的关键是你必须像处理器一样走代码,从复位向量开始,然后一路走来.当您解码一条指令时,您知道它的长度并知道下一条指令的位置,直到您触及无条件分支.除非程序员故意留下陷阱来欺骗反汇编程序,否则假定所有分支都有条件或无条件地指向有效指令.只需要一个下午或晚上就能完成整个过程或至少获得概念.你不一定需要完全解码指令,不必使这个成为一个完整的反汇编程序,只需要解码足以确定指令的长度并确定它是否是一个分支,如果是的话.作为一个16位指令,如果您选择,您可以一次构建一个包含所有可能指令位组合及其长度的表,这可以节省一些时间.你仍然需要通过分支解码你的方式.

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

有很多现有的模拟器和反汇编器,如果你想尝试跟随而不是自己编写,我自己有几个http://github.com/dwelch67.

变量和固定字长有利有弊.固定具有确定的优点,易于阅读易于解码,一切都很好和正确,但想想ram,特别是缓存,你可以在与ARM相同的缓存中塞入更多的x86指令.另一方面,ARM可以更容易地解码,更少的逻辑,功率等更多的解决方案.从历史上来说,内存昂贵,逻辑价格昂贵,而且一个字节就是它的工作方式.单字节操作码将您限制为256条指令,因此扩展为需要更多字节的某些操作码,更不用说使其无论如何都变量字长的立即数和地址.保持反向兼容性几十年,你最终到达现在的位置.

为了增加所有这些混淆,ARM现在具有可变字长指令集.Thumb有一个单变量字指令,分支,但你可以很容易地将其解码为固定长度.但他们创建了thumb2,它确实类似于可变字长指令集.此外,许多/大多数支持32位ARM指令的处理器也支持16位拇指指令,因此即使使用ARM处理器,您也不能简单地按字对齐数据并进行解码,因此必须使用可变字长.更糟糕的是ARM向/从拇指过渡通过执行来解码,你通常不能简单地从拇指拆卸和弄清楚手臂.混合模式编译器生成的分支通常涉及加载具有地址的寄存器进行分支,然后使用bx指令分支到它,因此反汇编程序需要查看bx,向后看执行分支中使用的寄存器和希望你在那里找到一个负载,并希望它是从它加载的.text段.