【问题标题】:How is machine code stored in the EXE file?机器码如何存储在 EXE 文件中?
【发布时间】:2013-01-15 20:29:13
【问题描述】:

我的问题如下:

  1. Portable Executable 格式(在 Windows/Unix 上)与一般的 x86/x64 指令集有何关系?
  2. PE 格式是否存储处理器支持的确切操作码集,还是操作系统转换为匹配 CPU 的更通用格式?
  3. EXE 文件如何指示所需的指令集扩展(如 3DNOW! 或 SSE/MMX?)
  4. 操作码在 Windows、Mac 和 unix 等所有平台上是否通用?
  5. Intel i386 兼容 CPU 芯片(如 Intel 和 AMD 的芯片)使用通用指令集。但我确信 ARM 驱动的 CPU 使用不同的操作码。这些非常非常不同还是概念相似?寄存器、int/float/double、SIMD 等?

在 .NET、Java 或 Flash 等较新的平台上,指令集是基于堆栈的操作码,JIT 在运行时将其转换为本机格式。习惯了这种格式,我想知道“旧”的原生 EXE 格式是如何执行和格式化的。例如,“寄存器”通常在较新的平台操作码中不可用,因为 JIT 将堆栈命令转换为它认为必要的 16/32 可用 CPU 寄存器。但在原生格式中,您需要按索引引用寄存器,并确定哪些寄存器可以重复使用以及频率。

【问题讨论】:

  • 请注意,操作码只是单个指令的机器代码字节或位的部分。如果有任何非隐式操作数,其余位对操作数进行编码。 (在某些圈子中,“操作码”被滥用来描述整个机器代码指令,因此这可能是该问题中术语滥用的原因。)

标签: x86 portable-executable machine-code instruction-set opcode


【解决方案1】:

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?)

虽然可执行文件可以指示其构建的指令集 (see Chris Dodd's answer),但我不认为可执行文件可以指示所需的扩展。但是,可执行代码在运行时可以检测到此类扩展。例如,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:用作第一个操作数的寄存器的索引。
  • 目标:用作目标操作数的寄存器的索引。
  • 操作数 2:第二个操作数。当R/I为0时,寄存器的索引。 R/I 为 1 时,无符号 8 位常量值。除了其中任何一个之外,操作数 2 中的一些位指示该值是否被移位/旋转。

有关更多详细信息,您应该阅读您想了解的特定 ARM 版本的文档。我在这个例子中使用了这个ARM7TDMI-S Data Sheet, Chapter 4

请注意,每条 ARM 指令,无论多么简单,都需要 4 个字节进行编码。由于可能的开销,现代 ARM 处理器允许您使用称为 Thumb 的替代 16 位指令集。它不能表达 32 位指令集所能表达的所有东西,但它也只有一半。

另一方面,x86-64 指令具有可变长度编码,并使用各种修饰符来调整单个指令的行为。如果您想将 ARM 指令与 x86 和 x86-64 指令的编码方式进行比较,您应该阅读我在 OSDev.org 上写的 x86-64 Instruction Encoding 文章。


您最初的问题非常广泛。如果你想知道更多,你应该做一些研究,并用你想知道的具体事情创建一个新问题。

【讨论】:

  • 完美而彻底的答案,Virtlink!你能添加一些关于 ARM 指令集的详细信息吗?我读到了每个操作码都有条件和其他独特的功能。你也能描述一下吗?
  • 您说“PE 文件通常存储一个特定处理器系列和操作系统系列的机器代码”但是同一个 .exe 文件如何在不同 CPU 的 Windows 笔记本电脑上运行?
  • @Maysara 我认为这些文件只包含我谈到的 .NET 虚拟机代码。这些指令并不特定于特定的处理器或操作系统,而是在程序运行时即时编译(即时)。它们看起来像独立的可执行文件,但如果系统上没有 .NET 运行时,它们就无法运行。示例包括为 WinRT、UWP 应用、Xamarin 和 Mono 编写的应用程序。
  • 我不相信可执行文件可以指示所需的扩展。正确。如果您在 Win 或 Linux 上使用 gcc -O3 -march=haswell 编译,则没有元数据可以帮助您避免在没有 AVX 或 BMI2 或其他任何东西的旧 CPU 上运行它。不过,我认为 MacOS 可能会在其 Mach-O64 可执行文件格式中对此提供一些支持;它们支持带有x86_64 and x86_64h "slices" 的胖二进制文件,我认为操作系统可以选择哪个切片,而程序本身不必根据 CPUID 进行自己的运行时调度。
  • @MaysaraAlhindi:Intel 和 AMD 或 Pentium 4 vs. Sandybridge 都是 x86-64(嗯,一些 P4 CPU 只是 32 位,但 x86-64 完全向后兼容 x86-32) . "family" 用于对相同架构 (ISA) 的 CPU 进行分组,不是 微架构系列(例如从 Pentium Pro 到 Nehalem 的 P6 系列,或 Bulldozer-family,或 Zen,或 Sandybridge -家庭从 SnB 到(目前)冰湖/阳光湾。)
【解决方案2】:

PE 文件格式(以及非 Windows 机器上的 ELF/COFF 文件格式)定义了一个出现在文件开头的标题,在这个标题中,有一个“机器”代码。在 PE 文件中,'Machine' 代码为 2 个字节,规范为各种机器定义了一堆常量:

0x1d3   Matsushita AM33
0x8664  AMD x64
0x1c0   ARM little endian   
0x1c4   ARMv7 (or higher) Thumb mode only
0xebc   EFI byte code   
0x14c   Intel 386 or later processors and compatible processors 
0x200   Intel Itanium processor family  
0x9041  Mitsubishi M32R little endian   
0x266   MIPS16  
0x366   MIPS with FPU
0x466   MIPS16 with FPU 
0x1f0   Power PC little endian  
0x1f1   Power PC with floating point support    
0x166   MIPS little endian  
0x1a2   Hitachi SH3 
0x1a3   Hitachi SH3 DSP 
0x1a6   Hitachi SH4 
0x1a8   Hitachi SH5     
0x1c2   ARM or Thumb (“interworking”)   
0x169   MIPS little endian WCE v2   

然后,在 PE(或 ELF)文件中有一个或多个包含(二进制)机器代码的“代码”部分。该代码被加载到内存中并由 CPU 直接执行。操作系统或动态链接器/加载器(执行实际加载)知道它在哪台机器上运行,因此它会在尝试加载和执行代码之前检查标头中的“机器”代码以确保它匹配。如果不匹配,可执行文件将被拒绝,因为它无法运行。

【讨论】:

  • 所以根据我的理解,PE可以包含几个“代码段”来支持几个拱门?这不就是胖二进制的概念吗?
  • @MaysaraAlhindi 没有,you can't have fat binaries on Windows
猜你喜欢
  • 2010-10-06
  • 1970-01-01
  • 2011-02-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多