Skip to content

Optimizing Assembly

nifei edited this page Oct 30, 2013 · 8 revisions

原文在这里: http://www.agner.org/optimize/

1 介绍

这部手册是如下系列的第二部:

  1. 优化c++软件: Windows, Linux 和 Mac 平台上的优化指南.

  2. 优化汇编语言的子程序: X86 平台的优化指南.

  3. Intel, AMD 和 VIA CPU 的微结构(microarchicture): 汇编程序员和编译器作者的指南.

  4. 指令表: Intel, AMD 和 VIA CPU 的指令背后的潜在操作, 数据吞吐量和指令操作分解.

  5. 不同 C++ 编译器和操作系统下的调用惯例.

这些手册的最新版本在这里: http://www.agner.org/optimize/. 版权说明在164页.

这本手册解释了怎样把汇编代码和高级编程语言结合起来以及怎样使用汇编代码来优化 CPU 密集型代码的速度.

这本手册面向高级汇编程序员和编译器作者. 读者最好已经了解汇编语言并且有一些汇编编程的经验. 建议初学者使用指南里介绍的优化技巧之前先搜索相关知识并获取一些编程经验. 我可以推荐一大堆互联网上的介绍, 手册, 论坛和讨论组 (在 http://www.agner.org/optimize/ 有列) 以及 R. C. Detmer 在2006年完成的这本书 "Introduction to 80x86 Assembly Language and Computer Archicture", 第二版.

这本手册囊括了使用 x86 和 x86-64 指令集的所有平台. 大多数 Intel, AMD 和 VIA 的微处理器都使用这套指令集. 可以使用这套指令集的操作系统包括 DOS, Windows, Linux, FreeBSD/OpenBSD 和 Intel-Based Mac OS. 手册包含了最新的微处理器和指令集. 手册3和4介绍了个别微处理器模型的细节.

比汇编语言更普适的优化技巧在手册1: "C++软件优化" 中有介绍. 个别微处理器的细节在手册3: "Intel, AMD 和 VIA CPU 的微结构" 有介绍. 指令表耗时(tables of instruction timings)等问题在手册4: "指令表: Intel, AMD 和 VIA CPU 的指令背后的潜在操作, 数据吞吐量和指令操作分解" 有介绍. 不同操作系统和编译器下的调用惯例在手册5: "不同 C++ 编译器和操作系统下的调用惯例" 中有讲.

汇编语言变成比高级语言编程难多了. 犯错误很容易, 调试很难. 警告! 不要把你的程序问题发给我. 我不回复这类邮件. 如果在相关书籍和手册中找不到你要的答案的话, 互联网上多的是能解答你编程问题的论坛.

纳秒征途好运!

1.1 使用汇编的理由

汇编语言如今不像过去用的那么多了. 但是还是有理由学习和使用它的. 主要是:

  1. 为了学习. 知道微处理器和编译器在指令层如何工作是很重要的, 这样我们就可以推测什么样的编程技巧最高效, 理解各种结构在高级语言中怎么工作的, 还可以追踪奇怪的错误.

  2. 为了调试和定位(verifying). 在查找错误和查看编译器能把一段特定代码优化成什么样的时候, 查看编译器生成的汇编代码或者调试器中的反汇编窗口很有帮助.

  3. 写编译器. 在开发编译器, 调试器和其他开发工具的时候, 理解汇编编程技巧是必需的.

  4. 嵌入式系统. 小型嵌入式系统的资源和主机都比PC少. 为追求速度或体积在小型嵌入式系统上有必要使用汇编编程.

  5. 硬件驱动和系统编码. 使用高级编程语言访问硬件, 系统控制寄存器等有时很困难或者做不到.

  6. 获取使用高级语言无法获取的指令. 有些汇编指令没有对等的高级语言语句.

  7. 自修正代码. 自修正代码有时候因为干扰了高效率的代码缓存而显得不那么划算. 但它也可以变得有用, 例如在一个必需多次计算自定义方法的数学程序中包含一个小型编译器.

  8. 为空间优化代码. 如今的存储空间和内存如此廉价, 为减少代码量使用汇编语言已不再值当. 但缓存尺寸仍然是一块关键的资源, 我们要让它适合代码缓存的大小, 在某些情况下, 为此优化一段关键代码的尺寸还是有用的.

  9. 为速度优化代码. 多数情况下当代的C++编译器把代码优化得很好. 但有些情况下编译器仍然表现很差, 这时候小心使用汇编编程可以在速度上获得惊人的优化.

  10. 函数库. 优化很多程序员都在用的函数库获利更多.

  11. 让函数库和多编译器, 多操作系统兼容. 让有多个输入的函数库和不同的编译器和操作系统兼容是可能的. 这需要使用汇编编程.

这本手册主要讨论优化代码速度, 其他几个方面也会讲下.

1.2 不使用汇编代码的理由

汇编编程有许多缺陷和问题, 建议决定使用汇编代码来完成特定需求之前考虑其他方案. 最重要的几条不适用汇编编程理由有:

  1. 开发时间. 使用汇编语言写代码比使用高级语言花时间多了.

  2. 可靠性和安全性. 使用汇编代码很容易犯错误. 汇编器不会检查你有没有遵守调用惯例和寄存器保存惯例. 没人管你在所有分支和路径中加起来的push和pop指令数目是不是相同. 汇编代码搞出潜在错误的方法很多, 除非你有一套系统的方法测试和验证, 项目的可靠性和安全性都会受到影响.

  3. 调试和验证. 汇编代码更难调试和验证因为出错的可能性比高级语言多.

  4. 可维护性. 汇编代码也更难修改和维护, 因为这门语言允许没有结构的乱糟糟的代码存在, 还允许你使用别人都看不懂的卑鄙的技巧. (你造吗, 我自己第二天都看不懂了, o(╯□╰)o) 你需要完善的文档和可持续编程风格. (啥? o(>﹏<)o)

  5. 系统代码可以使用伪指令函数(intrinsic functions)代替指令集. 最好的当代C++编译器提供伪指令函数来访问系统控制寄存器和其他系统指令. 伪指令函数可用时, 设备驱动和其他系统代码不再需要汇编代码了.

  6. 应用代码可以使用伪指令函数或者向量类代替汇编. 最好的当代C++编译器提供伪指令代码来实现向量操作和其他之前需要汇编编程的特殊指令. 为了利用SIMD的优越性, 我们不再非使用老式汇编代码不可了. 见34页.

  7. 可移植性. 汇编代码的平台相关性强. 移植到不同的平台上有困难. 而使用伪指令函数来代替汇编的代码可以移植到所有x86和x86-64的平台上.

  8. 编译器已经进步很多了. 现在最好的编译器在大多数情况下已经比汇编程序员的平均水平干得还要好.

  9. 编译代码有可能比汇编代码还要快, 因为编译器可以做过程间(inter-procedural)优化和全局优化. 为了让代码可以被测试和验证, 汇编程序员必须遵循函数与调用之间的调用规则. 很多编译器使用的优化方法因此不能使用, 比如内联函数, 寄存器分布, 常量传递(constant propagation), 从函数中剔除通用子表达式, 函数之间的调度等等. 这些优点可以通过使用伪指令代码加上c++代码来代替汇编代码加以利用.

1.3 这本手册谈论的操作系统

以下操作系统可以使用x86家族的微处理器: 16位: Dos, Windows 3.x. 32位: Windows, Linux, FreeBSD, OpenBSD, NetBSD, Intel-based Mac OS X. 64位: Windows, Linux, FreeBSD, OpenBSD, NetBSD, Intel-based Mac OS X.

所有类Unix操作系统(Linux, BSD, Mac OS)使用一样的调用惯例, 鲜有例外. 这本手册里提到的Linux相关的信息也适用于其他类Unix系统, 有些没被提到的系统也适用.

原文在这里: http://www.agner.org/optimize/

2 开始之前

2.1 开始编程之前要决定的事情

使用汇编开始编程之前, 你得好好想想为什么你要使用汇编语言, 你的程序的哪部分需要使用汇编来完成, 以及使用什么样的编程方法. 要是开发策略还不明了, 很快你就会发现自己在浪费时间, 优化程序中错误的部分, 使用汇编来干本来可以用c++干的事儿, 尝试优化不可能再优化的东西, 写出一堆一堆难以维护的代码, 或者充斥着错误而且无法调试的代码.

下面列出了开始编程之前需要考虑的几件事:

  • 不要用汇编写整个程序. 那会浪费时间. 只应该在追求速度而且可以取得大幅速度提升的地方使用汇编代码. 要用c或者c++实现大部分的程序. 它们是最容易和汇编代码混合使用的语言了.

  • 如果使用汇编的目的是完成系统代码或者使用某些标准c++不提供的指令, 应使用函数或者功能完备的类把汇编代码和程序的其他部分隔离开来. 可能的话使用伪指令函数(见34页)

  • 如果使用汇编的目的是优化速度就得先定位出程序的哪一部分最消耗cpu时间, 可能得用profiler .看看瓶颈是访问文件, 访问内存, CPU指令还是别的什么, 就像手册1: "优化c++软件" 中讲的那样. 把程序的关键部分用函数或者功能完备的类隔离出来.

  • 如果使用汇编是为了完成函数库, 要清楚地定义库的功能. 先定下是要完成函数库还是类库. 再定下是要使用静态链接(Windows的.lib, Linux的.a)还是动态链接(Windows的.dll, Linux的.so). 静态链接更高效, 但是从C#或者VB调用的话只能用动态链接库了. 可能动态和静态的版本都要事先.

  • 如果使用汇编是为了优化嵌入式应用的空间和速度, 找个支持C/C++和汇编的开发工具, 并且尽可能用C/C++实现.

  • 代码是可重用的还是应用独有的. 用心优化可重用代码更合理. 可重用代码最合适的实现方式是函数库或者类库.

  • 代码是否支持多线程. 多线程应用可以使用多核微处理器. 在线程之内需要保存以函数调用之间传递的数据应该由调用程序保存在C++类或者线程缓冲区内

  • 可移植性对应用软件是否重要. 软件要在Windows, Linux和Intel-based Mac OS下都工作吗? 32位和64位模式都要支持吗? 非x86平台呢? 这些事情对编译器的选择, 汇编器的选择和编程方法都很重要.

  • 软件是否要支持旧的微处理器? 如果是的话, 你可能要实现一个SSE2指令集之类的版本给微处理器用, 另一个版本用来和旧微处理器兼容. 甚至可以为每个特殊的CPU都实现一个优化版本. 建议使用自动CPU分配(见137页).

  • 有三种汇编编程方法可选: (1) 在C++编译器中使用伪指令函数和向量类. (2) 在C++编译器中使用内联汇编. (3)使用汇编器. 这三种方法和它们的优缺点在第5, 6, 7章讲述(见34, 36, 45页).

  • 使用汇编器的话还要在不同的语法中选择. (作者?)倾向使用和你的C++编译器生成的汇编代码兼容的汇编器.

  • 先使用C++实现并且尽可能用手册1: "优化C++软件"中介绍的方法优化它. 让编译器把代码翻译成汇编代码. 看看编译器生成的汇编代码, 看看你的(C++)代码是否还有改进余地.

  • 高度优化的代码对其他人来说是很难阅读和理解的,即使你自己过段时间回来看也这样. 为了让代码可维护, 把汇编代码组织成小一些的逻辑单位(过程或者宏), 遵循良好的接口定义和调用规范, 适当的注释都是很重要的. 制定一个可持续化的注释和文档策略.

  • 保存编译器, 汇编器和所有开发工具, 连同源代码和项目文件, 以备来日维护可用. 几年之后需要更新和修改代码的时候, 兼容的工具可能就没有了. (纳尼?)

2.2 制定测试策略

正如前文所述, 汇编代码爱出错, 难调试, 难写得条理清晰, 难读, 还难维护. 持续的测试策略可以改善这些问题, 节省很多时间.

我的建议是让汇编代码自成一体, 模块, 方法, 类或者接口对调用者易用的库. 先全用C++实现. 然后写一个覆盖所有待优化代码的测试程序. 使用测试程序和在最终产品中测试模块相比更安全也更容易.

测试程序有两个目的. 其一是验证汇编代码在所有分支下都正常工作. 其二是测试汇编代码的速度时, 可以不启动界面, 不访问文件, 也不调用应用程序的其他部分, 这样测试结果更准确并且可重现.

开发过程中的每一步, 每一次对代码的修改之后, 都应该使用测试程序.

保证测试程序正确地工作. 花很多时间根据测试结果在代码里面找错误, 结果实际上是测试程序出错了这样的事情也不少.

有各种测试方法可以用来验证代码是否工作正常. 白盒测试提供一系列精挑细选的输入数据集合保证代码中的各个分支路径和特例都会被测试到. 黑盒测试提供一系列随机输入数据并验证输出结果是否正确. 优秀并且足够长的随机生成机制有时可以发现白盒测试发现不了的极小概率错误.

测试程序可以通过对比汇编代码输出和C++代码输出来验证是否正确. 测试应覆盖所有边界条件并且最好有一些非法输入来验证代码生成的错误反馈是否正确.

速度测试应提供真实输入数据. 在包含很多分支的代码中, CPU的时间很大一部分可能消耗在错误的分支预测上. 错误的分支预测数量取决于输入数据的随机程度. 你可以先试验一下输入数据的随机程度对计算时间的影响, 然后定下一个符合典型真实应用(场景)的随机度.

提供长测试数据流的自动化测试程序通常比在最终软件中测试更多更快地找到错误. 好的测试程序可以找到大部分错误, 但是你不能保证它找到所有错误. 有可能有些错误只有在最终产品中(模块)结合起来时才会出现.

2.3 常见编程陷阱

下面列出了一些汇编编程时最经常犯的错误.

  1. 忘记保存寄存器. 有些寄存器保存调用者状态, 例如EBX. 如果函数中修改了这些寄存器的状态, 那要在函数开始时保存它们, 在退出前回复它们. 记住POP指令的顺序要和PUSH指令的顺序正好相反. 调用者会保存的寄存器列表见28页.

  2. 不配对的PUSHPOP指令. 在方法的所有可能路径中, PUSHPOP指令的数目都必须相等. 例如:

    例子2.1. 不配对的 push/pop

    
     push ebx
     test ecx, ecx
     jz Finished
     ...
     pop ebx
     Finished: ; 错误! 标记要在pop ebx之前跳转. 
     ret
     

    此处若ECX的值为0则被压栈的EBX没有出栈. 结果就是RET会弹出EBX之前的值从而跳到错误的地址去.

  3. 用了另有他用的寄存器. 有些编译器保留EBXEBP用作指针或者其他用处. 在内联汇编中使用这些寄存器做别的事情可能会引起错误.

  4. 压栈后的相对栈取址. 相对栈指针对变量取址时, 必须把之前修改栈指针的操作都考虑进去. 例如:

    例 2.2. 相对栈取址

    mov [esp+4], edi
    push ebp
    push ebx
    cmp esi, [esp+4] ; 可能出错!
    
    在此程序员可能意图比较ESIEDI, 但是ESP寄存器的值已经被两次PUSH操作修改了, 所以ESI实际上在和EBP作比较.
  5. 混淆变量的值和地址. 例如: 例 2.3. 值与地址(MASM语法)

    
     .data
     MyVariable DD 0 ; 定义变量
     .code
     mov eax, MyVariable ; 读取变量MyVariable的值
     mov eax, offset MyVariable; 读取MyVariable的地址
     lea eax, MyVariable ; 读取MyVariable的地址
     mov ebx, [eax] ; 通过指针读取MyVariable的值
     mov ebx, [100] ; 忽略括号, 读取常量100
     mov ebx, ds:[100] ; 从地址100读取变量
     
  6. 忽略调用惯例. 遵守调用惯例是很重要的, 比如参数的顺序, 参数是通过栈还是寄存器传递, 以及被调用者或者被调用函数有没有清空栈. 见27页.

  7. 函数名称压延. C++代码调用汇编函数要使用extern "C"避免名字压延. 有些系统要求汇编函数名称前有下划线_. 见30页.

  8. 忘记返回. 函数声明必须以RETENDP结尾. 只使用一个是不够的. 没有RET的话过程调用之后操作会继续在代码中执行.

  9. 忘记栈对齐. 在任何调用之前栈指针必须指向可以被16整除的地址, 除非在16位或者32位Windows下. 见27页.

  10. 忘记64位Windows的shadow space. 在64位Windows下任何函数调用之前都要保留32 个字节的空栈控件. 见30页.

  11. 混用调用惯例. 64位Windows和64位Linux的调用惯例是不同的. 见27页.

  12. 忘记清空浮点寄存器栈. 所有函数中用到的浮点栈寄存器都要被清空, 通常通过在返回前调用指令FSTP ST(0)来清空, 除了ST(0), 如果它被用作存放返回值的话. 有必要记录有多少个浮点寄存器被使用. 如果一个函数压栈到浮点寄存器栈的次数比出栈的次数多, 寄存器栈就会每次函数调用都增长. 栈溢出的时候就会产生异常. 这个异常可能在程序其他地方冒出来.

  13. 忘记清空MMX状态. 使用MMx寄存器的函数需要用EMMS指令在调用或返回前清空它们.

  14. 忘记清空YMM状态. 使用YMM寄存器的函数要在调用或返回前使用VZEROUPPERVZEROALL清空它们.

  15. 忘记清空方向标记位. 任何使用STD设置方向标记位的函数都要在调用或返回前使用CLD清空它.

  16. 混用有符号和无符号整形. 无符号整形通过JBJA来比较. 有符号整形通过JLJG. 混用有符号和无符号整形数会有意想不到的后果.

  17. 忘记按比例缩放数组索引. 数组索引要乘以数组元素的大小. 例如mov eax, MyIntegerArray[ebx*4].

  18. 数组越界. n个元素的数组索引是0到n-1, 不是1到n. 有问题的循环在数组中越界写入, 会在程序中其他地方引起问题, 很难找到.

  19. ECX=0来做循环. ECX等于0的话以LOOP指令结尾的循环会被执行232次. 确保在循环调用之前ECX等于0.

  20. INCDEC之后读carry标记位. INCDEC指令并不修改carry 标记位. 不要在调用它们之后使用诸如ADC, SBB, JC, JBE, SETA等指令读取carry标记位. 使用ADDSUB代替INC和`DEC来避免这类问题.

原文在这里: http://www.agner.org/optimize/

3 汇编编程基础

3.1 汇编器

x86指令集有几个汇编器, 但是没有一个好到推荐通用的. 汇编程序员面临着x86汇编没有通用语法的窘境. 不同的编译器使用不同的语法. 下面列出了常用的几个.

MASM 微软汇编器被包含在微软C++编译器里. 免费版本可以通过下载微软Windows驱动工具包 (WDK) 或者平台软件开发工具包 (SDK)得到, 也作为免费Visual C++ Express的插件. MASM事实上充当了好多年的Windows标准, 而且大多数Windows编译器也是用MASM语法输出汇编. MASM具有很多高级语言特性. 由于衍生自8086处理器的最早的汇编器, 该语法某种程度上有点混乱和不一致. 微软至今仍在维护MASM以期为Windows提供一套完备的开发工具, 但这显然已经无利可图了, 对MASM的维护也只是勉力为之. 新指令会定期加进来, 但是64位版本有些缺陷. 新版本只能在Windows XP和之后的操作系统上运行, 还得是在安装了编译器的情况下. 第6版之前(含)可以在任何系统上运行, 包括安装了Windows 仿真器的Linux. 这些版本散落在网上.

GAS

Gnu汇编器是Gnu Binutils 包的一部分, 这个包随着大多数Linux, BSD 和 Mac OS X 一起安装. Gnu编译器使用Gnu 汇编器生成汇编输出再做链接. Gnu 汇编器通常使用AT&T语法, 这种语法在机器生成代码时表现很好, 但是在人工生成的汇编代码使用上有诸多不便. AT&T语法在操作数的顺序上和其他所有x86的汇编器都不一样, 跟Intel与AMD发布的指令文档也不一样. 它使用不一样的前缀例如%和$来指定操作数的类型. Gnu汇编器在所有x86平台上都可以用. 好事是, 新一点的Gnu汇编器可以选择用Intel语法. Gnu-Intel语法和MASM语法几乎一样. Gnu-Intel语法值定义了指令码的语法, 没有指令, 函数, 宏等. 指令仍然使用老式的Gnu-AT&T语法. 通过指定.intel_syntax noprefix来使用Intel语法. 在离开C或C++代码中的内联汇编之前通过.att_syntax prefix回到AT&T语法.

NASM

NASM是免费的开源汇编器, 支持几个平台, 也支持Object文件类型. 它的语法比MASM更清楚和统一. NASM比MASM的高级特性少, 但是多数情况下足够高效. 如果不需要MASM语法的话, NASM是我最推荐的多平台下的汇编器.

YASM YASM和NASM很相似并且使用同样的语法. 之前YASM比NASM更可靠但是现在没有人定期更新它了.

FASM

Flat汇编器是另一款多平台开源汇编器. 语法和其他汇编器不兼容. FASM本身是用汇编语言实现的 - 听起来不错, 不幸的是这令它的开发和维护都没那么及时.

WASM

WASM汇编器包含在Open Watcom C++编译器中. 语法类似MSASM, 某种程度上不同. 不是很跟得上时代.

JWASM

JWASM是WASM的进阶版. 它和MASM语法完全兼容, 包括更高级的宏和高级指令. 如果需要使用MASM语法的话, JWASM不错.

TASM

CodeGear C++ Builder带有Borland Turbo 汇编器. 除了几处新的语法之外它和MASM完全兼容. TASM不再继续维护, 但还是可用的. TASM已经被淘汰了, 而且不支持现在的指令集了.

GOASM

GoAsm是32位64位Windows都可用的免费汇编器, 还包括源代码编译器, 链接器和调试器. 语法和MASM类似但不完全兼容. 它没有跟上最新的指令集. 一个叫做Easy Code的IDE可以使用.

HLA

高级汇编器(High Level Assembler)实际上是一个高级语言编译器, 它允许使用类汇编语言的语句并且声称汇编输出. 在发明HLA的时候这主意不错, 但是现在最好的C++编译器已经支持伪指令函数了, 我觉得没必要用HLA了.

内联汇编 微软和英特尔的C++编译器支持使用MASM的语法子集进行内联汇编. 可以访问C++变量, 方法和标签, 插入到汇编代码里就行了. 这很容易, 但是它不支持C++寄存器变量. 见36页.

Gnu编译器支持内联汇编, 可以访问Gnu汇编器在Intel和AT&T语法下的所有指令(instructions)和命令(directives). 在汇编中访问C++变量则挺复杂的.

Linux和Mac系统下的Intel编译器支持微软风格和Gnu风格的内联汇编.

<注> 关于instructions和directives: directive用来指导编译器去工作,如像 org .0x000. 其中的org是 directive,指导编译器将接下来的代码放在地址为0x000的空间。 还有如 db 0xff, DW 0x36等等. 而instruction 是指令,编译器将其转化成对应的机器码,然后CPU可以识别并执行这些机器码,如 mov P1,0xff. ADD R3,56等地址0000h~FFFFh是用十六进制表示的,计算机中的1K=2的10次方。0xffff行于 64个2的10次方。

C++的伪指令函数

这是最新的也是最便利的结合底层代码和高级语言的方式. 伪指令函数是以高级语言面貌出现的机器指令. 比如你可以在C++中调用伪指令函数来做向量加法, 这和使用向量相加的汇编指令是等价的. 更进一步, 可以定义向量类 的重载运算符+, 这样向量的加法就可以简单第写成+. 微软, 英特尔和Gnu的编译器都支持为伪指令函数. 见34页和手册1: "优化C++软件".

选择哪一个汇编器?

多数情况下最简单的解决方案是在C++代码里使用伪指令函数. 编译器会做大部分优化, 程序员只需专心选择最好的算法并组织向量中的数据就好了. 系统程序员可以使用伪指令函数访问系统指令, 不需要使用汇编语言.

真正需要底层开发时, 例如高度优化的函数库或者设备驱动, 你可能需要用到汇编器.

可能选择跟你在用的C++编译器兼容的汇编器好一些. 这样你就可以先用编译器把C++翻译成汇编代码, 进一步优化汇编代码, 然后使用汇编器. 如果汇编器和编译器生成的汇编代码不兼容的话, 你就得用编译器生成对象文件(*.obj), 再反汇编成你需要的汇编语法. objconv反汇编器支持集中不同的语法方言.

很多情况下NASM汇编器是一个合适的选择, 它支持多平台和对象文件格式, 维护的不错, 通常跟的上最新的指令集.

除非特别说明, 这本手册的例子使用MASM语法. MASM语法在 msdn.microsoft.com 的微软Macro汇编器文档中有讲.

www.agner.org/optimize 有各种语法手册, 编码手册和论坛的链接.

3.2 寄存器集和基本指令

16位模式寄存器

通用寄存器(General Purpose)和整形寄存器

全寄存器(Full Register) 部分寄存器(Partial register) 部分寄存器(Partial register)
bit 0 - 15 bit 8 - 15 bit 0 - 7
AX AH AL
BX BH BL
CX CH CL
DX DH DL
SI
DI
BP
SP
Flags
IP
表 3.1. 16位模式下的通用寄存器.

微处理器和操作系统支持的话, 32位寄存器在16位模式下也可用. ESP的高位字不要用, 在跳转时它不会被保存.

浮点寄存器(Floating Point Registers)

全寄存器
bit 0 - 79
ST(0)
ST(1)
ST(2)
ST(3)
ST(4)
ST(5)
ST(6)
ST(7)
表 3.2. 浮点栈寄存器

微处理器支持的话MMX寄存器也可以用. 微处理器和操作系统支持的话XMM寄存器可以使用.

段寄存器

全寄存器
0-15位
CS
DS
ES
SS
表 3.3. 16位模式下的段寄存器

FS和GS寄存器可能可用.

32位模式下的寄存器

通用寄存器和整形寄存器

全寄存器 0-31位 部分寄存器 0-15位 部分寄存器 8-15位 部分寄存器 0-7位
EAX AX AH AL
EBX BX BH BL
ECX CX CH CL
EDX DX DH DL
ESI SI
EDI DI
EBP BP
ESP SP
EFlags Flags
EIP IP
表 3.4. 32位模式下的通用寄存器

浮点和64位向量寄存器

全寄存器0-79位 部分寄存器0-63位
ST(0) MM0
ST(1) MM1
ST(2) MM2
ST(3) MM3
ST(4) MM4
ST(5) MM5
ST(6) MM6
ST(7) MM7
表 3.5. 浮点和MMX寄存器

MMX寄存器只在微处理器支持的情况下可用. ST和MMX寄存器在同一块代码里不可混用. 使用MMX寄存器的代码要调用EMMS指令来和后续的使用ST寄存器部分划清界限.

128和256位整形向量和浮点向量寄存器

全寄存器0-255位 全寄存器或部分寄存器0-127位
YMM0 XMM0
YMM1 XMM1
YMM2 XMM2
YMM3 XMM3
YMM4 XMM4
YMM5 XMM5
YMM6 XMM6
YMM7 XMM7
表 3.6. 32位模式下的XMM寄存器和YMM寄存器

只有在微处理器和操作系统都支持的情况下XMM寄存器可用. 浮点向量的指令在单精度和双精度下分别只使用32位或64位的XMM寄存器. (Scalar floating point instructions use only 32 or 64 bits of the XMM registers for single or double precision, respectively) 这句话是说XMM寄存器还分32位和64位的, 还是说只有寄存器中的一部分被使用了? YMM寄存器只在处理器和操作系统都支持AVX指令集的情况下可用.

段寄存器

全寄存器0-15位
CS
DS
ES
FS
GS
SS
表 3.7. 32位模式下的段寄存器