计算机执行机器代码,用字节序列编码低级的操作,包括处理数据、管理存储器、读写存储设备上的数据,以及利用网络通信。编译器基于编程语言的原则、目标机器的指令集和操作系统遵循的规则,经过一系列的阶段产生机器代码。
GCC C语言编译器以汇编代码的形式产生输出,汇编代码是机器代码的文本表示,给出程序中的每一条指令。然后GCC调用汇编器和链接器,从而根据汇编代码生成可执行的机器代码。在本章中,我们会近距离地观察机器代码,以及人类可读的表示-——汇编代码。
高级语言编程,机器会屏蔽程序实现的细节, 且我们使用起来不容易出错,为什么我们还需要花时间学习机器代码?
程序员学习汇编代码的需求随着时间的推移也发生了变化,开始时只要求程序员能直接用汇编语言编写程序,现在则是要求他们能够阅读和理解编译器产生的代码。
我们将详细学习两种特别的汇编语言: 了解如何将 C程序编译成这些形式的机器代码。阅读编译器产生的汇编代码,需要具备的技能不同于手工编写汇编代码。我们必须了解典型的编译器在将 C程序结构变换成机器代码时所做的转换。相对于C代码表示的计算操作,优化编译器能够重新排列执行顺序,消除不必要的计算,用快速操作替换慢速操作,甚至将递归计算变换成迭代计算。源代码与对应的汇编码的关系通常不太容易理解——就像要拼出的拼图与盒子上图片的设计不太一样。
学习建议: 精通细节是理解更深合更基本概念的先决条件。 “理解一般规则,而不愿意劳神学习细节”实际上是自欺欺人。
本文讲解事项点:
Linux使用了平坦寻址方式将整个存储空间看成一个大的字节数组。
编译选项 -01 告诉编译器使用第一级别的优化, 提高优化级别会使最终程序运行得更快,但编译时间更长,从得到的程序性能方面考虑,第二级别的优化 -02 是被认为较好的选择。
实际上gcc命令调用了一系列程序,使得源代码转化为可执行的代码。
机器级编程,其中两种抽象尤为重要。
在整个编译过程中,编译器会完成大部分的工作,将把用C语言提供的相对比较抽象的执行模型表示的程序转化成处理器执行的非常基本的指令。汇编代码有一个的主要特点,即它用可读性更好的文本格式来表示。能够理解汇编代码以及它与原始C代码的联系,是理解计算机如何执行程序的关键一步。
IA32机器代码和原始C代码的差别非常大,一些通常对C语言程序员隐藏的处理器状态是可见的。
int accum = 0;
int sum(int x, int y) {
int t = x + y;
accum += t;
return t;
}
命令上使用 -S 能得到C语言编译器产生的汇编代码 code.s :
gcc -O1 -S code.c
以上代码中每个缩进去的行都对应一条机器指令。比如,pushl指令表示应该将寄存器 % ebp 的内容压入程序栈。这段代码中已经除去了所有关于局部变量名或数据类型的信息。我们还看到了一个对全局变量 accum 的引用,这是因为编译器还不能确定这个变量会放在存储器中的哪个位置。
使用 -c 命令行选项, GCC会编译并汇编该代码 生成 code.o :
gcc -O1 -c code.c
如何找到程序的字节表示? 可以利用反汇编器,根据目标代码生成类似汇编代码的格式。 在Linux中 带 -d 命令行标志的程序
OBJDUMP可以充当这个角色。
我们看到按照前面的字节顺序排列的17个十六进制字节值,它们分成了几组,每组有1~6个字节。每组都是一条指令,右边是等价的汇编语言。
其中一些关于机器代码和它的反汇编表示的特性值得注意:
生成实际可执行的代码需要对一组目标代码文件运行链接器, 这一组目标代码文件必须有一个main函数, 定义一个main.c文件,里面有这样的函数:
int main() {
return sum(1, 3);
}
gcc -O1 -o prog code.o main.c
文件prog变成了9123个字节,因为它不仅包含两个过程的代码,还包含了用来启动和终止程序的信息,以及用来与操作系统交互的信息。我们也可以反汇编prog文件∶
这段代码与code.c反汇编产生的代码几乎完全一样。