第三章 程序的机器级表示
X86 寻址方式经历三代:
1 DOS时代的平坦模式,不区分用户空间和内核空间,很不安全
2 8086的分段模式
3 IA32的带保护模式的平坦模式
GCC将源代码转化为可执行代码的步骤:
C预处理器——扩展源代码-生成.i文件
编译器——产生两个源代码的汇编代码-——生成.s文件
汇编器——将汇编代码转化成二进制目标代码——生成.o文件
链接器——产生可执行代码文件
ISA的定义:
(1)指令集结构ISA
是机器级程序的格式和行为,定义了处理器状态、指令的格式,以及每条指令对状态的影响。
(2)机器级程序使用的存储器地址是虚拟地址
看上去是一个非常大的字节数组,实际上是将多个硬件存储器和操作系统软件组合起来。
机器级代码
ISA:机器级程序的格式和行为,定义为指令集体系机构,它定义了处理器状态指令的格式,以及每条指令对状态的影响。
机器级程序使用的存储器地址是虚拟地址,提供的存储器模型看上去是一个非常大的字符数组。
PC:程序计数器。在IA32中,用%eip表示,指示将要执行的下一条指令在存储器中的地址。
程序存储器:包含程序的可执行机器代码,操作系统需要的一些信息,用来管理过程调用和返回的运行时栈,以及用户分配的存储器块。
代码示例
64位机上想得到32位代码:gcc -m32 -S xxx.c
编译并产生汇编目标文件xxx.o:gcc -O1 -c xxx.c
获得汇编代码:gcc -S xxx.c -o xxx.s
Ubuntu中获得汇编代码:gcc -S xxx.c更接近教材
教材中获得汇编代码:gcc -O1 -S xxx.c(编译器使用的事第一级优化)
汇编代码中需要牢记以下几句:
pushl %ebp
movl %esp,%ebp
……
popl %ebp
ret
二进制文件可用od命令查看,也可以用gdb的x查看:(gdb)x/17xb sum表示检查17个十六进制的字节;
显示代码过多或过少可用more、less结合管道查看,也可以用输出重定向:
od xxx.o | more
od xxx.o > xxx.txt
反汇编:objdump -d xxx.o
MAC OS中没有objdump,可用otool代替;
汇编代码中另类结尾有'l'指的是大小指示符。
注意: 64位机器上想要得到32代码:gcc -m32 -S xxx.c
MAC OS中没有objdump, 有个基本等价的命令otool
Ubuntu中 gcc -S code.c (不带-O1) 产生的代码更接近教材中代码(删除"."开头的语句)
访问信息
IA32的整数寄存器:
其中,esi、edi可以用来操纵数组,esp、ebp用来操纵栈帧。
通用寄存器中的eax,ebx,ecx,edx中,32位的eax,16位的ax,8位的ah,al都是独立的。例如:
假定当前是32位x86机器,eax寄存器的值为0x8226,执行完addw (0x8266, %ax指令后eax的值是多少?
解析:0x8226+0x826=0x1044c, ax是16位寄存器,出现溢出,最高位的1会丢掉,剩下0x44c,不要以为eax是32位的不会发生溢出。
一、操作数指示符
操作数:指示出执行一个操作中要引用的源数据值,以及放置结果的目标位置。
1.操作数的三种类型
立即数
寄存器
存储器
2.结果存放的两种可能
寄存器中
存储器中
3.寻址方式
(1)立即数寻址方式
格式:)后加用标准c表示法表示的整数,如$0xAFF
(2)寄存器寻址方式
如%eax,与汇编中学过的AX寄存器类比。
(3)存储器寻址方式
直接寻址方式
寄存器间接寻址方式
寄存器相对寻址方式
基址变址寻址方式
相对基址变址寻址方式
二、数据传送指令
1.mov指令
(1)功能
把一个字节(字)操作数从源SRC传送至目的地DST
(2)格式
MOV DST,SRC
(3)操作数
MOV reg/mem, imm ;立即数寄存器或存储器
MOV reg/mem/seg, reg ;寄存器的值寄存器/内存/段寄存器
MOV reg/seg, mem ;内存单元的值寄存器/段寄存器
MOV reg/mem, seg ;段寄存器的值寄存器/内存单元
IA32的限制:两个操作数都不能指向存储器。
(4)变种
movb 传送字节
movw 传送字
movl 传送双字
movs 符号位扩展
movz 零扩展
2.push&pop
(1)堆栈
需要注意两点:
1.后进先出
2.栈指针指向栈顶元素
3.栈朝低地址方向增长
(2)压栈push
指令格式——PUSH r16/m16/seg
指令功能
第一步:SP←SP-2 ;堆栈指针SP上移
第二步:(SS):(SP)←r16/m16/seg ;字操作数存入堆栈顶部
注意 堆栈操作必须至少以字为单位,这时栈顶指针-2
如果压入的是双字,栈顶指针-4
(3)出栈pop
指令格式——POP r16/m16/seg
指令功能
第一步:r16/m16/seg← (SS):(SP) ;栈顶的一个字传送到指定的目的操作数
第二步:SP←SP+2 ;堆栈指针SP下移,指向新的栈顶
栈顶指针变化同压栈。
数据传送示例
指针是地址。
局部变量通常是保存在寄存器中,而不是存储器中。寄存器访问比存储器访问要快得多。
算数和逻辑操作
第一类:目的操作数必须是一个寄存器
第二类:一元操作。操作数既是源又是目的。可以是寄存器也可以是存储器。
第三类:二元操作。第二个操作数既是源又是目的。但两个操作数不能同时是存储器。
第四类:移位操作。位移量是一个立即数或放在单字节寄存器%cl中。移位操作的目的操作数可以是一个寄存器或是一个存储器位置。
更详细一点就是;
SAL 算术左移
SHL 逻辑左移
SAR 算术右移(补符号位)
SHR 逻辑右移(补0)
源操作数:移位量——立即数或CL
目的操作数:要移位的数值——寄存器或存储器
特殊操作:
1.乘法
1)乘积截断
imull
双操作数,从两个32位操作数产生一个32位的乘积。
(2)乘积不截断
mull,无符号数乘法
imull,有符号数乘法
都要求一个参数必须在寄存器%eax中,另一个作为指令的源操作数给出。乘积的高32位在%edx中,低32位在%eax中。
2.除法
(1)有符号除法
idivl 操作数
将DX:AX中的64位数作为被除数,操作数中为除数,结果商在AX中,余数在DX中。
(2)无符号除法
divl指令
通常会事先设定寄存器%edx为0.
控制
机器代码提供两种基本的低级机制来实现有条件的行为:测试数据值,然后根据测试的结果来改变控制流或者数据流。
控制部分运用分支、循环语句实现。最核心的是跳转语句、有条件跳转、无条件跳转。
条件码(状态寄存器):描述最近的算术或逻辑操作的属性。CF、ZF、SF、OF.
leal指令不改变任何条件吗,因为是用来进行地址计算的。
有两类指令只设置条件码而不改变任何其他寄存器。CMP指令根据他们的两个操作数之差来设置条件码。除了至设置条件码而不更新目标寄存器之外。CMP与SUB行为是一样的。
CF:进位标志。检测无符号溢出。
ZF:零标志。最近操作结果为0。
SF:符号标志。最近操作结果为负数。
OF:溢出标志。最近操作有补码溢出——正/负溢出。
Leal进行地址运算所以不改变条件码。
访问条件码
条件码通常不会直接读取,常用三种方法:
1.根据条件码的某个组合,将一个字节设置为0或者1
2.条件跳转到程序的某个其他部分
3.有条件的传送数据
跳转指令及其编码
跳转指令会导致执行切换到程序中一个全新的位置。
在汇编代码中,这些跳转的目的地通常用一个标号指明。
在产生目标代码文件时,汇编器会确定所有带标号指令的地址,并将跳转目标(目的指令的地址)编码为跳转指令的一部分。
jmp指令是无条件跳转,他可以直接跳转也可以间接跳转。
当执行PC有关寻址时,程序计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址。
翻译条件分支
有条件和无条件的跳转。
Goto语句,无条件跳转。使代码难以阅读和调试。
循环
1.do-while
2.while
3.for
条件传送代码
实现条件操作的传统方式是利用控制的条件转移。
数据的条件转移是一种替代策略。
switch语句
switch(开关)语句可以根据一个整数索引值进行多重分支。
执行switch语句的关键步骤是通过跳转表来访问代码位置。
过程
一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部分。
栈帧结构
机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复、本地存储。
为单个过程分配的那部分栈成为栈帧。
寄存器%ebp为帧指针
寄存器%esp为栈指针
栈帧结构
机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复、本地存储。
为单个过程分配的那部分栈成为栈帧。
寄存器%ebp为帧指针
寄存器%esp为栈指针
转移控制
Call指令有一个目标:指明被调用过程起始的指令地址。
call指令效果:将返回地址入栈,并跳转到被调用过程的起始处
寄存器使用惯例
程序寄存器组是唯一能被所有过程共享的资源。
必须保证当一个过程调用另一个过程时,被调用者不会覆盖某个调用者稍后会使用的寄存器的值。
递归过程
每个调用在栈中都有自己的私有空间,多个未完成调用的局部变量不会相互影响。
当过程被调用时分配局部存储,当返回时释放内存。
使代码继续到完成部分,回复栈和被调用者保存寄存器,然后返回的两种情况:终止条件,递归调用