计算机执行机器代码;
用字节序列编码低级的操作,包括处理数据、管理存储器、读写存储器设备上的数据、以及利用网络通信。
编译器基于编程语言的规则、目标机器的指令集和操作系统遵循的规则,经过一系列阶段产生机器代码。
例如GCC C语言编译器以汇编代码的形式产生输出,汇编代码的本质是机器代码的文本表示。然后GCC调用汇编器和链接器,从而根据汇编代码生成可执行的机器代码。
近距离观察机器代码,以及人类可读的形式——汇编代码。用高级语言编程的时候,机器屏蔽了程序的细节,即机器级实现。高级语言的抽象级别比较高,用高级语言编写的程序可以在不同的机器上实现,而汇编代码则是也特定机器密切相关的。
虽然编译器承担了汇编代码的大部分工作,但是对于严谨的程序员来说,能够阅读和理解汇编代码仍是一项很重要的技能。通过适当的命令行调用编译器生成以汇编形式表示的输出文件。通过阅读这些汇编代码,能够理解编译器的优化能力,并分析代码中隐含的低效率。
精通细节是理解更深和更基本概念的先决条件。
两种机器语言(汇编语言):Intel IA32和x86-64;
IA32是主导语言;而x86-64是前者在64位机器上的扩展;
Intel处理器是从简单的16位处理器发展起来的;
其体系结构也在相应地发展,一方面为了向后兼容,所以会看起来有一些相当奇怪的设计;现在编译器和操作系统可能都不使用这些特性。
将IA32扩展到64位,称为x86-64。
程序员学习汇编代码的需求随时间推移发生了变化,开始时要求程序员能直接用汇编语言编写程序,现在则是要求他们能够看懂和理解编译器的代码;
编译器会产生一个以汇编代码形式表示的输出文件。
理解汇编语言的好处:
1、通过阅读这些汇编代码,我们能够理解编译器的优化能力,并分析代码中隐含的低效率。
2、试图最大化一段关键代码的性能的程序员,会尝试源代码的各种形式,每次编译并检查产生的汇编代码,从而了解程序运行的效率如何。
3、高级语言提供的抽象层会隐藏我们想要了解的有关程序运行时行为的信息。
====================================================
历史观点
Intel处理器系列俗称x86,经历了一个长期的,不断进化的过程。
以下列举了一些Intel处理器的模型,以及它们的一些关键特性,特别是影响机器级变成的特性:
有很多厂家生产了与Intel兼容的处理器,能够运行相同的机器代码;
i386型号的处理器开始,能够支持Unix操作系统。
Linux使用平坦寻址的方式,使得程序员将整个存储空间看做一个大的字节数组。
====================================================
程序编码
优化级别影响了最终程序的运行速度。
目标代码文件o是机器代码的一种形式,它包含所有指令的二进制表示,但是还没有填入地址的全局值。
链接器将两个目标代码文件与实现库函数的代码合并,并产生最终的可执行代码文件p。可执行代码是我们要考虑的机器代码的第二种形式。也就是处理器执行的代码格式。
在一台IA32的机器上,用Unix命令行编译两个文件p1.c和p2.c:
unix>gcc -o1 -o p p1.c p2.c
命令gcc指的是GCC C编译器,也是Linux上默认的编译器;也可以用简单指令cc来启动它
编译选项-o告诉编译器使用第一级优化;
通常提高优化级别会使得程序运行得更快,但是编译时间会更长,用调试工具对代码进行调试更困难;这是因为使用更高级别的优化使得产生的机器代码和初始源代码之间的关系难以理解。
实际上gcc命令做了一系列步骤:
首先使用C预处理器扩展源代码,插入所有用#include命令制定的文件,扩展所有用#define声明指定的宏。
然后编译器产生两个源代码的汇编代码,名字分别为p1.s和p2.s。
接下来汇编器将汇编代码转换为二进制目标代码,名为p1.o和p2.o。
最终链接器将连个目标代码文件与实现库函数的代码文件合并,产生最终可执行代码文件p。
目标代码是机器代码的一种形式,它包含所有指令的二进制表示,但是还没有填入地址的全局值。
机器级代码
利用更简单的抽象来隐藏实现的细节。计算机系统使用了多种不同形式的抽象。
第一种抽象:机器级程序的格式和行为定义为指令集体系结构。即ISA(Instruction set architecture)。定义了处理器状态、指令的格式,以及每条指令对状态的影响。将程序的行为描述成好像每条指令是按顺序执行的,一条指令结束后,另一条指令开始。处理器的硬件远比描述的复杂而精细。但是可以保证其整体行为与ISA指定的顺序执行完全一致。
第二种抽象:机器级程序使用的存储器地址是虚拟地址。提供的存储器模型 看上去是一个非常大的字节数组。
机器代码和原始的C代码差别非常大,一些通常对C语言程序员隐藏的处理器状态是可见的:
程序计数器:指向下一条指令在存储器中的地址。
整数寄存器文件包含8个命名的位置,分别存储32位的值。这些寄存器可以存储地址或整数数据。例如过程的局部变量和函数的返回值。
条件码寄存器保存着最近执行的算术或逻辑指令的状态信息。他们用来实现控制或数据流中的条件变化,比如说用来实现if和while语句。
一组浮点寄存器存放浮点数据。
程序存储器包含:程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,以及用户分配的存储器块。程序存储器用虚拟地址来寻址。操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器存储器中的物理地址。
一条机器指令只执行一个非常基本的操作。
====================================================
数据格式
====================================================
访问信息
====================================================
算术和逻辑操作
====================================================
控制
====================================================
过程
====================================================
数组分配的访问
====================================================
异质的数据结构
====================================================
理解指针
====================================================
使用GDB调试器
====================================================
存储器的越界引用和缓冲区溢出
====================================================
x86-64:将IA32扩展到64位
====================================================
浮点程序的机器级表示
====================================================
小结