程序的机器级表示(汇编级)
X86 寻址方式经历三代:
1 DOS时代的平坦模式,不区分用户空间和内核空间,很不安全
2 8086的分段模式
3 IA32的带保护模式的平坦模式
机器级代码
计算机系统使用了多种不同形式的抽象,利用更简单的抽象模型来隐藏实现的细节。
对于机器级编程来说,其中两种抽象尤为重要:
1、指令集体系结构(Instruction set architecture ISA)
它定义了处理器状态、指令的格式,以及每条指令对状态的影响。
IA32将程序的行为描述成好像每条指令时按顺序执行的,一条指令结束后,下一条再开始。(实际上处理器并发地执行许多指令,但是可以采取措施保证整体行为与ISA指定的顺序执行完全一致)
2、机器级程序使用的存储器地址是虚拟地址
提供的存储器模型看上去是一个非常大的字节数组。存储器系统的实际实现是将多个硬件存储器和操作系统软件组合起来。
程序存储器(program memory)包含:程序的可执行机器代码、操作系统需要的一些信息、栈、堆。程序存储器用虚拟地址来寻址(此虚拟地址不是机器级虚拟地址)。操作系统负责管理虚拟地址空间(程序级虚拟地址),将虚拟地址翻译成实际处理器存储器中的物理地址(机器级虚拟地址)。
3.2.2代码实例
int accum = 0;
int sum(int x, int y)
{
int t = x + y;
accum += t;
return t;
}
在命令行上使用-S选项,就能得到C语言编译器产生的汇编代码。同时,使得GCC运行编译器,产生一个汇编文件code.s
汇编代码包含各种声明,如果我们使用"-c"命令行选项,GCC会编译并汇编该代码。产生二进制的code.o文件。二进制文件可以用od 命令查看,也可以用gdb的x命令查看。 有些输出内容过多,我们可以使用 more或less命令结合管道查看,也可以使用输出重定向来查看。
od code.o | more od code.o > code.txt
数据格式
16位的机器体系结构,Intel用术语"字(word)"表示16位数据类型。32位机器体系结构是从16位扩展过来,因此称32位数为“双字(double words)”,称64位数为“四字(quad words)”。大部分指令都是针对字节和双字操作的。
C声明 Intel数据类型 GAS后缀 大小(字节)
char 字节(byte) b 1
short 字(word) w 2
int 双字(double words) l 4
unsigned 双字(double words) l 4
long int 双字(double words) l 4
unsigned long 双字(double words) l 4
char * 双字(double words) l 4
float 单精度(signal) s 4
double 双精度(double) l 8
long double 扩展精度 t 10/12
GAS中的每个操作指令都有一个字符后缀,用于表明操作数的大小。例如,mov有三种形式: movb(传送字节)、movw(传送字)、movl(传送双字)。其中float的后缀也是l,这不会与整数的混淆,因为浮点数使用的一组完全不同的指令和寄存器(浮点数寄存器)。
在IA32中央处理单元(CPU)中,包含了8个32位整数寄存器(如下图)。从图中可以看到在每个32位寄存器的名字前面都会有一个%e,在这里可以把e理解成extended(扩展的),因为早期的8086寄存器是16位,所以加e之后就变成32位的了。
Linux——平坦寻址方式:
ds,ss,cs等各段的段基地址都指向同一个地方,不管是数据段还是代码段,只要他们的偏移相等,那么他们就是寻址一样的物理内存,所以我们就只需指明偏移就能得到统一的寻址目标,不管这个目标是在代码段还是数据段或者堆栈段之中。
操作数:指示出执行一个操作中要引用的源数据值,以及放置结果的目标位置。
操作器的三种类型
寄存器组是被所有过程共享的资源,但同一时刻只有一个过程激活,所以需要保证被调用者不会影响调用者在寄存器中的值。
将%eax, %edx, %ecx作为调用者保存寄存器,被调用者可以覆盖这些寄存器;
将%ebx, %ebi, %edi作为被调用者保存寄存器,需要在过程开始前pushl,在过程结束后popl到寄存器。
一.存放的可能
存储器中
二.立即数寻址方式
格式:$后加用标准c表示法表示的整数,如$0xAFF
寄存器寻址方式如%eax,与汇编中学过的AX寄存器类比。
三.存储器寻址方式
直接寻址方式
寄存器间接寻址方式
寄存器相对寻址方式
基址变址寻址方式
相对基址变址寻址方式
操作数指示符包括立即数,寄存器,存储器,下图是对应的操作数的格式及对应的寻址方式。
类型 |
格式 |
操作数值 |
名称 |
立即数 |
$Imm |
Imm |
立即数寻址 |
寄存器 |
Ea |
R[Ea] |
寄存器寻址 |
存储器 |
Imm |
M[Imm] |
绝对寻址 |
存储器 |
(Ea) |
M[R[Ea]] |
间接寻址 |
存储器 |
Imm(Eb) |
M[Imm + R[Eb]] |
(基址+偏移量)寻址 |
存储器 |
(Eb, Ei) |
M[R[Eb] + R[Ei]] |
变址寻址 |
存储器 |
Imm(Eb, Ei) |
M[Imm + R[Eb] + R[Ei]] |
变址寻址 |
存储器 |
(, Ei, s) |
M[R[Ei] * s] |
比例变址寻址 |
存储器 |
Imm(, Ei, s) |
M[Imm + R[Ei] * s] |
比例变址寻址 |
存储器 |
(Eb, Ei, s) |
M[R[Eb] + R[Ei]*s] |
比例变址寻址 |
寄存器 |
Imm (Eb, Ei, s) |
M[Imm + R[Eb] + R[Ei]*s] |
比例变址寻址 |
数据传送指令:
move指令:将源操作符的值复制到目的操作数中。源操作数指定的值是一个立即数,存储在寄存器中或者存储器中。目的操作数指定一个位置,要么是一个寄存器,要么是一个存储器地址。。传送指令的链各个操作数不能都指向存储器位置。这些指令的寄存器操作数,对于movl来说,可以是8个32位寄存器(%eax~%ebp),对于movw来说,可以是8个16位寄存器(%ax~%bp),movb可以使单字节寄存器元素(%ah~%bh,%al~%bl)。
汇编中可以用条件测试和跳转组合起来实现循环的效果,但是大多数汇编器中都要先将其他形式的循环转换成do-while格式。
do body-statement while(test-expr);
循环体body-statement至少执行一次。
loop: body-statement t = test-expr; if(t) goto loop;
即先执行循环体语句,再执行判断。
while (test-expr) body-statement
GCC的方法是,使用条件分支,表示省略循环体的第一次执行:
if(!test-expr) goto done; do body-statement while(test-expr); done:
than
t = test-expr; if(!t) goto done: loop: body-statement t = test-expr; if(t) goto loop; done:
switch语句
switch语句可以根据一个整数索引值进行多重分支。处理具有多种可能结果的测试时,这种语句特别有用。它们不仅提高了代码的可读性,而且使用跳转表这个数据结构使用实现更加高效。跳转表是一个数组,表项i是一个代码段的地址,这个代码段实现当switch索引值等于i时程序应该执行的动作。程序代码用于索引值来执行一个跳转表内的数组引用,确定跳转指令的目标。和使用一组很长的
if-else相比,使用跳转表的优点是执行switch语句的时间与switch的case数量无关。GCC根据switch语句中case的数量和case中值的稀少程序来翻译开关语句。当case数据比较多(例如4个以上),并且值的范围跨度比较小时,就会使用跳转表。
栈由高地址向低地址方向增长。对单个过程分配的栈称为 栈帧。以两个指针来界定:帧指针%ebp和栈指针%esp.栈指针是不断变化的,所以大多数信息基于帧指针%ebp.(注意在我的电脑上,帧指针是%esp,所以在汇编时总是由 movl 8(%esp) %eax来得到参数)。
家庭作业
3.64
A.从word_sum代码的第5~7行从栈中的3个值分别是result的返回地址,s1.p,s2.p。
原因:从word_sum汇编代码第9行可知第6、7行的代码是从栈中调用s1.p,s2.p。由第11行代码及题目中的提示可知ret $4使栈指针增加8所以第5行代码从栈中调用的是result的返回地址。
B.栈帧中分配的5个字段分别用于存储以下值,如下图所示。
s2.sum |
s2.prod |
s1.v |
s1.p |
&s2(word_sum的返回地址) |
-4 %ebp
-8
-12
-16
-20 %esp
原因:从第6,7行可以看出s1.p的位置是%esp+4即-16的位置。从第8,9行可看出s1.v的位置是%esp+8的位置即-12的位置,从第10行可知%esp(-20)为位置为调用word_sum函数的返回地址即&s2。从第13行可知s2.prod的位置为%ebp-8即-8的位置。从第14行可知s2.sum的位置为%esp-4即-4的位置。
C.向函数传递结构体参数的通用策略:结构体的每一个变量可以看做是单独的参数进行传入。
D.从函数返回结构体值的通用策略:将返回变量的地址看做第一个参数传入函数。而不是在函数中分配栈空间给一个临时变量,因为%eax存不下一个结构体,所以%eax充当返回变量的指针的角色。、
3.65
由结构体str2的C代码和题中汇编代码第一行知4+B=12,所以由数据对齐知,B在栈中被分配的空间为8。
由str2的C代码知8+4+2B=28,所以由数据对齐知B可以是8或者7。
再由结构体str1的C代码和题中的汇编代码最后一行知2*A*B=44。
根据数据对齐的原理可求得A=3,B=7 。