第三章 程序的机器级表示
3.1历史观点
一、GCC C语言编译器以汇编代码的形式产生输出,汇编代码是机器代码的文本表示,给出程序中的每一条指令。
二、X86 寻址方式经历三代:
1 DOS时代的平坦模式,不区分用户空间和内核空间,很不安全
2 8086的分段模式
3 IA32的带保护模式的平坦模式
3.2程序编码
一、gcc -01 -o p p1.c p2.c
-01 表示编辑器使用第一级优化。一般认为第二级优化-02是较好的选择。
二、机器级编程的两种重要抽象:
1、机器级程序的格式和行为,定义为指令集体系结构(ISA),它定义了处理器状态、指令的格式、以及每条指令对状态的影响。将程序的行为描述成好像每条指令是按顺序执行的。
2、机器级程序使用的存储器地址是虚拟地址,提供的存储器模型看上去是一个非常大的字节数组。存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。
三、
gcc -S xxx.c -o xxx.s 获得汇编代码(这里不做进一步工作),gcc -c xxx.c -o xxx.o 产生目标文件,二进制格式。也可以用objdump -d xxx.o 反汇编;
注意:
64位机器上想要得到
32代码:gcc -m32 -S xxx
.c
MAC OS中没有objdump, 有个基本等价的命令otool
四、二进制文件可以用od 命令查看,也可以用gdb的x命令查看。 有些输出内容过多,我们可以使用 more或less命令结合管道查看,也可以使用输出重定向来查看。
od code.o | more
od code.o > code.txt
五、Linux和Windows的汇编格式的区别
1
Intel代码省略了指示大小的后缀
2
Intel代码省略了寄存器名字前面的‘
%’符号
3
Intel代码用不同的方式来描述存储器中位置
4 在带有多个操作数的指令情况下,列出操作数的顺序相反
3.3数据格式
一、Intel
8 位:字节
16位:字
32位:双字
64位:四字
二、c语言基本数据类型对应的IA32表示:大多数常用数据类型都是以双字形式存储的。
浮点数有三种形式:单精度(4字节)float
双精度(8字节)double
扩展精度(10字节)long double
三、数据传送指令的三个变种:movb 传送字节
movw 传送字
movl 传送双字
3.4访问信息
一、一个 IA32中央处理单元CPU包含一组8个存储32位值的寄存器,用来存储整数数据和指针
二、%esi
%edi可以用来操纵数组,
%esp
%ebp用来操纵栈帧。
三、操作数的三种类型:
1、立即数:常数值
2、寄存器:某个寄存器的内容
3、存储器:根据计算出来的地址(通常称为有效地址)访问某个存储器位置
四、有效地址的计算方式 Imm(Eb,Ei,s) = Imm + R[Eb] + R[Ei]*s
五、注意不能从内存地址直接
MOV到另一个内存地址,要用寄存器中转一下。
六、MOVS和MOVZ指令类都是将一个较小的源数据复制到一个较大的数据位置,高位用符号位扩展(MOVS)或者零扩展(MOVZ)进行填充。
七、堆栈遵循“先出后进”的原则。栈顶元素的地址是所有栈中元素地址中最低的。
八、c语言中的指针其实就是地址,间接引用指针就是将该指针放在一个寄存器中,然后在存储器引用中使用这个寄存器
九、局部变量通常保存在寄存器中,而不是存储器中。寄存器访问比存储器访问要快得多。
3.5算术和逻辑操作
一、四组操作:
加载有效地址:
加载有效地址指令leal实际上是movl指令的变形。第一个操作数并不是从指定的位置读入数据,实际是将有效地址写入目的操作数,目的操作数必须是寄存器。
一元操作:只有一个操作数,可以是寄存器也可是存储器位置。
二元操作:源操作数是第一个,可以是立即数、寄存器、存储器。目的操作数是第二个,可以是寄存器、存储器。两个不能同时为存储器。
移位:第一个是移位量,用单个字节编码(只允许
0-
31位的移位),可以是立即数或者放在单字节寄存器%
cl中。左移指令SAL,SHL两者的效果是一样的,都是将右边填上0。SAR执行算术移位(填上符号位),SHR执行逻辑移位(填上
0)。移位操作的目的操作数可以是一个寄存器或是一个存储器位置。
二、特殊的算术操作
1、imull指令称为“双操作数”,从两个32位操作数产生一个32位的乘积。
IA32还提供了两个不同的“单操作数”乘法指令,以计算两个32位值的全64位乘积。无符号数乘法(mull)、补码乘法(imull)。这两条指令都要求一个参数必须在寄存 器%eax中,另一个作为指令的源操作数给出。乘积的高32位存放在寄存器%edx中,低32位存放在寄存器%eax中。
2、有符号除法指令idivl将寄存器%edx(高32位)和%eax(低32位)中的64位数作为被除数,而除数作为指令的操作数给出,指令将商存储在寄存器%eax中,将余数存储在寄存器%edx中。
3.6控制
一、控制中最核心的是跳转语句:
有条件跳转(实现
if,
switch,
while,
for)
无条件跳转jmp(实现
goto)
二、条件码寄存器:描述了最近的算术或逻辑操作的属性,可以检测这些寄存器来执行条件分支指令。常用条件码有:
CF:进位标志
ZF:零标志
SF:符号标志
OF:溢出标志
三、leal不改变任何条件码。
访问条件码:
1、根据条件码的某个组合,将一个字节设置为
0或
1。SET指令根据t=
a-b的结果设置条件码
2、可以条件跳转到程序的某个其他部分
3、可以有条件的传送数据
四、跳转指令:
无条件跳转:
JMP 可以是直接跳转也可以是间接跳转(写法是*后面加操作数指示符)
有条件跳转:根据条件码的某个组合,或者跳转或者继续执行下一条指令。
五、循环
汇编中可以用条件测试和跳转组合起来实现循环的效果,但是大多数汇编器中都要先将其他形式的循环转换成do-while格式。
1.do-while循环
do-while语句的通用形式:
do
body-statement
while(test-expr);
可以翻译成:
loop:
body-statement
t = test-expr;
if(t)
goto loop;
2.while循环
While语句的通用形式:
while (test-expr)
body-statement
GCC的方法是,使用条件分支,表示省略循环体的第一次执行,从而将代码转换成do-while循环
if(!test-expr)
goto done;
do
body-statement
while(test-expr);
done:
翻译成goto代码:
t = test-expr;
if(!t)
goto done:
loop:
body-statement
t = test-expr;
if(t)
goto loop;
done:
3.for循环
for循环的通用形式:
for(init-expr;test-expr;update-expr)
body-statement
与下面这段使用while循环代码的行为一样
init-expr;
while(test-expr){
body-statement
update-expr;
}
六、条件传送指令:实现条件操作的传统方法是利用控制的条件转移。
七、Switch语句:根据一个整数索引值进行多重分支。通过使用跳转表这种数据结构实现更加高效。跳转表是一个数组,表项i是一个代码段的地址,这个代码段实现当开关索引值 为i时程序该做的。
3.7过程
一、过程调用包括将数据和控制从代码的一部分传递到另一部分,需要在进入时为过程的局部变量分配空间并在退出时释放空间,这通过程序栈实现。
二、栈用来传递参数、存储返回信息、保存寄存器,以及本地存储。
三、栈帧:为单个过程分配的那部分栈。
四、最顶端的栈帧以两个指针界定,寄存器%ebp为帧指针,寄存器%esp为栈指针。程序执行时,栈指针可以移动,大多数信息的访问都是相对于帧指针的。
五、转移控制
1、call指令有一个目标,即指明被调用过程起始的指令地址,效果是将返回地址入栈,并跳转到被调用过程的起始处。
2、ret指令从栈中弹出地址,并跳转到这个位置,使用这个指令栈指针要指向call指令存储返回地址的位置。
六、程序寄存器组是唯一能被所有过程共享的资源。
根据惯例寄存器%eax,%edx,%ecx被划分为调用者保存寄存器。
%ebx,%esi,%edi被划分为被调用者保存寄存器。
%ebp,%esp 惯例保持
%eax用来保存返回值
七、关于栈帧的gdb命令:backtrace/bt/frame/up/down