逆向学习:解析40年前里程碑处理器 80386 预取队列电路

EETOP 2025-07-11 12:02

关注我们 设为星标

EETOP

百万芯片工程师专业技术论坛

官方微信号



1985 年,英特尔推出了具有开创性意义的 386 处理器,到今年正好40周年。

它是 x86 架构中的首款 32 位处理器。为提升性能,386 配备了一个 16 字节的指令预取队列。预取队列的作用是在指令需要执行前就从内存中获取它们,这样处理器在执行指令时通常无需等待内存读取。指令预取利用了处理器 思考(即执行运算等不占用内存总线操作)且内存总线闲置的时间窗口。

在本文中,我将详细剖析 386 的预取队列电路。其中一个有趣的电路是增量器,它给一个指针加 1,以按顺序遍历内存。这听起来好像很简单,但为了实现高性能,增量器采用了复杂的电路设计。预取队列使用一个大型电路网络来对字节进行移位操作,确保它们能正确对齐。它还设有一个简洁的电路,用于将有符号的 8 位和 16 位数字扩展为 32 位。本文虽不会有重大发现,但如果你对底层电路和动态逻辑感兴趣,不妨继续阅读。

下图展示了在显微镜下,386 那如指甲盖大小、闪闪发亮的硅芯片。尽管它看起来可能像一座分区奇特城市的航拍图,但芯片照片揭示了芯片的功能模块。左上角的预取单元(Prefetch Unit)就是我们关注的模块。

在本文中,我将讨论预取队列电路(用红色突出显示部分),暂不涉及右侧的预取控制电路。预取单元从与内存通信的总线接口单元(Bus Interface Unit,右上角)接收数据。指令解码单元(Instruction Decode Unit)则逐个字节地从预取单元接收预取指令,并对操作码进行解码,以便执行。

This die photo of the 386 shows the location of the registers. Click this image (or any other) for a larger version.

这张 386 的芯片裸片照片显示了寄存器的位置

芯片的左四分之一部分由一条条电路组成,这些电路看起来比芯片其他部分规整得多。这种类似网格的外观源于每个功能模块(在很大程度上)都是通过将相同电路重复 32 次构建而成的,每个位对应一次,并排排列。垂直数据线以 32 位为一组上下延伸,连接各个功能模块。为了实现这一点,每个电路在芯片上必须适配相同的宽度;这种布局限制迫使电路设计师开发出一种能高效利用该宽度且不超出允许宽度的电路。预取队列的电路采用了相同方法:每个电路宽 66 微米,重复 32 次。正如后文将看到的,要把预取电路塞进这个固定宽度,需要一些布局技巧。

预取器的工作内容

预取单元的目的是通过在指令需要执行前从内存中读取它们来加快性能,这样处理器就无需等待从内存获取指令。预取利用了内存总线闲置的时间,尽量减少与其他读写数据指令的冲突。在 386 中,预取的指令存储在一个 16 字节的队列中,该队列由四个 32 位块组成。

下图放大展示了预取器及其主要组件。你可以看到,相同的电路(在大多数情况下)是如何重复 32 次,形成垂直条带的。顶部是来自总线接口单元的 32 条总线线路。这些线路通过总线接口单元在数据通路和外部内存之间建立连接。当右侧的 32 条水平线分支并形成 32 条垂直线(每个位对应一条)时,这些线路形成一个三角形图案。接下来是取指指针(fetch pointer)和限制寄存器(limit register),以及一个用于检查取指指针是否已到达限制的电路。注意,增量器和限制检查电路的两个低阶位(右侧)的电路缺失了。在增量器的底部,你可以看到某些位位置的电路与其他位置相比,有一块缺失,打破了重复块的图案。16 字节的预取队列在增量器下方。尽管这个内存是预取器的核心,但它的电路占用的面积相对较小。

A close-up of the prefetcher with the main blocks labeled. At the right, the prefetcher receives control signals.

预取器的特写,标注了主要模块。在右侧,预取器接收控制信号

预取器的底部会根据需要对数据进行移位以对齐。一个 32 位的值可能会跨预取缓冲区的两个 32 位行拆分存储。为处理这种情况,预取器包含一个数据移位网络来对数据进行移位和对齐。这个网络占用大量空间,但这里没有有源电路:只有水平和垂直导线组成的网格。

最后,符号扩展电路(sign extend circuitry)会根据需要将有符号的 位或 16 位值转换为有符号的 16 位或 32 位值。可以看到,符号扩展电路非常不规则,尤其是在中间部分。一个锁存器存储预取队列的输出,供数据通路的其他部分使用。

限制检查

如果你编写过 x86 程序,可能知道处理器的指令指针(EIP),它存储着下一条要执行指令的地址。在程序执行时,指令指针会从一条指令指向下一条指令。然而,事实证明,指令指针实际上并不 存在!相反,386 有一个 提前指令取指指针Advance Instruction Fetch Pointer),它存储着下一条要预取到预取队列中的指令地址。但有时处理器需要知道指令指针的值,例如,在调用子例程时确定返回地址,或者计算相对跳转的目标地址。那该怎么办呢?处理器从预取队列电路中获取提前指令取指指针地址,然后减去预取队列当前的长度。结果就是下一条要执行指令的地址,即所需的指令指针值。

提前指令取指指针(即下一条要预取指令的地址)存储在预取队列电路顶部的一个寄存器中。随着指令被预取,这个指针会由预取电路递增。(由于每次取指 32 位,所以这个指针每次递增 4,且最低两位始终为 0。)

但是什么阻止预取器预取过度,超出有效内存范围呢?x86 架构因使用段来定义内存的有效区域而闻名。一个段有起始和结束地址(称为基地址和限制地址),通过阻止对段外的访问来保护内存。386 有六个活动段;相关的是存储程序指令的代码段(Code Segment)。因此,代码段的限制地址控制着预取器何时必须停止预取。预取队列包含一个电路,当取指指针到达代码段的限制时,该电路会停止预取。在本节中,我将描述这个电路。

比较两个值看似简单,但 386 使用了一些技巧来加快比较速度。基本思路是使用 30 个异或(XOR)门来比较两个寄存器的位。(为什么是 30 位而不是 32 位呢?因为每次取 32 位,地址的最低两位是 00,可以忽略。)如果两个寄存器匹配,所有异或结果都为 0;如果不匹配,至少有一个异或结果为 1。从概念上讲,将异或门的输出连接到一个 32 输入的或(OR)门,就能得到期望的结果:如果所有位都匹配,输出为 0;如果存在不匹配,输出为 1。但遗憾的是,出于电学原因,使用标准 CMOS 逻辑构建一个 32 输入的或门是不现实的,而且它的体积大到难以融入电路。相反,386 使用动态逻辑来实现一个分散的或非(NOR)门,预取器的每一列中有一个晶体管。

下图展示了一位相等比较的实现。其机制是,如果两个寄存器不同,右侧的晶体管会导通,将相等总线(equality bus)拉低。这个电路重复 30 次,比较所有位:如果有任何不匹配,相等总线会被拉低;如果所有位都匹配,总线保持高电平。左侧的三个门实现异或非(XNOR);这个电路可能看起来过于复杂,但它是实现异或非的标准方法。右侧的或非门会在时钟相位 之外阻止比较(其重要性将在下文解释)。

This circuit is repeated 30 times to compare the registers.

这个电路重复 30 次以比较寄存器

相等总线水平穿过预取器,如果有任何位不匹配,总线就会被拉低。但是什么让总线保持高电平呢?这由下面的动态电路负责。与常规静态门不同,动态逻辑由处理器的时钟信号控制,并依赖电路中的电容来保存数据。386 由两相时钟信号控制。在第一个时钟相位,下方的预充电晶体管导通,将相等总线拉高。在第二个时钟相位,上方的异或电路被启用,如果两个寄存器不匹配,就将相等总线拉低。同时,CMOS 开关在时钟相位 导通,将相等总线的值传递给锁存器。保持器keeper)电路会在相等总线未被显式拉低时保持其高电平,以避免相等总线上的电压缓慢消散的风险。保持器使用一个弱晶体管,在总线未被激活时保持其高电平。但如果总线被拉低,保持器晶体管会被 overpowered 并关闭。

这是等值比较的输出电路,该电路位于预取器右侧

这种动态逻辑降低了功耗和电路尺寸。由于总线在相反的时钟相位充电和放电,避免了通过晶体管的持续电流(相比之下,像 8086 这样的 NMOS 处理器可能在总线上使用上拉电阻。当总线被拉低时,电流会流过上拉和下拉晶体管,这会增加功耗,使芯片更热,并限制时钟速度) 。

增量器

每次预取后,提前指令取指指针必须递增,以保存下一条要预取指令的地址。递增这个指针是增量器的工作。(因为每次取指 32 位,所以指针每次递增 4。但在芯片照片中,你可以看到增量器和限制检查电路中有一个缺口,最低两位的电路被省略了。因此,增量器的电路将其值加 1,这样指针(附加两个零位)就以 为步长递增。)

构建一个增量器电路很直接,例如,可以使用由 30 个半加器组成的链。但问题在于,高速递增一个 30 位的值很困难,因为进位会从一个位位置传递到下一个位位置。这类似于用十进制计算 99999999 + 1;你需要繁琐地进位,进位,再进位,依此类推,遍历所有数字,导致这是一个缓慢的顺序过程。

增量器采用了一种更快的方法。首先,它几乎并行地高速计算所有进位。然后,根据进位并行计算每个输出位—— 如果有进位进入某个位位置,就翻转该位。

计算进位在概念上很直接:如果值的末尾有一个由 1 组成的块,所有这些位都会产生进位,但进位会被最右边的 位阻止。例如,递增二进制数 11011 会得到 11100;最后两位产生进位,但零位阻止了进位。早在 1959 年,英国曼彻斯特大学就开发出了实现这种功能的电路,被称为曼彻斯特进位链(Manchester carry chain)。

在曼彻斯特进位链中,为每个数据位构建一个开关链,如下所示。对于 1 位,闭合开关;对于 位,打开开关(开关由晶体管实现)。为了计算进位,从右侧输入一个进位信号。信号会通过闭合的开关,直到遇到一个打开的开关,然后被阻断。进位链上的输出为我们提供了每个位置所需的进位值。

曼彻斯特进位链的概念,位。

由于曼彻斯特进位链中的开关都可以并行设置,且进位信号能高速通过开关,所以这个电路能快速计算出我们需要的进位。然后,进位会并行翻转相关的位,比直接使用加法器快得多地得到结果。

当然,在实际实现中存在复杂情况。进位链中的进位信号是反相的,所以低信号通过进位链传播以表示进位(将信号拉低比拉高更快)。但在必要时,需要让线路拉高。与相等电路一样,解决方案是动态逻辑。也就是说,进位线在一个时钟相位预充电至高电平,然后在第二个时钟相位进行处理,可能会将线路拉低。

下一个问题是,进位信号在通过多个晶体管和长导线时会减弱。解决方案是每个段都有一个电路来放大信号,使用时钟控制的反相器和不对称反相器。重要的是,这个放大器不在进位链路径中,所以不会减慢链中信号的速度。

增量器中典型位的曼彻斯特进位链电路。

上图展示了增量器中典型位的曼彻斯特进位链电路实现。链本身在底部,晶体管开关如前所述。在时钟相位 1,预充电晶体管将进位链的这个段拉至高电平。在时钟相位 2,链上的信号通过右侧的 时钟控制反相器” 产生本地进位信号。如果有进位,下一个位会被异或门翻转,产生递增后的输出。保持器 放大器” 是一个不对称反相器,产生强低输出但弱高输出。当没有进位时,其弱输出使进位链保持高电平。但一旦检测到进位,它会强烈地将进位链拉低以增强进位信号。

但这个电路对于期望的性能来说仍然不够。增量器并行使用了第二种进位技术:进位跳跃(carry skip)。其概念是查看位块,并允许进位跳过整个块。下图展示了进位跳跃电路的简化实现。每个块由 到 位组成。如果一个块中的所有位都是 1,与门(AND gate)会导通进位跳跃线中的相关晶体管。这允许进位跳跃信号以块为单位(从左到右)传播。当它到达一个包含 位的块时,相应的晶体管会关闭,像在曼彻斯特进位链中那样阻止进位。与门都并行工作,所以晶体管会快速并行导通或关闭。然后,进位跳跃信号通过少量晶体管,无需经过任何逻辑(进位跳跃信号就像一列快车,跳过大多数站点,而曼彻斯特进位链是到所有站点的慢车)。与曼彻斯特进位链一样,进位跳跃的实现需要在线路上使用预充电电路、保持器 放大器和时钟控制逻辑,但我将跳过细节。

一个抽象且简化的进位跳跃电路。块大小与 386 的电路不匹配。

一个有趣的特点是大型与门的布局。一个 6 输入的与门是一个大型器件,难以适配增量器的一个单元。解决方案是将该门分散在多个单元中。具体来说,该门使用标准的 CMOS 与非门(NAND)电路,NMOS 晶体管串联,PMOS 晶体管并联。每个单元有一个 NMOS 晶体管和一个 PMOS 晶体管,链在末端连接以形成所需的与非门(对输出取反得到所需的与功能)。这种分散的布局技术不常见,但能使每个位的电路大致保持相同大小。

由于这些技术,增量器电路的逆向工程颇具挑战性。特别是,预取器的大部分由一个电路块重复 32 次组成,每个位对应一次。另一方面,增量器由四个不同的电路块组成,以不规则模式重复。具体来说,一个块启动进位链,第二个块继续进位链,第三个块结束进位链。结束块之前的块不同(一个大晶体管驱动最后一个块),总共有四个变体。这种不规则模式在之前的预取器照片中可见。

对齐网络

预取器的底部会根据需要旋转数据以对齐。与某些处理器不同,x86 不强制要求内存访问对齐。也就是说,一个 32 位的值无需在内存中从 字节边界开始。因此,一个 32 位的值可能会跨预取队列的两个 32 位行拆分存储。此外,当指令解码器从预取队列中获取指令的一个字节时,该字节可能在预取队列的任何位置。

为了处理这些问题,预取器包含一个对齐网络,它可以旋转字节,以输出符合处理器其他部分所需对齐方式的一个字节、字或四个字节。

下图展示了这个对齐网络的一部分。从预取队列(顶部)输出的每个位有四条导线,用于 24 位、16 位、位或 位旋转。每条旋转导线连接到 32 条水平位线之一。最后,每条水平位线有一个输出抽头,连接到下方的数据通路。(垂直线在芯片的下层 M1 金属层,而水平线在上层 M2 金属层。为了拍摄这张照片,我移除了 M2 层以显示下层。原始水平线的阴影仍然可见。 )

对齐网络的一部分。

其原理是,通过选择一组垂直旋转线,预取队列的 32 位输出将向左旋转相应的量。例如,要旋转 位,位会通过 旋转 8” 线向下传输。预取队列的位 会激活水平位线 8,位 会激活水平位线 9,依此类推,位 31 会回绕到水平位线 7。由于水平位线 连接到输出 8,结果就是位 作为位 输出,位 作为位 输出,依此类推。

对齐 32 位值的四种可能性。上方的四个字节按指定方式移位,以产生下方所需的输出。

对于对齐过程,预取队列中一个 32 位输出可能会以四种不同方式跨两个 32 位条目拆分存储,如上所示。这些组合由多路复用器(multiplexers)和驱动器(drivers)实现。两个 32 位多路复用器选择预取队列中相关的两行(上方的蓝色和绿色)。四个 32 位驱动器连接到四组垂直线,其中一组驱动器被激活以产生所需的移位。每个驱动器的每个字节都进行了布线,以实现上述对齐。例如,旋转 位的驱动器从 绿色” 多路复用器获取其最高字节,从 蓝色” 多路复用器获取其他三个字节。结果是,跨两个队列行拆分的四个字节被旋转,形成一个对齐的 32 位值。

符号扩展

最后一个电路是符号扩展电路。假设你想将一个 8 位数值与一个 32 位数值相加。一个无符号 8 位数值可以通过简单地将高位填充为零来扩展为 32 位。但对于有符号数值来说,情况就更复杂了。例如,-1 的 8 位表示是 0xFF,而其 32 位表示是 0xFFFFFFFF。要将一个 8 位有符号数值转换为 32 位,高 24 位必须用原始数值的最高位(即符号位)来填充。换句话说,对于正数,扩展位填充为 0;而对于负数,扩展位填充为 1。这个过程就称为符号扩展。9

在 386 处理器中,预取器底部的一个电路负责对指令中的数值进行符号扩展。该电路支持将 8 位数值扩展为 16 位或 32 位,也支持将 16 位数值扩展为 32 位。此电路会根据指令的要求,用零或符号位来扩展数值。

下图展示了这个符号扩展电路的一个位的结构。它由左侧和右侧的锁存器以及中间的多路复用器组成。锁存器是采用标准的 386 电路,通过 CMOS 开关构建而成(参见脚注)。7 多路复用器从三个值中选择一个:来自交换网络的位值、用于符号扩展的 0,或用于符号扩展的 1。如果选择位值,多路复用器由 CMOS 开关构成;若选择 0 或 1 值,则由两个晶体管构成。这个电路被复制了 32 次,但最低字节只包含锁存器,没有多路复用器,因为符号扩展不会修改最低字节。

预取器中与 31-8 位相关的符号扩展电路。

符号扩展电路的第二部分用于确定这些位应该填充 0 还是 1,并将控制信号发送给上面的电路。左侧的门电路确定符号扩展位应该是 0 还是 1。对于 16 位符号扩展,该位来自数据的第 15 位;而对于 8 位符号扩展,该位来自第 7 位。右侧的四个门电路生成用于每个位的符号扩展信号,分别为 31-16 位范围和 15-8 位范围产生独立的信号。

该电路确定哪些位应该填充 0 或 1。

这个电路在芯片上的布局有些不同寻常。预取器的大部分电路由 32 个相同的列组成,每列对应一个位。8 上面描述的电路只实现了一次,使用了大约 16 个门电路(未显示缓冲器和反相器)。尽管如此,该电路被挤在第 17 位到第 7 位的位置,导致布局出现了不规则性。此外,与 386 处理器的其他部分相比,该电路在硅片上的实现方式也很特别。386 的大部分电路使用两层金属进行互连,尽量减少多晶硅布线的使用。然而,上面提到的这个电路还使用了长段的多晶硅来连接各个门电路。

符号扩展电路的布局。该电路位于预取队列的底部。

指令在芯片中的传输路径

指令在 386 芯片中的传输路径曲折复杂。首先,右上角的总线接口单元从内存中读取指令,并通过 32 位总线(蓝色)将其发送到预取单元。预取单元将指令存储在 16 字节的预取队列中。

指令在预取队列中进进出出,路径曲折。

如何从预取队列中执行一条指令呢?事实证明,这有两条不同的路径。假设你要执行一条将 12345678 加到 EAX 寄存器的指令。预取队列中会存放五个字节:05(操作码)、78、56、34 和 12。预取队列通过 8 位总线(红色)将操作码逐个字节地提供给解码器。该总线从预取队列的对齐网络中取出最低 8 位,并将该字节发送到一个缓冲器(红色箭头头部的小方块)。操作码再从这里传输到指令解码器。10 指令解码器反过来使用大型查找表(PLA)将 x86 指令转换为包含 19 个不同字段的 111 位内部格式。

另一方面,指令的数据字节通过 32 位数据总线(橙色)从预取队列传输到算术逻辑单元(ALU)。与前面提到的总线不同,这条数据总线分布较广,每条线贯穿数据通路的每一列。因此,数据也可以存储到寄存器中。例如,MOV(移动)指令可以将指令中的一个值(即 “立即数”)存储到寄存器中。

结论

386 处理器的预取队列包含约 7400 个晶体管,比整个英特尔 8080 处理器的晶体管数量还多(而且这还只是队列本身,不包括预取控制逻辑)。这体现了处理器技术的飞速发展:386 中一个功能单元的一部分所包含的晶体管数量,就超过了 11 年前的整个 8080 处理器。而这个单元还不到整个 386 处理器的 3%。

每次观察 x86 电路时,我都能看到为支持向后兼容所付出的复杂性,也更能理解为什么精简指令集计算机(RISC)会流行起来。预取器也不例外。其复杂性在很大程度上源于 386 对未对齐内存访问的支持,这需要一个字节移位网络来将字节调整到 32 位对齐状态。此外,在指令总线的另一端是复杂的指令解码器,用于解码复杂的 x86 指令。而解码 RISC 指令则要简单得多。

无论如何,希望你能对预取电路的介绍感兴趣。我计划撰写更多关于 386 的内容,所以请在 Bluesky(@righto.com)上关注我,或通过 RSS 获取更新。我之前已经写过多篇关于 386 的文章,一个不错的起点可能是我对 386 芯片的综述。

原文:https://www.righto.com/

欢迎加入EETOP RISC-V 等微信群
邀请报名
图片


声明:内容取材于网络,仅代表作者观点,如有内容违规问题,请联系处理。 
处理器
Copyright © 2025 成都科技区角科技有限公司
蜀ICP备2025143415号-1
  
川公网安备51015602001305号