ARM 操作码与 x86 操作码有很大不同吗?
是的,他们是。您应该假设不同处理器系列的所有指令集完全不同且不兼容。指令集首先定义一种编码,它指定以下一个或多个:
- 指令操作码;
- 寻址方式;
- 操作数大小;
- 地址大小;
- 操作数本身。
编码进一步取决于它可以寻址多少寄存器,是否必须向后兼容,是否必须快速解码,以及指令的复杂程度。
关于复杂性:ARM 指令集要求使用专门的加载/存储指令将所有操作数从内存加载到寄存器并从寄存器存储到内存,而 x86 指令可以将单个内存地址编码为其操作数之一,因此没有单独的加载/存储指令。
然后是指令集本身:不同的处理器会有专门的指令来处理特定的情况。即使两个处理器系列对同一事物(例如一条add
指令)具有相同的指令,它们的编码也非常不同,并且语义可能略有不同。
如您所见,由于任何 CPU 设计人员都可以决定所有这些因素,这使得不同处理器系列的指令集架构完全不同且不兼容。
寄存器、int/float/double 和 SIMD 在不同架构上的概念是否非常不同?
不,它们非常相似。每个现代架构都有寄存器并且可以处理整数,并且大多数可以处理某种大小的 IEEE 754 兼容浮点指令。例如,x86 体系结构具有 80 位浮点值,这些值被截断以适合您知道的 32 位或 64 位浮点值。SIMD 指令背后的想法在所有支持它的架构上也是相同的,但许多不支持它,并且大多数对它们有不同的要求或限制。
操作码在 Windows、Mac 和 Unix 等所有平台上是否通用?
给定三个 Intel x86 系统,一个运行 Windows,一个运行 Mac OS X,一个运行 Unix/Linux,那么是的,操作码完全相同,因为它们运行在同一个处理器上。但是,每个操作系统都不同。内存分配、图形、设备驱动程序接口和线程等许多方面都需要操作系统特定的代码。因此,您通常无法在 Linux 上运行为 Windows 编译的可执行文件。
PE 格式是否存储处理器支持的确切操作码集,还是操作系统转换为匹配 CPU 的更通用格式?
不,PE 格式不存储操作码集。如前所述,不同处理器系列的指令集架构差异太大,无法实现这一点。PE 文件通常存储一个特定处理器系列和操作系统系列的机器代码,并且只能在此类处理器和操作系统上运行。
但是有一个例外:.NET 程序集也是 PE 文件,但它们包含不特定于任何处理器或操作系统的通用指令。这样的 PE 文件可以在其他系统上“运行”,但不能直接运行。例如, Linux 上的mono可以运行这样的 .NET 程序集。
EXE 文件如何指示所需的指令集扩展(如 3DNOW! 或 SSE/MMX?)
虽然可执行文件可以指示为其构建的指令集(请参阅 Chris Dodd 的回答),但我不相信可执行文件可以指示所需的扩展。但是,可执行代码在运行时可以检测到此类扩展。例如,x86 指令集有一条CPUID
指令返回该特定 CPU 支持的所有扩展和功能。可执行文件只会测试它并在处理器不满足要求时中止。
.NET 与本机代码
您似乎对 .NET 程序集及其指令集(称为 CIL(通用中间语言))有所了解。每条 CIL 指令都遵循特定的编码,并将评估堆栈用于其操作数。CIL 指令集保持非常通用和高级。当它运行(在 Windows 上由mscoree.dll
,在 Linux 上由mono
)并调用方法时,即时 (JIT) 编译器将获取该方法的 CIL 指令并将它们编译为机器代码。根据操作系统和处理器系列,编译器必须决定使用哪些机器指令以及如何对其进行编码。编译后的结果存储在内存中的某处。下次调用该方法时,代码会直接跳转到已编译的机器代码,并且可以像本机可执行文件一样高效地执行。
ARM 指令是如何编码的?
我从未使用过 ARM,但通过快速浏览文档,我可以告诉您以下内容。一条 ARM 指令的长度始终为 32 位。有许多特殊的编码(例如,用于分支和协处理器指令),但 ARM 指令的一般格式是这样的:
31 28 27 26 25 21 20 16
+---+---+---+---+---+---+---+---+---+---+---+---+- --+---+---+---+--
| 条件 | 0 | 0 |R/I| 操作码 | 小号 | 操作数 1 | ...
+---+---+---+---+---+---+---+---+---+---+---+---+- --+---+---+---+--
12 0
--+---+---+---+---+---+---+---+---+---+---+---+--- +---+---+---+---+
... | 目的地 | 操作数 2 |
--+---+---+---+---+---+---+---+---+---+---+---+--- +---+---+---+---+
这些字段的含义如下:
- 条件:当为真时,导致指令被执行的条件。这会查看零、进位、负数和溢出标志。设置为 1110 时,始终执行该指令。
- R/I : 当为 0 时,操作数 2为寄存器。为1 时,操作数 2为常数值。
- 操作码:指令的操作码。
- S : 当为 1 时,根据指令的结果设置零、进位、负数和溢出标志。
- Operand1:用作第一个操作数的寄存器的索引。
- Destination:用作目标操作数的寄存器的索引。
- 操作数 2:第二个操作数。当R/I为 0 时,寄存器的索引。当R/I为 1 时,无符号 8 位常数值。除了其中任何一个之外,操作数 2 中的一些位指示该值是否被移位/循环。
有关更多详细信息,您应该阅读您想了解的特定 ARM 版本的文档。我在这个例子中使用了这个ARM7TDMI-S 数据表,第 4 章。
请注意,每条 ARM 指令,无论多么简单,都需要 4 个字节进行编码。由于可能的开销,现代 ARM 处理器允许您使用称为Thumb的替代 16 位指令集。它不能表达 32 位指令集所能表达的所有东西,但它也只有一半大。
另一方面,x86-64 指令具有可变长度编码,并使用各种修饰符来调整单个指令的行为。如果您想将 ARM 指令与 x86 和 x86-64 指令的编码方式进行比较,您应该阅读我在 OSDev.org 上撰写的x86-64 指令编码文章。
您最初的问题非常广泛。如果你想知道更多,你应该做一些研究,并用你想知道的具体事情创建一个新问题。