程序的机器级表示。
GCC C语言编译器以汇编代码的形式产生输出,汇编代码是机器代码的文本表示,然后GCC调用汇编器和链接器,从而根据汇编代码生成可执行的机器代码。
一、历史观点
Linux使用了平坦寻址方式,使程序员将整个存储空间看做一个大的字节数组。
二、程序编码
将源代码转化成可执行代码,C预处理器扩展源代码。
1.机器码代码
两种抽象:机器级程序的格式和行为,定义为指令集体系结构;二是机器级程序使用的存储器地址是虚拟地址,提供的存储器模型看上去是一个非常大的字节数组。
2.代码示例
3.关于格式的注释
三、数据格式
1、大多数常用数据类型都是以双字形式存储的。包括INT和LONG INT。
2、处理字符串数据时,通常会用到字节。
3、浮点数有三种形式:单精度值float,双精度值double,和扩展精度。
4、大多数GCC生成的汇编代码指令都有一个字符后缀,表明操作数的大小。
四、访问信息
一个IA32中央处理单元CPU包含一组8个存储32位值的寄存器。寄存器用来存储整数数据和指针。
1.操作数指示符
各种不同的操作数的可能性被分为三种类型。第一种类型是立即数,也就是常数值。第二种类型是寄存器,它表示某个寄存器的内容,对双字操作来说,可以是8个32位寄存器中的一个。第三类操作数是存储器引用,它会根据计算出来的地址访问某个存储器地址。
2.数据传送指令
把许多不同的指令分成了指令类,一类中的指令执行一样的操作,只不过操作数大小不同。如 MOV中的指令将源操作数的值复制到目的操作数中。源操作数指定的值是一个立即数,存储在寄存器中或者存储器中。目的操作数指定一个位置,要么是一个寄存器,要么是一个存储地址。而 PUSH指令的功能是把数据压入到栈上,而 POPL指令是弹出数据。这些指令都只有一个操作数——压入的数据源和弹出的数据目的。
因为栈和程序代码以及其他形式的程序数据都是放在同样的存储器中,所以程序可以用标准的存储器寻址方法访问栈内任意位置。
3.数据传送示例
五、算术和逻辑操作
给出的每个指令类都有对字节、字、和双字数据进行操作的指令。这些指令被分为四组:加载有效地址、一元操作、二元操作和移位;二元操作有两个操作数,而一元操作有一个操作数。
1.加载有效地址
加载有效地址指令LEAL实际上MOVL指令的变形。它的指令形式是从存储器读数据到寄存器,但是实际上它根本没有引用存储器。
目的操作数必须是一个寄存器。
2.一元操作和二元操作
第二组中的操作是一元操作,它只有一个操作数,既是源又是目的;第三组中操作是二元操作,其中第二个操作数既是源又是目的。
3.移位操作
最后一组是移位操作,先给出移位量,然后第二项给出的是要移位的位数。它可以进行算术和逻辑右移。移位量可以是单节编码,还可以是一个立即数。移位操作数的目的操作数可以是一个寄存器,或者是一个存储器位置。
4.讨论
5.特殊的算术操作
六、控制
到目前为止,我们只考虑了直线代码的行为,也就是指令一条接着一条顺序的执行。机器代码提供两种基本的低级低制来实现有条件的行为:测试数据值,然后根据测试的结果来改变控制流或者数据流。
用JUMP指令可以改变一组机器代码的执行顺序;编译器必须产生指令序列,这些指令序列构建在这种实现C语言控制结构的低级机制之上。
1.条件码
除了整数寄存器,CPU还维护着一组单个位的条件码寄存器。最常见的条件码有CF、ZF、SF、OF;
LEAL指令不改变任何条件码,因为它是用来进行地址计算的。
CMP指令和SUB指令的行为是一样的。
TEST指令和AND指令的行为一样的,除了他们只设置条件码而改变目的寄存器的值。典型的用法,两个操作数是一样的,或者其中的一个操作数是一个掩码,用来指示哪些位应该被测试。
2.条件访问码
条件码通常不会直接读取,常见的使用方法有:1.)可以根据条件码的某个组合,将一个字节设置为0或者1;2.)可以条件跳转到程序的某个其他的部分3.)可以有条件地传送数据。
机器代码对于有符号和无符号两种情况都使用一样的指令,这是因为许多的算术运算对无符号和补码算术都有一样的位级行为。
3.跳转指令极其编码
跳转指令会导致执行切换到程序中一个全新的位置。
4.翻译条件分支
将条件表达式和语句从C语言翻译成机器代码,最常用的方式是结合有条件和无条件跳转。
5.循环
大多数汇编器根据一个循环的do-while形式来产生循环代码。
6.条件传送指令
实现条件操作的传统方法是利用控制的条件转移。数据的条件转移是一种替代的策略。这种方法先计算一个条件操作的两种结果,然后再根据条件是否满足从而选取一个。
7.switch语句
选择开关语句不仅仅提高C代码的可读性,而是通过使用跳转表这种数据结构使得实现更加高效。
七、过程
一个过程调用包括将数据和控制从代码的一部分传递到另一部分。另外,它必须在进入时为过程的局部分量分配空间,并在退出时释放这些空间。数据传递和、局部变量的分配和释放通过操纵程序栈来实现。
1.栈帧结构
IA32程序用程序栈来支持过程调用。机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。为单个过程分配的那部分栈为栈帧,栈帧的最顶端以两个指针界定,寄存器%ebp为帧指针,而寄存器%esp为栈指针。当程序执行时,栈指针可以移动,因此大多数信息的访问都是相对于帧指针的。(栈用来传递参数,存储返回信息,保存寄存器,以及本地存储)
2.转移控制
CALL指令有一个目标,即指明被调用过程起始的指令地址。CALL指令的效果是将返回地址入栈,并跳转到被调用过程的起始处。RET指令从栈中弹出地址,并跳转到这个位置。
3.寄存器使用惯例
寄存器%eax,%edx和%ecx被划分为调用者保存寄存器。当过程P调用Q时,Q可以覆盖这些寄存器,而不会破坏任何P所需要的数据。另一方面,寄存器%ebx,%esi和%dei被划分为被调用者保存寄存器。根据描述的惯例,必须保持寄存器%ebp和%esp。
4.过程示例
编译器根据一组很简单的惯例来产生惯例栈结构的代码。参数在栈上传递给函数,可以从栈中用相对于%ebp的正偏移量,来访问他们。可以用push指令或者是从栈指针减去偏移量来在栈上分配空间,在返回前,函数必须将栈恢复到原始条件,可以恢复所有的被调用者保存寄存器和%ebp,并且重置%esp使其指向返回地址。为了使程序能够正确执行,让所有过程都遵循一组建立和恢复栈的一致惯例很重要。
5.递归过程
八、数组分配和访问
1.基本原则
2.指针运算
如果P是一个指向类型为T的数据的指针,P为值xp,那么表达式p+i的值为xp+L,这里L是指数据类型T的大小。
3.嵌套的数组
4.定长数组
5.变长数组
九、异质的数据结构
1.结构
2.联合
3.数据对齐
十、综合理解指针
十一、使用GDB调试器
通常的方法是在程序感兴趣的地方附近设置断点。断点可以设置在函数入口后面,或者一个程序的地址处。在断点处,可以单步追踪程序,一次只执行几条指令,或是前进到下一断点。一些命令,如 kill-停止程序,break sum-在函数sum入口设置断点。disas-反汇编当前函数。等。