逆向工程386处理器的预取队列电路

Ken Shirriff 论文

摘要

详细介绍386处理器预取队列电路的逆向工程,解释所用的增量器、对齐网络和动态逻辑。

<p>1985年,英特尔推出了具有突破性的386处理器,这是x86架构中的首款32位处理器。 为了提升性能,386配备了一个16字节的指令预取队列。 预取队列的作用是在指令实际需要之前从内存中读取它们, 这样处理器在执行指令时通常无需等待内存。 指令预取利用了处理器“思考”且内存总线可能空闲的时间段。</p> <p>在本文中,我将详细研究386的预取队列电路。 其中一个有趣的电路是增量器,它给指针加1以逐步遍历内存。 这听起来很简单,但增量器使用了复杂的电路以实现高性能。 预取队列使用一个大型网络 来移动字节以使它们正确对齐。 它还有一个紧凑的电路,用于将带符号的8位和16位数字 扩展到32位。 这篇博文没有重大发现,但如果你对底层电路和动态逻辑感兴趣,请继续阅读。</p> <p>下面的照片展示了386在显微镜下闪闪发光的指甲大小硅芯片。 尽管它看起来像一座分区奇特的城市的航拍图,但这张芯片照片揭示了芯片的功能模块。 左上角的预取单元是相关模块。 在这篇文章中,我将讨论 预取队列电路(以红色高亮),跳过右侧的预取控制电路。 预取单元从与内存通信的总线接口单元(右上角)接收数据。 指令解码单元从预取单元逐字节接收预取的指令,并解码 操作码以供执行。</p> <p><a href="https://static.righto.com/images/386-prefetch/386-die-labeled.jpg"><img alt="这张386芯片照片显示了寄存器的位置。点击此图片(或任何其他图片)可查看大图。" class="hilite" height="534" src="https://static.righto.com/images/386-prefetch/386-die-labeled-w500.jpg" title="这张386芯片照片显示了寄存器的位置。点击此图片(或任何其他图片)可查看大图。" width="500" /></a><div class="cite">这张386芯片照片显示了寄存器的位置。点击此图片(或任何其他图片)可查看大图。</div></p> <p>芯片左侧四分之一由一系列看起来比芯片其余部分整齐得多的电路条带组成。 这种网格状外观的原因在于 每个功能块(大部分情况下)通过重复相同的电路32次(每个位一次)并排构建而成。 垂直的数据线以32位为单位上下延伸,连接各个功能块。 为了使这成为可能,每个电路必须适应芯片上相同的宽度;这种布局约束迫使电路 设计人员开发出一种有效利用该宽度而不超出允许宽度的电路。 预取队列的电路采用了相同的方法:每个电路宽66微米<span id="fnref:width"><a class="ref" href="#fn:width">1</a></span>,并重复32次。 正如我们将看到的,将预取电路适应这个固定宽度需要一些布局技巧。</p> <h2>预取器的作用</h2> <p>预取单元的目的是通过在实际需要之前从内存读取指令来提高性能, 这样处理器就不需要等待从内存获取指令。 预取利用了内存总线空闲的时间,最大限度地减少与正在读写数据的其他指令 的冲突。 在386中,预取的指令存储在一个16字节的队列中,由四个32位块组成。<span id="fnref:cache"><a class="ref" href="#fn:cache">2</a></span></p> <p>下面的示意图放大了预取器并显示了其主要组件。 你可以看到相同的电路(大多数情况下)重复了32次,形成了垂直条带。 顶部是来自总线接口单元的32条总线。这些线路提供了数据通路与 外部内存之间的连接,通过总线接口单元。 这些线路形成一个三角形图案,右侧的32条水平线分支并形成32条垂直线,每条对应一位。 接下来是取指指针和界限寄存器,以及一个用于检查取指指针是否 达到界限的电路。 请注意,增量器和界限检查电路的低两位(右侧)是 缺失的。 在增量器底部,你可以看到某些位位置有一个其他位缺失的电路块, 打破了重复块的模式。 16字节的预取队列位于增量器下方。虽然这片内存是预取器的核心,但其 电路仅占相对较小的面积。</p> <p><a href="https://static.righto.com/images/386-prefetch/prefetcher-labeled.jpg"><img alt="预取器的特写,主要模块已标注。在右侧,预取器接收控制信号。" class="hilite" height="387" src="https://static.righto.com/images/386-prefetch/prefetcher-labeled-w600.jpg" title="预取器的特写,主要模块已标注。在右侧,预取器接收控制信号。" width="600" /></a><div class="cite">预取器的特写,主要模块已标注。在右侧,预取器接收控制信号。</div></p> <p>预取器的底部部分移动数据以根据需要进行对齐。 一个32位的值可能被分割在预取缓冲区的两行32位 行中。 为了处理这种情况,预取器包含一个数据移位网络来移动和对齐其数据。 这个网络占用了大量空间,但这里没有有源电路:只是水平和垂直导线的网格。</p> <p>最后,符号扩展电路根据需要将带符号的8位或16位值转换为带符号的16位或32位值。 你可以看到符号扩展电路非常不规则,尤其是在中间部分。 一个锁存器存储预取队列的输出,以供数据通路其余部分使用。</p> <h2>界限检查</h2> <p>如果你编写过x86程序,你可能知道处理器的指令指针(EIP),它保存了 下一条要执行的指令的地址。 随着程序的执行,指令指针在指令之间移动。 然而,事实证明指令指针实际上并不存在! 相反,386有一个“高级指令取指指针”,它保存了下一个要取到预取队列中的指令的 地址。 但有时处理器需要知道指令指针的值,例如,在调用子程序时确定返回 地址,或计算相对跳转的目标地址。 那么会发生什么呢? 处理器从预取队列电路获取高级指令取指指针地址,并减去 预取队列的当前长度。 结果是下一条要执行的指令的地址,即所需的指令指针值。</p> <p>高级指令取指指针——下一条要预取的指令的地址——存储在 预取队列电路顶部的 寄存器中。 当指令被预取时,预取电路会递增这个指针。(由于指令每次以32位获取, 该指针以4为步长递增,低两位始终为0。)</p> <p>但是,是什么阻止预取器预取过远并超出有效内存范围呢? x86架构臭名昭著地使用段来定义有效内存区域。 一个段有起始和结束地址(称为基址和界限),通过阻止访问段外内存来保护内存。 386有六个活动段;相关的是保存程序指令的代码段。 因此,代码段的界限地址控制着预取器必须何时停止预取。<span id="fnref:paging"><a class="ref" href="#fn:paging">3</a></span> 预取
查看原文
查看缓存全文

缓存时间: 2026/05/16 03:34

# 逆向工程 386 处理器的预取队列电路 来源:http://www.righto.com/2025/05/386-prefetch-circuitry-reverse-engineered.html 1985 年,英特尔推出了革命性的 386 处理器,这是 x86 架构中第一款 32 位处理器。为了提升性能,386 配备了一个 16 字节的指令预取队列。预取队列的作用是在指令需要之前就从内存中取出它们,这样一来,处理器在执行指令时通常无需等待内存。指令预取利用了处理器“思考”且内存总线闲置的时间。 在本文中,我将详细介绍 386 的预取队列电路。其中一个有趣的电路是增量器,它给指针加 1 以逐步遍历内存。这听起来很简单,但增量器为了实现高性能使用了复杂的电路。预取队列使用了一个大型网络来移位字节,使其正确对齐。此外,它还有一个紧凑的电路,用于将带符号的 8 位和 16 位数扩展到 32 位。这篇文章中没有什么重大发现,但如果你对底层电路和动态逻辑感兴趣,请继续往下看。 下面的照片是在显微镜下看到的 386 闪闪发光的指甲盖大小的硅晶片。虽然它看起来像一张分区奇特的城市航拍图,但晶片照片显示出了芯片的功能模块。左上角的预取单元是相关模块。在本文中,我将讨论预取队列电路(以红色高亮显示),而跳过右侧的预取控制电路。预取单元从与内存通信的总线接口单元(右上角)接收数据。指令解码单元从预取单元逐一接收预取的指令字节,并解码操作码以供执行。 这张 386 的晶片照片显示了寄存器的位置。点击此图像(或其他图像)可查看大图。(https://static.righto.com/images/386-prefetch/386-die-labeled.jpg) 这张 386 的晶片照片显示了寄存器的位置。点击此图像(或其他图像)可查看大图。 芯片的左四分之一由条带状电路组成,看起来比芯片的其他部分有序得多。之所以出现这种网格状外观,是因为每个功能块(大部分情况下)都是将同一个电路重复 32 次,每比特一次,并排放置而成。垂直数据线以 32 位一组上下延伸,连接各个功能块。为了使这正常工作,每个电路必须适应晶片上相同的宽度;这种布局约束迫使电路设计者开发出一种能有效利用该宽度且不超过允许宽度的电路。预取队列的电路也采用了同样的方法:每个电路宽 66 微米¹,并重复 32 次。正如我们将看到的,将预取电路装入这个固定宽度需要一些布局技巧。 ## 预取器的作用 预取单元的目的是通过提前从内存读取指令来提升性能,这样处理器就不必等待从内存获取指令。预取利用了内存总线空闲的时间,尽量减少与正在读取或写入数据的其他指令的冲突。在 386 中,预取的指令存储在一个 16 字节的队列中,由四个 32 位块组成。² 下图放大了预取器并显示了其主要组件。你可以看到,大多数情况下,相同的电路重复了 32 次,形成了垂直的带状区域。顶部是来自总线接口单元的 32 条总线线路。这些线路通过总线接口单元提供数据路径与外部内存之间的连接。这些线路形成了一个三角形图案,右侧的 32 条水平线分叉并形成 32 条垂直线,每比特一条。接下来是取指指针和限制寄存器,以及一个检查取指指针是否达到限制的电路。注意,增量器和限制检查电路的两个低位(右侧)缺失。在增量器的底部,你可以看到某些位位置与其他位置相比缺少一块电路,打破了重复块的模式。16 字节的预取队列位于增量器下方。虽然这个内存是预取器的核心,但其电路占用的面积相对较小。 预取器的特写视图,标有主要模块。右侧,预取器接收控制信号。(https://static.righto.com/images/386-prefetch/prefetcher-labeled.jpg) 预取器的特写视图,标有主要模块。右侧,预取器接收控制信号。 预取器的底部部分移位数据以按需对齐。一个 32 位值可能跨过预取缓冲区的两行 32 位列。为了处理这种情况,预取器包含一个数据移位网络,用于移位和对齐数据。这个网络占据了很大空间,但这里没有有源电路:只是一个水平和垂直导线的网格。 最后,符号扩展电路根据需要将带符号的 8 位或 16 位值扩展为带符号的 16 位或 32 位值。你可以看到符号扩展电路非常不规则,尤其是在中间部分。一个锁存器存储预取队列的输出,供数据路径的其余部分使用。 ## 限制检查 如果你编写过 x86 程序,你可能知道处理器的指令指针(EIP),它保存着下一条要执行指令的地址。随着程序的执行,指令指针逐条指令移动。然而,事实证明,指令指针实际上并不存在!相反,386 有一个“高级取指指针”,它保存着下一条要取入预取队列的指令的地址。但有时处理器需要知道指令指针的值,例如,确定调用子程序时的返回地址,或计算相对跳转的目标地址。那么会发生什么呢?处理器从预取队列电路获取高级取指指针地址,并减去预取队列的当前长度。结果就是下一条要执行指令的地址,即所需的指令指针值。 高级取指指针——下一条要预取指令的地址——存储在预取队列电路顶部的寄存器中。随着指令被预取,这个指针由预取电路递增。(由于指令一次取 32 位,该指针以 4 为步进递增,最低两位始终为 0。) 但是,是什么阻止预取器预取过远而超出有效内存范围呢?x86 架构臭名昭著地使用段来定义内存的有效区域。每个段都有一个起始地址和结束地址(称为基址和限制),通过阻止段外的访问来保护内存。386 有六个活动段;相关的是存放程序指令的代码段。因此,代码段的限制地址控制着预取器何时必须停止预取。³预取队列包含一个电路,当取指指针达到代码段的限制时停止预取。在本节中,我将描述该电路。 比较两个值看似简单,但 386 使用了一些技巧来使其快速完成。基本思想是使用 30 个异或门来比较两个寄存器的相应位。(为什么是 30 位而不是 32 位?由于一次取 32 位,地址的最低有效位为 00,可以忽略。)如果两个寄存器匹配,所有异或值将为 0;如果不匹配,异或值将为 1。从概念上讲,将这些异或门连接到一个 32 输入或门将得到所需结果:如果所有位匹配则为 0,如果有不匹配则为 1。不幸的是,使用标准 CMOS 逻辑构建 32 输入或门出于电气原因是不切实际的,而且尺寸太大,无法装入电路。相反,386 使用动态逻辑实现了一个分散的或非门,在预取器的每一列中有一个晶体管。 下面的原理图显示了相等比较的一位实现。机制是:如果两个寄存器不同,右侧的晶体管会导通,将相等总线拉低。该电路复制了 30 次,比较所有位:如果存在任何不匹配,相等总线将被拉低;但如果所有位都匹配,总线保持高电平。左侧的三个门实现了同或(XNOR);这个电路可能看起来过于复杂,但它是实现同或的标准方法。右侧的或非门在时钟相位 2 之外阻止比较。(其重要性将在下面解释。) 该电路重复 30 次以比较寄存器。(https://static.righto.com/images/386-prefetch/equality-logic.jpg) 该电路重复 30 次以比较寄存器。 相等总线水平穿过预取器,如果有任何位不匹配,它会被拉低。但什么将总线拉高呢?这就是下面动态电路的工作。与常规静态门不同,动态逻辑由处理器的时钟信号控制,并依赖电路中的电容来保持数据。386 由一个两相时钟信号控制。⁴在第一个时钟相位,下面的预充电晶体管导通,将相等总线拉高。在第二个时钟相位,上面的异或电路被使能,如果两个寄存器不匹配,则将相等总线拉低。同时,CMOS 开关在时钟相位 2 导通,将相等总线的值传递给锁存器。“保持器”电路保持相等总线为高电平,除非它被明确拉低,以避免相等总线上的电压缓慢消散的风险。保持器使用一个弱晶体管在非活动状态下保持总线为高。但如果总线被拉低,保持晶体管会被压倒并关断。 这是相等比较的输出电路。该电路位于预取器的右侧。(https://static.righto.com/images/386-prefetch/equality-out.jpg) 这是相等比较的输出电路。该电路位于预取器的右侧。 这种动态逻辑降低了功耗和电路尺寸。由于总线在相反的时钟相位充电和放电,可以避免晶体管中的持续电流。(相比之下,像 8086 这样的 NMOS 处理器可能会在总线上使用上拉电阻。当总线被拉低时,会有电流流过上拉和下拉晶体管。这会增加功耗,使芯片升温,并限制时钟速度。) ## 增量器 每次预取后,高级取指指针必须递增,以保存下一条要预取指令的地址。递增这个指针是增量器的工作。(由于每次取指是 32 位,指针每次增加 4。但在晶片照片中,你可以看到增量器和限制检查电路中有一个凹口,那里省略了最低两位的电路。因此,增量器的电路将其值加 1,因此指针(附加两个零位)以 4 为步进增加。) 构建增量器电路很简单,例如,你可以使用一串 30 个半加器。问题在于高速递增一个 30 位值很难,因为进位会从一个位置传播到下一个位置。这类似于计算 99999999 + 1;你需要繁琐地一遍又一遍地进位 1,穿过所有数字,导致一个缓慢的串行过程。 增量器使用了一种更快的方法。首先,它高速计算所有进位,几乎是并行完成。然后,它根据进位并行计算每个输出位——如果某个位置有进位,则该位翻转。 计算进位在概念上很简单:如果数值末尾有一串 1 位,所有这些位都会产生进位,但进位会被最右边的 0 位停止。例如,递增二进制 11011 得到 11100;最后两个位有进位,但零位停止了进位。实现这种电路的电路早在 1959 年就在英国曼彻斯特大学开发出来,被称为曼彻斯特进位链。 在曼彻斯特进位链中,你构建一个开关链,每个数据位一个,如下所示。对于 1 位,你闭合开关;对于 0 位,你断开开关。(开关由晶体管实现。)为了计算进位,你从右侧输入一个进位信号。该信号会穿过闭合的开关,直到遇到一个断开的开关,然后被阻止。⁵沿链的输出给出了每个位置所需的进位值。 曼彻斯特进位链的概念,4 位。(https://static.righto.com/images/386-prefetch/chain.jpg) 曼彻斯特进位链的概念,4 位。 由于曼彻斯特进位链中的开关可以全部并行设置,并且进位信号高速穿过开关,该电路快速计算我们需要的进位。然后进位并行翻转相关位,从而比简单加法器更快地得到结果。 当然,在实际实现中存在复杂性。进位链中的进位信号是反相的,因此低信号在进位链中传播表示进位。(将信号拉低比拉高快。)但是,需要某种东西在必要时将线路拉高。与相等电路一样,解决方案是动态逻辑。也就是说,进位线在一个时钟相位期间被预充电为高,然后在第二个时钟相位进行处理,可能将线路拉低。 下一个问题是,进位信号在穿过多个晶体管和长导线时会衰减。解决方案是每个段都有一个放大信号的电路,使用时钟反相器和非对称反相器。重要的是,这个放大器不在进位链路径中,因此不会减慢信号通过链的速度。 增量器中典型位的曼彻斯特进位链电路。(https://static.righto.com/images/386-prefetch/chain-circuit.jpg) 增量器中典型位的曼彻斯特进位链电路。 上面的原理图显示了典型位的曼彻斯特进位链实现。链本身在底部,如前所述带有晶体管开关。在时钟相位 1 期间,预充电晶体管将进位链的这一段拉高。在时钟相位 2 期间,链上的信号通过右侧的“时钟反相器”产生本地进位信号。如果有进位,下一位会被异或门翻转,产生递增后的输出。⁶“保持器/放大器”是一个非对称反相器,产生强低输出但弱高输出。当没有进位时,其弱输出保持进位链为高电平。但一旦进位到来,

相似文章

Intel 8087浮点芯片的指令解码

Ken Shirriff

对Intel 8087浮点协处理器指令解码的详细逆向工程分析,解释主CPU与协处理器之间的交互、微码ROM的使用以及总线接口单元。