第三章 程序的机器级表示
3.1 历史观点
- X86 寻址方式经历三代:
1 DOS时代的平坦模式,不区分用户空间和内核空间,很不安全
2 8086的分段模式
3 IA32的带保护模式的平坦模式
3.2 程序编码
gcc -01 -o p p1.c
- -01 表示使用第一级优化。优化的级别与编译时间和最终产生代码的形式都有关系,一般认为第二级优化-02 是较好的选择。
- -o 表示将p1.c编译后的可执行文件命名为p
- 计算机系统使用了多种不同形式的抽象,对于机器级编程来说,两种抽象尤为重要。第一种是机器级程序的格式和行为,定义为指令集体系结构(ISA),他定义了处理器状态、指令的格式,以及每条指令对状态的影响。
- 程序计数器(CS:IP)
- 整数寄存器(AX,BX,CX,DX)
- 条件码寄存器(OF,SF,ZF,AF,PF,CF)
- 浮点寄存器
3.2.1机器级代码
几个处理器:
3.2.2代码示例
int accum = 0;int sum(int x, int y){ int t = x + y; accum += t; return t;}
在命令行上使用“-S”选项,就能得到C语言编译器产生的汇编代码:
unix> gcc -01 -S code.c
这会使GCC运行编译器产生一个汇编文件code.s,但不做其他进一步工作
如果在命令行上使用“-C”选项,GCC会编译并汇编该代码:
unix> gcc -01 -c code.c
这就会产生目标代码文件code.o,他是二进制格式,无法直接查看。
机器代码和它的反汇编表示的一些特性需要注意:
- IA32指令长度从1到15个字节不等
- 设计指令格式的方式是,从某个给定位置开始,可以将字节唯一的解码成机器指令。
- 反汇编器只是基于机器代码文件中的字节序列来确定汇编代码,不需要访问程序的源代码或汇编代码。
- 反汇编器使用的指令命名规则与GCC生成的汇编代码使用的有些差别。
3.3 数据格式
1.Intel中:
8 位:字节16位:字32位:双字64位:四字
2.c语言基本数据类型对应的IA32表示
char 字节 1字节short 字 2字节int 双字 4字节long int 双字 4字节long long int (不支持) 4字节char * 双字 4字节float 单精度 4字节double 双精度 8字节long double 扩展精度 10/12字节
3.数据传送指令的三个变种:
- movb 传送字节
- movw 传送字
- movl 传送双字
3.4 访问信息
IA32的cpu中有8个32位的寄存器,%e是前缀,依次是:ax,cx,dx,bx,si,di,sp,bp。前6个可以看作是通用寄存器,大多数情况下。前3个和后3个的保存和恢复惯例不同。最后两个指向程序栈中重要位置的指针。
指令的源数据值可以以常数形式给出,或是从寄存器或存储器中读出。也就是,常数,寄存器,内存。
IA32的一条限制:数据传送指令的两个操作数不能都指向存储器位置。
数据传送指令的源操作数在左,目的操作数在右(ATT风格),(Intel风格则相反)。
栈在程序的虚拟地址空间的上部,再往上就是内核虚拟空间了,栈底紧挨着内核虚拟空间,栈顶向下增长。%esp保存这栈顶元素的地址。
将一个双字压入栈,先将%esp减小4,然后将双字放入这多出来的4个字节的空间中。出栈则是先读出4个字节,然后%esp加4。
因为栈和程序代码以及其他形式的程序数据都是放在同样的存储器中(虚拟地址空间),所以程序用标准的存储器寻址方法访问栈内任意位置。
3.5 算术和逻辑操作
加载有效地址地址指令,leal,只是将源操作数计算出来的地址交给目的操作数,貌似:源操作数是存储器访问格式的,目的操作数是寄存器。
一元操作:incb/w/l,decb/w/l,negb/w/l,notb/w/l,前面两个好理解,第三个是取负,这里用到了一个概念,取负是指2变成-2,-3变成3,可以认为,就将后面的操作数认为是补码编码的了,然后如果本来是2的补码,现在就要编程-2的补码,也就是进行0-2的运算。第四个是取反。
二元操作:addb/w/l,subb/w/l,imulb/w/l,xorb/w/l,orb/w/l,andb/w/l。值得注意的是:第3个,乘是先数学上的乘,然后截断一下。这是2章中得到的结论,至少是对于imulb/w/l3者。后3个好理解,异或,或,并。
移位:salb/w/l,shlb/w/l,sarb/w/l,shrb/w/l。向左left,向右right移位。向左总是补0的,向右就是算术移位和逻辑移位了。移位量是单个字节的编码,因为只允许0到31位的移位(只考虑移位量的低五位)。移位量只可以是一个立即数或者单字节寄存器元素%cl。
3.6控制
到目前为止,我们只考虑了直线代码的行为,也就是指令一条接着一条顺序的执行。机器代码提供两种基本的低级低制来实现有条件的行为:测试数据值,然后根据测试的结果来改变控制流或者数据流。
用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代码的可读性,而是通过使用跳转表这种数据结构使得实现更加高效。
3.7过程
一个过程调用包括将数据和控制从代码的一部分传递到另一部分。另外,它必须在进入时为过程的局部分量分配空间,并在退出时释放这些空间。数据传递和、局部变量的分配和释放通过操纵程序栈来实现。
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使其指向返回地址。为了使程序能够正确执行,让所有过程都遵循一组建立和恢复栈的一致惯例很重要。
参考文件:百度百科
20135316王剑桥的博客地址:http://www.cnblogs.com/20135316wjq/