【问题标题】:x86 decoding instruction opcode bytex86 解码指令操作码字节
【发布时间】:2017-09-14 23:42:27
【问题描述】:

我正在创建一个 x86 解码器,我正在努力理解和寻找一种有效的方法来计算指令的助记符。

我知道操作码 6 MSB 是操作码位,但我在助记符表中找不到使用这 6 位的任何地方。我找到的唯一助记表是整个操作码字节本身,而不仅仅是 6 个 MSB。

我想问有哪些有效的方法可以继续解码操作码字节中编码的助记符,以及是否有使用 6 个 MSB 而不是整个操作码字节的表引用。

【问题讨论】:

  • 低 2 位也是操作码的一部分...例如,jcc 指令对于从 0x70 到 0x7F 的每个操作码值都有不同的助记符。事实上,有时 ModR/M 字节中的 /r 字段也是操作码的一部分。 (例如 shlshr)。
  • 现代 x86 机器码的问题在于没有一种有效/简单的解码方法。例如,rep nop 实际上解码为pause,或者rep bsf 解码为tzcnt(如果支持BMI1,否则解码为bsf)。所以你必须检查其他指令的强制性前缀。
  • @PeterCordes 我使用的资源之一是c-jump.com/CIS77/CPU/x86/X77_0050_add_opcode.htm 我知道操作码字节的唯一 6 个 MSB 何时不代表助记符但对于常规指令似乎是这样的根据他们所说的方式。我在问这些常规情况下,我如何使用这 6 个 MSB 来确定助记符,就像他们在示例中所做的那样。
  • 你的意思是像 C 语言中的const char *mnemonic = table[(uint8_t)opcode>>2];?你就那样做。尽管实际上您可能需要structs 的 256 项表,其中一个成员是enum,说明它是什么类型的指令(或指向将解码其余字节的函数的函数指针) .
  • @PeterCordes 是的,但是当在线查看助记符表时,我找不到确定真正使用哪个助记符的方法。例如,当opcode 为 0x6(推送)时,使用const char *mnemonic = table[(uint8_t)opcode>>2]; 查看此表sparksandflames.com/files/x86InstructionChart.html,如果我要 rsh 2 个字节,它很容易被误认为是 0x5(添加)

标签: data-structures x86 disassembly opcode mnemonics


【解决方案1】:

但是没有一种有效的方法来存储一个不重复的助记词表吗?

这已成为算法和数据结构的问题。

正如您所指出的,许多操作码表条目(至少对于没有0f 转义字节的表:http://sparksandflames.com/files/x86InstructionChart.html)确实以 4 或 2 组重复,即具有相同的 6 位或 7 位前缀选择相同的助记词。

显然,一个 256 条目的结构表很简单,但会重复。它非常快速且易于使用,因为它可能仍然足够小,不会经常缓存丢失。 (特别是因为公共条目会在缓存中保持热状态;x86 代码经常使用相同的操作码。)

您可以用简单性/性能换取空间。

您可以有一个 64 条目的结构表,其中一个成员是指向要使用低 2 位索引的辅助表的指针。如果指针为NULL,则表示指令遵循add/and/xor/等模式,其中低2位告诉您8位与操作数大小和方向(r/ m,reg 或 reg,r/m)。

当存在某些前缀时(例如,rep noppause),您的结构还需要用于转换为其他指令的条目。此外,AVX VEX 前缀使用了过去对另一条指令无效的编码。如果你想为所有当前扩展做一个完整的工作,x86 解码是相当疯狂的。

实际上,只使用函数指针表可能是最简单(也很有效)的。或者一个带有const char* mnemonicint (*decode)(const char*mnemonic, const char *insn_bytes, unsigned prefix_bitmap) 函数的结构,所以很多操作码可以指向同一个解码函数,但仍然得到不同的助记符。有时该函数会忽略传递的助记符,但有时这就是它所需要的全部。您将拥有一个用于解码许多解码函数会调用的寻址模式的通用函数。

这与您实现解释的 x86 仿真器的方式非常相似,而不是进行动态重新编译。一个普通的解码循环,然后通过函数指针进行调度。


您可能使用的更复杂的数据结构是 radix trie 又名前缀树。另见https://en.wikipedia.org/wiki/Trie#Bitwise_tries

这是进入愚蠢的季节,因为密度如此之高,查找表更有意义。 (未定义的操作码很少)。

【讨论】:

  • 所以如果我的目标是性能(速度方面),你会说只存储一个 256 条目表是最好的选择吗?另外为什么我需要一个结构?我想可能会创建一个所有助记符的枚举,然后创建一个 256 条目表作为该枚举的索引表,你的想法是什么?
  • 是的,我猜 256 条目表的性能最好。选择额外解码步骤的额外分支不太值得。
  • @Jorayen:除非您有一个单独的表用于其他特殊情况,否则您将使用一个结构来保存助记符并告诉您如何将剩余的字节解码为操作数。 (例如jcc/call vs. add vs. mul r/m32 vs. imul r,r/m32,imm32 vs. 其他特殊情况)。对于特殊情况,ModR/M 中的 /r 字段中还有 3 个操作码位,以表明这一点(例如,使用指向另一个表的指针)。拥有一个在每次访问时都使用大部分成员的结构可以提供良好的空间局部性,因此它可以很好地缓存。
  • 是的,我想问你关于额外分支的事情,因为并非所有操作码似乎都遵循类似的模式,因为例如使用累加器寄存器或推送/弹出指令的替代编码来自/到堆栈的段,例如0x04 操作码,它是add *AL*, imm8,尽管根据通过解码0x04 的正常解码步骤,人们会期望这是对内存位置的add 操作,因为@ 987654342@ 标志设置为 0,而不是 imm 到 reg add 操作,因为 MSB 未设置为 1。您对处理这些有什么想法
  • @Jorayen:对于立即操作数指令还有另一种模式,使用 modr/m 中的 /r 字段将它们打包成几个操作码。我将使用一个由操作码字节索引的 256 条目表(另一个用于跟随 0f 转义字节的操作码)。对于给定的操作码,首先检查前缀是否进入另一条指令。然后检查函数指针是否为非 NULL。如果是,则调用它(以指令字节、助记符和struct* 作为参数,以指令长度作为返回值)。如果不是,请检查结构中的标志以查找 /r = opcode bits 模式。
猜你喜欢
  • 1970-01-01
  • 2016-10-03
  • 2022-11-23
  • 2014-12-23
  • 2013-07-26
  • 1970-01-01
  • 2016-08-18
  • 1970-01-01
  • 2021-10-23
相关资源
最近更新 更多