第三章 程序的机器级表示
一、历史观点
Intel处理器(X86)
二、程序编码
gcc -01 -o p p1.c p2.c
①编译选项-01 表示编译器使用第一级优化
②编译选项-02 表示编译器使用第二级优化(较好的选择)
③-o 表示分别将p1.c和p2.c编译后的可执行文件命名为p
GCC将源代码转化为可执行代码的步骤:
C预处理器:扩展源代码,插入所有#include命令指定的文件,并扩展生成.i文件
编译器:产生两个源代码的汇编代码,生成.s文件
汇编器:将汇编代码转化成二进制目标代码——生成.o文件
链接器:产生可执行代码文件p
回顾:gcc命令编译运行C语言
-
预处理阶段:将*.c文件转化为*.i预处理过的C程序。
-
编译阶段:将*.i文件编译为汇编代码*.s文件。
-
汇编阶段:将*.s文件转化为*.o的二进制目标代码文件。
-
链接阶段:将*.o文件转化为可执行文件。
-
生成可执行文件:将*.o转换为可执行文件。
-
执行可执行C语言文件。
(1)机器级代码
两种抽象
(1)指令集体系结构ISA
(2)机器级程序使用的存储器地址是虚拟地址(多个硬件存储器和操作系统软件组合起来)
处理器状态:
- 程序计数器(CS:IP)
- 整数寄存器(AX,BX,CX,DX)
- 条件码寄存器(OF,SF,ZF,AF,PF,CF)
- 浮点寄存器
(2)代码示例
C语言代码文件code.c:
int accum = 0;
int sum(int x, int y)
{
int t = x + y;
accum += t;
return t;
}
产生汇编代码code.s:
gcc -01 -S code.c
编译并汇编该代码,产生目标代码code.o:gcc -01 -c code.c
反汇编器——查看目标代码文件
abjdump -d code.o
三、数据格式
数据类型:
字:16位
双字:32位
四位:64位
基本数据类型:
C 声明 | Intel | 汇编代码后缀 | 大小(字节) |
char | 字节 | b | 1 |
short | 字 | w | 2 |
int | 双字 | l | 4 |
long int | 双字 | l | 4 |
long long int | — | — | 4 |
char * | 双字 | l | 4 |
float | 单精度 | s | 4 |
double | 双精度 | l | 8 |
long double | 扩展精度 | t | 10/12 |
数据传送指令
movb 传送字节
movw 传送字
movl 传送双字
四、访问信息
(一)操作数指示符
操作数:指示出执行一个操作中要引用的源数据值,以及放置结果的目标位置。
操作数类型:
- 立即数 (常数值) 例:$0x1F
- 寄存器 例:%ax
- 存储器 有效地址
寻址方式:
(1)立即数寻址方式
(2)寄存器寻址方式
(3)存储器寻址方式
(4)直接寻址方式
(5)寄存器间接寻址方式
(6)寄存器相对寻址方式
(7)基址变址寻址方式
(8)相对基址变址寻址方式
(二)数据传送指令
1.mov指令: 把源操作数的值复制到目的操作数上
movb、movw、movl
2.堆栈
- 遵循“后进先出”的原则
- 栈指针指向栈顶元素
- 栈顶元素的地址在栈中元素地址是最低的
压栈push:将数据压入栈中
出栈pop:弹出数据
3.数据传送示例
c语言中的指针其实就是地址,间接引用指针就是将该指针放在一个寄存器中,然后在存储器引用中使用这个寄存器
局部变量通常保存在寄存器
(五)算术和逻辑操作
字节加法:addb
字加法;addw
双字加法:addl
1、加载有效地址
加载有效地址指令——leal,是movl指令的变形,对比汇编中的LEA指令学习。
将有效地址写入到目的操作数,目的操作数必须是寄存器
2、一元操作和二元操作
1.一元操作
只有一个操作数,既是源又是目的,可以是一个寄存器,或者存储器位置。
例:加法运算符(++)和减1运算符
2.二元操作
第二个数既是源又是目的
第一个操作数可以是立即数、寄存器或者存储器位置
第二个操作数可以是寄存器或者存储器位置
两个操作数但是不能同时是存储器位置。
三、移位操作
右边填0:SAL 算术左移
SHL 逻辑左移
填上符号位:
SAR 算术右移
填上0:SHR 逻辑右移
移位操作源操作数可以是立即数或CL
移位操作目的操作数可以是寄存器或存储器
4、特殊的算术操作
(六) 控制
一、条件码
CF:进位标志
ZF:零标志
SF:符号标志
OF:溢出标志
注意:
- leal不改变条件码寄存器
- CMP与SUB的区别:CMP也是根据两个操作数之差设置条件码,但只设置条件码而不更新目标寄存器
- 有条件跳转的条件看状态寄存器(教材上叫条件码寄存器)
常用指令:
MOV 不影响标志位
PUSH POP 不影响标志位
XCHG 交换指令 不影响标志位
XLAT 换码指令 不影响标志位
LEA 有效地址送寄存器指令 不影响标志位
PUSHF 标志进栈指令 不影响标志位
POPF 标志出栈指令 标志位由装入值决定
2、访问条件码
- 根据条件码的某个组合,将一个字节设置成0或1;
- 跳转到程序某个其他的部分
- 有条件的传送数据。
SET指令根据t=a-b的结果设置条件码
SET指令:
3、跳转指令及其编码
JUMP指令会导致执行切换到程序一个全新的位置,通常用一个标号指明
jmp指令是无条件跳转,可分为直接跳转和间接跳转:
直接跳转:后面跟标号作为跳转目标
间接跳转:*后面跟一个操作数指示
区别:
jmp *%eax 用寄存器%eax中的值作为跳转目标
jmp *(%eax) 以%eax中的值作为读地址,从储存器中读取跳转目标
控制中最核心的是跳转语句:
-
有条件跳转(实现if,switch,while,for)
-
无条件跳转jmp(实现goto)
当执行PC相关的寻址时,程序计数器的值是跳转指令后面那条指令的地址,而不是跳转指令本身的地址。
4、翻译条件分支
将条件表达式和语句从c语言翻译成机器语言,最常用的方式就是结合有条件和无条件跳转。
无条件跳转:例如goto。书上的例子就是把if-else语句翻译成了goto形式,然后再由这个形式翻译成汇编语言
C语言中if-else语句的通用形式:
if(test-expr)
then-statement
else
else-statement
汇编结构:
t=test-expr;
if!(t)
goto false;
then-statement
goto done;
false:
else-statement
done:
5、循环
汇编中可以用条件测试和跳转组合起来实现循环的效果,但是大多数汇编器中都要先将其他形式的循环转换成do-while格式。
1.do-while循环
通用形式:
do
body-statement
while(test-expr);
循环体body-statement至少执行一次。
可以翻译成如下所示的条件和goto语句:
loop:
body-statement
t = test-expr;
if(t)
goto loop;
即先执行循环体语句,再执行测试表达式。
2.while循环
C语言中while语句的通用形式:
while(test-expr)
body-statement
汇编结构:
t=test-expr;
if(!t)
goto done;
loop:
body-statement
t=test-expr;
if(t)
goto loop;
done:
for循环
C语言中for语句的通用形式:
for(init-expr;test-expr;update-expr)
body-statement
汇编结构:
init-expr
t=test-expr;
if(!t)
goto done;
loop:
body-statement
update-expr;
t=test-expr;
if(t)
goto loop;
done:
3.for循环
for循环
C语言中for语句的通用形式:
for(init-expr;test-expr;update-expr)
body-statement
汇编结构:
init-expr
t=test-expr;
if(!t)
goto done;
loop:
body-statement
update-expr;
t=test-expr;
if(t)
goto loop;
done:
6、条件传送指令(详细看书本)
7、Switch语句
Switch语句是多重分支的典型(这个已经掌握的比较好,详细见书本)
(七) 过程
栈帧结构
-
为单个过程分配的栈叫做栈帧,寄存器%ebp为帧指针,而寄存器指针%esp为栈指针,程序执行时栈指针移动,大多数信息的访问都是相对于帧指针。
-
栈向低地址方向增长,而栈指针%esp指向栈顶元素。
转移控制
call:目标是指明被调用过程起始的指令地址,效果是将返回地址入栈,并跳转到被调用过程的起始处。
ret:从栈中弹出地址,并跳转到这个位置。
函数返回值存在%eax中
1.call
call指令和转移指令相似,同样分直接和间接,直接调用的目标是标号,间接调用的目标是*后面跟一个操作数指示符,和JMP一样。
CALL指令的效果是将返回地址入栈,并跳转到被调用过程的起始处。返回地址是还在程序中紧跟在call后面的那条指令的地址。
然后就会用到ret了。
2.ret
ret指从栈中弹出地址,并跳转到这个位置。
在上学期的汇编语言学习中,call和ret常被用来进行子函数、子模块的调用。
3.leave
这个指令可以使栈做好返回的准备,等价于:
movl %ebp,%esp
popl %ebp
查看函数调用栈信息的GDB命令
Ÿ backtrace/bt n
n是一个正整数,表示只打印栈顶上n层的栈信息。
-n表一个负整数,表示只打印栈底下n层的栈信息。
Ÿ frame n
n是一个从0开始的整数,是栈中的层编号。这个指令的意思是移动到n指定的栈帧中去,并打印选中的栈的信息。如果没有n,则打印当前帧的信息。
Ÿ up n
表示向栈的上面移动n层,可以不打n,表示向上移动一层。
Ÿ down n
表示向栈的下面移动n层,可以不打n,表示向下移动一层。
寄存器使用惯例
寄存器%eax、%edx和%ecx被划分为调用者保存寄存器。
寄存器%ebx、%esi、%edi被划分为被调用者保存寄存器。
过程示例
GCC坚持一个函数使用的所有栈空间必须是16字节的整数倍,包括保存%ebp值的4个字节和返回值的4个字节。
递归过程
递归调用自身和调用其它函数是一样的。栈规则提供了一种机制,每次函数调用都有它自己私有的状态信息存储(保存的返回位置,栈指针和被调用者保存寄存器的值)
补充汇编语言命令:
算术指令
ADD 加法指令 影响标志位
ADC 带进位加法指令 影响标志位
INC 加一指令 不影响CF,影响别的标志位
SUB 减法指令 影响标志位
SBB 带借位减法指令 影响标志位
DEC 减一指令 不影响CF,影响其他标志位
NEG 求补指令 影响标志位 只有操作数为0,例如字运算对-128求补,OF=1,其他时候OF=0
CMP 比较指令 做减法运算但不存储结果,根据结果设置条件标志位
MUL 无符号数乘法指令
IMUL 有符号数乘法指令 均对CF和OF位以外的条件码位无定义(即状态不定)
DIV 无符号数除法指令
IDIV 带符号数除法指令 除法指令对所有条件码位均无定义
位操作指令:
AND 逻辑与
OR 逻辑或
NOT 逻辑非 不影响标志位
XOR 异或
TEST 测试指令 除NOT外的四种,置CF、OF为0,AF无定义,SF,ZF,PF根据运算结果设置
移位指令:
SHL 逻辑左移指令
SHR 逻辑右移指令 移位指令根据结果设置SF,ZF,PF位
ROL 循环左移指令
ROR 循环右移指令 循环移位指令不影响除CF,OF之外的其他条件位
串处理指令:
MOVS 串传送指令
STOS 存入串指令
LODS 从串取指令 均不影响条件位
CMPS 串比较指令
SCAS 串扫描指令 均不保存结果,只根据结果设置条件码
控制转移指令:
JMP 无条件转移指令 不影响条件码
参考资料:20135202闫佳欣的《第三章 程序的机器级表示》总结