【问题标题】:How is a 15 bytes instruction transferred form memory to CPU?一条 15 字节的指令如何从内存传输到 CPU?
【发布时间】:2019-07-21 20:43:21
【问题描述】:

假设我们使用的是 x86-64 机器,这意味着它的通用寄存器是 64 位长,它的数据总线一次可以处理 64 位,它的 ALU 最多可以处理 64 位(对吗?)。

有一个简单的指令,比如

MOV $5, %eax

通过 64 位数据总线将 32 位数字移动到 CPU 寄存器中。

我已阅读以下内容:

An x86-64 instruction may be at most 15 bytes in length.

我的问题是,如果数据总线最大为 64 位,这怎么可能?它如何处理 120 位的指令。 CPU 是否在多个周期中获取它?

我的第二个问题是,是否有更长的特殊寄存器来存储所有 120 位?

【问题讨论】:

  • 常规指令一次最多只能处理 64 位,但较新处理器的 SIMD 扩展可以在一条指令中处理 256 甚至 512 位。
  • 数据总线和指令长度不相关。 8088 有一个 8 位数据总线以节省成本,但它的指令可以更长。但至少从 Pentium 4 时代开始,x86 CPU 就已经拥有 64 位数据总线,而多通道内存的数量是这个数字的几倍

标签: assembly x86-64 cpu-architecture cpu-registers instruction-set


【解决方案1】:

指令编码

现代 X86 指令由以下内容构建:

  • 前缀(0、1、2、3、4)
  • VEX (0, 2, 3)
  • 操作码 (1)
  • ModR/M (1)
  • SIB (0,1)
  • DISP (0, 1, 2, 4)
  • IMM (0, 1, 2, 4)

前缀是零到四个字节:

第 1 组:LOCK 或 REP
第 2 组:段(CS、SS、DS、ES、FS、GS — 并非全部都以 64 位提供)和分支提示(即是否更可能采用分支?)
第 3 组:操作数大小(66H,对于某些指令是强制性的!)
第 4 组:地址大小

VEX

VEX 用于 AVX 扩展(大部分)

操作码

OPCODE 是实际指令,只有 8 位,如果你不计算 VEX 和一些其他前缀/特殊字节,例如著名的0F。 (在过去,这是访问 x86 协处理器的方式。)

ModR/M 定义模式

它告诉我们在这个指令中使用了哪个寄存器和/或内存模式。某些指令不支持所有可用模式。

尺度、指数、基数

SIB 是 ModR/M 的扩展。

位移

DISP 是位移,添加到地址寄存器的立即数(如 [ESP+13])它也可以是内存位置的直接地址。

立即

IMM 一个立即数(MOV EBX, $8 中的 8 是加载在 EBX 中的值,即立即数。)

请注意,IMM 通常限制为 32 位。 REX 可用于获取 64 位,但并非对所有指令都可用(因为任何一条指令的总字节数为 15 字节)。要在寄存器中加载 64 位,您总是从内存中加载它。这样做的一种方法是使用基于 IP 的地址。 (类似这样的东西:MOV R8, [RIP, -42])另外我注意到过去的编译器,如 gcc 并没有使用该指令。但是,对于 64 位处理器,可以使用 32 位位移,因此该值几乎可以在任何地方 (±2Gb)。

加载指令

64 位处理器将指令加载到指令缓存中。它一次加载 16 个字节(可能因处理器而异)。然后处理器解释这些字节。根据处理器的不同,它可能会将这些字节转换为一组 RISC 指令或直接执行指令

例如,LOOP label 指令实际上几乎相当于至少两条指令:

SUB ECX, 1
JNZ label

过去有些处理器很难处理这样的问题,因此 LOOP 非常慢。一个原因是SUB 改变了许多EFLAGS,而LOOP 没有改变。

解释器不会在寄存器中加载指令。它将它加载到 CPU 中并在相应的单元(ALU、ACU、FPU 等)中处理它。不过,还有指向当前指令的 RIP 寄存器。就您而言,RIP 始终指向当前指令的开头或下一条指令的开头。

它是如何实现的,我不知道。他们可能会很快(瞬间)确定关注哪个单元并将指令推送到那里。确定大小并不复杂,因此他们可以快速获取所有字节并将它们推送到相关单元 FIFO,可能是 15 或 16 字节的值(即 FIFO 中的一项肯定总是 16 字节,一个字节可能被忽略,这使得硬件甚至没有行来读取它!)这些字节每次都将定位在相同的位置。因此,如果输入没有LOCKREP,它将在该FIFO 字节中输入00h

请注意,在单元之间移动 FIFO 中的 16 个字节是没有意义的。多年来,GPU 一直在其 FIFO 中移动大量数据。

您可以说这些 FIFO 是附加寄存器。寄存器文件与 FIFO 是一样的,只是它具有随机访问而不是“PUSH/POP”类型的机制。两者都使用类似的技术,即内存,将数据保存在 FIFO 和寄存器中。

文档

我建议第一个文件,目前标题为:

Intel® 64 and IA-32 architectures software developer’s manual combined volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4

来自英特尔的关于可用说明的好读物(并非绝对所有内容,但足以开始使用!)

【讨论】:

  • 没有任何 x86-64 CPU 不能解码为微指令。最后一个不这样做的 x86 CPU 是 P5 Pentium,双问题超标量流水线可以配对简单的 x86 指令,但不能将复杂指令拆分为更好的配对。 Transmeta 内部软件 JITing 到 VLIW ISA 也值得注意,但这意味着 x86“代码获取”是纯粹的数据处理。
  • x86-64 使用 RIP,而不是 EIP。而且确定大小并不复杂,因此他们可以快速获取所有字节并将它们推送到相关的单元FIFO中,甚至没有关闭。 x86 指令长度解码是hard,对于想要在每个时钟解码 4 或 5 条指令的高性能 x86 CPU 来说是一个主要问题。在 Intel CPU 中有一个完全独立的流水线阶段,在预解码和解码之间有一个缓冲区。 add cx, 1234 将使用 Length-Changing Prefix 停止该阶段。但无论如何,解码发生在一个地方,在微指令被路由到执行单元之前。
  • 因为 DISP 比较小,所以值需要非常接近 你在说什么? RIP 相对寻址使用rel32。几乎所有代码都是使用代码模型构建的,该代码模型将所有静态代码和数据彼此相隔 2GiB,因此所有静态代码始终可以通过 RIP 相对寻址来寻址。 int global_foo; int f(){return global_foo;} 将使用相对于 RIP 的 mov 加载,当由包括 GCC 在内的任何健全的编译器编译时。但是 gcc 会在需要宽常量的任何时候使用mov r64, imm64,这不是很好,但通常比静态数据要好。
  • 不,REX 可以与所有指令一起使用以指示 64 位 操作数大小。例如add r/m64, imm32 需要一个 REX 前缀。 REX 仅将 mov 的立即数大小从 5 字节 mov r32, imm32(没有 ModRM)更改为 10 字节 mov r64, imm64(REX + 相同的操作码 + 8 字节)。您的更新根本没有帮助,并且以不同的方式出错。 \@prl 指出了 3 个不同的遗漏,而不是 2 个。
  • 不过,对于 64 位处理器,32 位位移可用 意义不大。 RIP 相对寻址是 x86-64 的新功能。如果你说的是 32 位编译器,他们没有使用它,因为它不可用。
【解决方案2】:

指令提取是与代码提取分开的数据路径。 使用 64 位mov 指令没有完成。有专门的逻辑来处理获取和解码可变长度未对齐的 x86 指令。

一条指令可以跨越 4k 页边界,因此它的字节来自 2 个不连续的物理页!前端必须能够获取指令字节并将它们组装到缓冲区中。

即使 8086 也有一个小的指令预取缓冲区,尽管解码不一定需要它,因为在 8088 上它比最长的指令小(不包括前缀)。


请参阅 David Kanter's Sandybridge writeup 了解 Sandybridge 的前端图表(以及 Nehalem 和 Bulldozer)。还有Agner Fog's microarch guide。有关最新 AMD 前端的更多信息,请参阅 https://en.wikichip.org/wiki/amd/microarchitectures/zen#Decode

在 P6 和 SnB 系列 Intel CPU 上,代码提取和预解码(以查找 insn 边界)发生在 16 字节块中,每个周期查找最多 6 条指令的长度,每个周期消耗最多 16 个字节的 x86 机器代码.如果指令运行超过块的末尾,则预解码器会保留这些字节,直到下一个周期。 Agner Fog 的 microarch pdf 有一些关于优化以避免预解码瓶颈的细节; x86 解码是困难。例如在某些情况下,操作数大小的前缀会改变指令的 rest 的长度。例如66 前缀是 add eax, imm32(5 个字节)和 add ax, imm1666 + 3 个字节)之间的唯一区别。在这种情况下,Intel CPU 中的预解码器会停止,需要额外的周期来处理它。 (Alexis 的回答声称长度查找很容易。对于多年来积累的所有 ISA 扩展,这肯定是不容易,其中 VEX 前缀是另一条指令的无效编码,例如. 当您尝试并行执行多条指令时,它变得更加困难,因为您必须在第一条指令之后考虑所有指令的多个起点。较旧的 CPU 过去解码前缀很慢,例如需要额外的周期每个前缀甚至转义字节。但现代主流英特尔(非低功耗)可以处理任意数量的前缀而不会受到惩罚。)

一次最多向解码器提供 4 条指令(或 5 或 6 条宏融合)。或者 Skylake 有 5 个解码器,如果有 2 对 dec/jcc 或其他宏可熔对,则最多可处理 7 条指令。根据 uarch,这可以产生多达 7 个微操作 (uops)(Core2/Nehalem 上的 4-1-1-1 模式)、4 个(Skylake 之前的 SnB 系列)或 5 个(Skylake)。

并行解码 x86 指令是一个瓶颈,现代 CPU(自 SnB 系列以来的英特尔,自 Zen 以来的 AMD)缓存解码 uops 以缩短代码热部分的速度。 Pentium 4 的跟踪缓存是该方向的早期实验,但效果不佳(并且它没有解码器吞吐量来维持可接受的跟踪缓存未命中性能)。

另请参阅What's the relationship between early 90s Pentium microprocessor and today's Intel designs? on retrocomputing,我的回答谈到了为什么 P4 是 CPU 架构的死胡同,以及 P6 系列 (PPro / PIII) 如何演变成英特尔当前的 Sandybridge 系列。


所有 x86-64 CPU 都足够新,可以通过宽内部数据路径实现高性能,但 16 位和 32 位 CPU 具有相同的 15 字节最大长度(包括冗余前缀)。如果他们在查看操作码、modrm + 额外寻址模式字节和/或立即数之前分别解码这些指令,他们可能会使用至少足够大的缓冲区来保存不包括前缀的指令。

除了原始的 8086 之外,其中一个 64k 的代码段充满了一条指令的 REP 前缀有效的。那时英特尔还没有对指令长度定义任何限制,并且 8086 解码前缀与指令的其余部分分开。

【讨论】:

    猜你喜欢
    • 2018-09-30
    • 2018-10-23
    • 2015-07-02
    • 2017-12-10
    • 2012-03-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多