文章目录
- 前言
- 3.1 Intel处理器的发展历史
前言
在用高级语言如 C 语言编程时,我们被屏蔽了程序具体的机器级实现。相比之下,在用汇编代码写程序时,程序员必须明确指定程序该如何管理存储器(memory)和用来执行计算的低级指令。
使用现代的优化编译器,产生的代码通常至少与一个熟练的汇编语言程序员手工编写的代码一样有效。最好的一点是,用高级语言编写的程序可以在很多不同的机器上编译执行,而汇编代码则是与特定机器密切相关的。
启动编译器时带上适当的选项,编译器就会产生一个汇编代码文件,汇编代码非常接近于计算机执行的实际机器代码。与目标代码的二进制格式相比,它的主要特色在于它采用的是更加易读的文本格式。通过阅读这些汇编代码,能够理解编译器的优化能力,并分析出代码中潜在的低效率。
本章的学习目标
- 学习某种汇编语言的详细内容,明白 C 程序是如何编译成这种形式的机器代码的。
- 了解典型的编译器在将 C 程序结构变换成机器代码时所做的转换。相对于 C 代码中表示的计算操作,优化编译器能够重新排列执行顺序,消除不必要的计算并替换慢速操作,例如用加法和移位来代替乘法,甚至于将递归计算变换成迭代计算。
学习脉络
- 我们的技术讲解是从快速浏览 C、汇编代码以及目标代码之间的关系开始的。
- 然后会讲到 IA32 的细节,从数据的表示和处理,及控制的实现开始。会看到如何实现 C 语言中的控制结构,如
if
、while
和switch
语句。这时,会讲到过程的实现,包括运行栈是如何支持过程间数据和控制的传递,以及局部变量的存储(storage)。 - 接着,会考虑在机器级如何实现像数组、结构和联合(union)这样的数据结构。基于这些机器级编程的背景知识,看看存储器访问越界的问题,以及系统容易遭受缓冲区溢出攻击的问题。
- 这一部分的结尾,会给出一些用 GDB 调试器来检查机器级程序运行时行为的技巧。
- 简要介绍了一些 GCC 对在 C 程序中嵌入汇编代码的支持。在某些应用程序中,程序员必须要用汇编代码来访问机器的某些低级特性。这时,嵌入汇编代码就是最好的方法。
3.1 Intel处理器的发展历史
Intel 处理器系列始于一个单芯片、16位微处理器。
下面的列表展示了按照时间顺序排列的 Intel 处理器模型,以及它们的一些关键特性。我们用实现这些处理器所需要的晶体管数量来表明它们复杂性的演变过程(K 表示 1000,M 表示 1000000)。
- 8086:(1978,29K个晶体管)。它是第一代单芯片、16位微处理器之一。8088,即8086加上8位外部总线(external bus),构成最初的 IBM 个人计算机的心脏。IBM与当时还很小的微软签订合同,开发 MS-DOS 操作系统。最初的机器型号有 32 768 字节的存储器和两个软驱(没有硬盘驱动器)。从体系结构上来说,这些机器只有 655 360字节的地址空间——地址只有 20 位长(1 048 576 字节可被寻址),而操作系统保留了 393 216 字节自用。
- 80286:(1982, 134K个晶体管)。增加了更多的寻址模式(有些现在已经废弃了)。构成了 IBM PC-AT 个人计算机的基础,这种计算机是MS Windows 最初的使用平台。
- i386:(1985,275K个晶体管)。将体系结构扩展的 32 位。增加了平面寻址模式(flat addressing model),Linux 和最近版本的 Windows 系列操作系统都是使用的这种模式。这是 Intel 系列中第一台支持 Unix 操作系统的机器。
- i486:(1989,1.9M个晶体管)。改善了性能,同时将浮点单元集成到处理器芯片上,但是没有改变指令集。
- Pentium:(1993,3.1M个晶体管)。改善了性能,不过只对指令集增加了小的扩展。
- PentiumPro:(1995,6.5M个晶体管)。引入了全新的处理器设计,在内部被称为P6微体系结构。指令集中增加了一类“条件传送(conditional move)” 指令。
- Pentium/MMX:(1997,4.5M个晶体管)。在 Pentium 处理器中增加了处理整数向量的新指令类。每个数据可以是1、2或4个字节长。每个向量总长 64 位。
- Pentium II:(1997,7M个晶体管)。通过在 P6 微体系结构中实现 MMX 指令,合并了以前分离的 PentiumPro 和 Pentium/MMX 系列。
- Pentium III:(1999,8.2M个晶体管)。引入另一类处理整数或浮点数向量的指令,每个数据可以是1、2 或 4 个字节长,打包成 128 位的向量。由于在芯片上包括了二级高速缓存,这种芯片后来的版本最多使用了 24M 个晶体管。
- Pentium 4:(2001,42M个晶体管)。在向量指令中增加了 8 字节整数和浮点格式,以及针对这些格式的 144 个新指令。在编号惯例上,Intel 不再使用罗马数字。
每个时间上相继的处理器设计都是向后兼容的——也就是,较早版本上编译的代码是可以在较新的处理器上运行的。为了保持这种进化传统,指令集中有许多非常奇怪的东西。Intel 现在称其指令集为 IA32,也就是“Intel 32 位体系结构(Intel Architecture 32-bit)”。这个处理器系列也俗称为 “x86”,反映出直到 i486 的处理器命名惯例。
对由 GCC 编译器产生出的、运行在 Linux 操作系统平台上的程序,感兴趣的人并没关注到 IA32 复杂性的大部分。最初的 8086 中的存储器模型和它在 80286 中的扩展都已经过时了。作为替代,Linux 使用了平面寻址方式(flat addressing),在这种寻址方式中,程序员将整个存储空间看做一个大的字节数组。
从列出的发展过程中,可以看到,IA32 中加入了很多处理小整数和浮点数向量的格式和指令。增加这些特性是为了提高多媒体应用程序的性能,例如图像处理、音频和视频编码和解码,以及三维计算机图形。不幸的是,目前版本的 GCC 产生的代码不会使用这些新特性。实际上,在默认启动方式下,GCC 会假设它是为一个 i386 机器产生代码,编译器不会试图使用许多添加到现在看来已经非常老的体系结构的扩展特性。