机器代码如何存储在EXE文件中?

2023-11-20

我的问题如下:

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

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


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,那么yes操作码完全相同,因为它们在同一处理器上运行。然而,每个操作系统都是不同的。内存分配、图形、设备驱动程序接口和线程等许多方面都需要操作系统特定的代码。因此,通常无法在 Linux 上运行为 Windows 编译的可执行文件。

PE 格式是否存储处理器支持的确切操作码集,或者是操作系统转换以匹配 CPU 的更通用格式?

不,PE 格式不存储操作码集。如前所述,不同处理器系列的指令集架构差异太大,无法实现这一点。 PE 文件通常存储特定处理器系列和操作系统系列的机器代码,并且只能在此类处理器和操作系统上运行。

但有一个例外:.NET 程序集也是 PE 文件,但它们包含不特定于任何处理器或操作系统的通用指令。这样的PE文件can可以在其他系统上“运行”,但不能直接运行。例如,mono在 Linux 上可以运行此类 .NET 程序集。

EXE 文件如何指示所需的指令集扩展(例如 3DNOW!或 SSE/MMX?)

虽然可执行文件可以指示其构建的指令集(看看克里斯·多德的回答),我不相信可执行文件可以指示所需的扩展。然而,可执行代码在运行时可以检测到此类扩展。例如,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
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+--
|   Condition   | 0 | 0 |R/I|    Opcode     | S |   Operand 1   | ...
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+--

                   12                                               0
  --+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
... |  Destination  |               Operand 2                       |
  --+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
  

这些字段的含义如下:

  • 健康)状况:条件为真时,会导致执行指令。它着眼于零、进位、负数和溢出标志。当设置为 1110 时,始终执行该指令。
  • R/I:当 0 时,操作数2是一个寄存器。当 1 时,操作数2是一个常数值。
  • Opcode:指令的操作码。
  • S:当为 1 时,根据指令的结果设置零、进位、负数和溢出标志。
  • Operand1:用作第一个操作数的寄存器的索引。
  • 目的地:用作目标操作数的寄存器的索引。
  • 操作数2:第二个操作数。什么时候R/I是0,寄存器的索引。什么时候R/I为 1,无符号 8 位常量值。除了其中任何一个之外,操作数 2 中的一些位还指示该值是否移位/循环。

有关更详细的信息,您应该阅读您想要了解的特定 ARM 版本的文档。我用过这个ARM7TDMI-S 数据表,第 4 章对于这个例子。

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

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


你原来的问题非常广泛。如果您想了解更多,您应该做一些研究并针对您想了解的具体内容提出一个新问题。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

机器代码如何存储在EXE文件中? 的相关文章

  • "rdtsc": "=a" (a0), "=d" (d0) 这是做什么的? [复制]

    这个问题在这里已经有答案了 我是 C 和基准测试的新手 我不明白这部分代码的作用是什么 所以我发现了一些关于 edx eax 寄存器的信息 但我不完全理解它是如何在代码中发挥作用的 所以我理解这段代码本质上返回了CPU周期的当前滴答声 那么
  • 查看x86架构中的cpu缓存内容

    如何查看或转储基于 x86 的架构的 cpu 缓存内容 每次进行缓存刷新时 我如何才能看到刷新了什么 在哪里 你不能 真的 CPU 缓存被设计为对于 CPU 上运行的代码是透明的 它具有加快代码执行速度的效果 但 CPU 管理有关缓存的所有
  • 问题:gcc 从我的 C++ 生成的程序集

    编译这段代码 int main return 0 using gcc S filename cpp 生成此程序集 file heloworld cpp text globl main type main function main LFB0
  • 汇编语言中数组的冒泡排序

    我需要对一个无组织的数组进行冒泡排序 其中包含从最大到最小的 7 个整数 因此它看起来像 9 6 5 4 3 2 1 我通过编译器运行我的代码 它说 我不明白这段代码有什么问题 code segment assume ds code cs
  • 算术恒等式和 EFLAGS

    因为 x not x 1 意味着 a b a not b 1 那么 sub rax rcx 相当于 mov temp rcx not temp add rax temp add rax 1 其中 temp 是一些被认为是易失性的寄存器 换句
  • 计算 Amd Zen 2 处理器上的 L3 缓存访问事件

    我试图找出与 perf stat 命令一起使用的事件来计算 AMD Zen 2 处理器上的 L3 缓存访问次数 根据 PPR http developer amd com wordpress media 2017 11 54945 PPR
  • 如何在汇编器中实现相对 JMP (x86)?

    在为 x86 平台构建汇编程序时 我遇到了一些编码问题JMP操作说明 OPCODE INSTRUCTION SIZE EB cb JMP rel8 2 E9 cw JMP rel16 4 because of 0x66 16 bit pre
  • 调用always_inline‘_mm_mullo_epi32’时内联失败:目标特定选项不匹配

    我正在尝试使用 cmake 编译 C 程序 该程序使用 SIMD 内在函数 当我尝试编译它时 出现两个错误 usr lib gcc x86 64 linux gnu 5 include smmintrin h 326 1 错误 调用alwa
  • 字大小及其指示

    请参阅下面关于各种指令集架构中的字长以及它与汇编语言的关系的问题 感谢您提供的所有帮助 先说几个事实 如有错误 请指正 处理器架构的字长表示 编辑 其中一些是错误的 请参阅下面 Seva 的帖子 每个寄存器的最大尺寸 每个内存地址的最大尺寸
  • 分配内存空间的宏

    我需要让一个汇编程序员来计算帕斯卡三角形 https en wikipedia org wiki Pascal 27s triangle 这样帕斯卡三角形的每一行都与其他行分开存储在内存中 我想做一个 但我不知道如何使用宏在汇编中做到这一点
  • 跨 AVX 通道的最佳方式是什么?

    有些问题具有类似的标题 但我的问题涉及其他地方未涵盖的一个非常具体的用例 我有 4 个 128d 寄存器 x0 x1 x2 x3 我想将它们的内容重新组合在 5 个 256d 寄存器 y0 y1 y2 y3 y4 中 以准备其他计算 on
  • 汇编编程语言:程序仅当输入为 ESC 时退出,并在退出前要求确认(y/n),否则循环

    我只是汇编语言编程的初学者 我们的第一个任务是让程序仅在输入为 ESC 时退出 退出之前请求确认 y n 否则循环 我知道 ESC 在 ASCII 代码中具有等效值 但我对插入位置或是否需要添加更多内容感到困惑 请帮我 这是程序 model
  • 编写 AMD64 SysV 程序集时使用哪些寄存器作为临时寄存器?

    我正在使用实现一个功能cpuid根据 AMD64 SysV ABI 进行组装 我需要在函数本身中使用 2 个临时寄存器 第一个用于累积返回值 第二个用作计数器 我的功能目前如下所示 zero argument function some c
  • 为什么不能执行 mov [eax], [ebx] [重复]

    这个问题在这里已经有答案了 我可以做这个 mov eax ebx 和这个 mov eax ebx 甚至这个 mov eax ebx 但不是这个 错误C2415 mov eax ebx 只是wtf 为什么 它与 ptr1 ptr2 相同 为什
  • 如何在汇编语言中换行打印多个字符串

    我试图在汇编中的不同行上打印多个字符串 但使用我的代码 它只打印最后一个字符串 我对汇编语言非常陌生 所以请耐心等待 section text global start start mov edx len mov edx len1 mov
  • 将引导加载程序存储在软盘映像上的哪里?

    我将编写并测试引导加载程序 为了做到这一点 我计划将引导加载程序复制到软盘映像文件上并将其安装在虚拟机中 但是 我不确定将引导加载程序的机器代码放在哪里 它是否只是转储到文件的前几个字节中 软盘的引导扇区是第一个扇区 如果您谈论的是原始软盘
  • 奇怪的 MSC 8.0 错误:“ESP 的值未在函数调用中正确保存...”

    我们最近尝试将一些 Visual Studio 项目分解为库 并且在测试项目中一切似乎都编译和构建得很好 其中一个库项目作为依赖项 然而 尝试运行该应用程序给我们带来了以下令人讨厌的运行时错误消息 运行时检查失败 0 ESP 的值未在函数调
  • 近调用/跳转表并不总是在引导加载程序中工作

    一般问题 我一直在开发一个简单的引导加载程序 并在某些环境中偶然发现了一个问题 在这些环境中 此类指令不起作用 mov si call tbl SI Call table pointer call call tbl Call print c
  • Intel 64 和 IA-32 上的 MESI 有何意义

    MESI 的要点是保留共享内存系统的概念 然而 对于存储缓冲区 事情就变得复杂了 一旦数据到达 MESI 实现的缓存 下游内存就会保持一致 然而 在此之前 每个核心可能对内存位置 X 中的内容存在分歧 具体取决于每个核心的本地存储缓冲区中的
  • 如何知道寄存器是否是“通用寄存器”?

    我试图了解寄存器必须具备什么标准才能被称为 通用寄存器 我相信通用寄存器是一个可以用于任何用途的寄存器 用于计算 将数据移入 移出等 并且是一个没有特殊用途的寄存器 现在我读到了ESP寄存器是通用寄存器 我猜是ESP寄存器可以用于任何事情

随机推荐