编辑:希望使它更具可读性。
硬件不会将内存视为一长串无组织的字节。所有处理器,固定或可变字长,都有特定的引导方法。通常是处理器内存/地址空间中的已知地址,其中包含引导代码第一条指令的地址或第一条指令本身的地址。从那里开始,对于每条指令,当前指令的地址都是开始解码的位置。
例如,对于 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 指令分支到它,