计算机执行机器代码,用字节序列编码低级的的操作,包括处理数据、内存管理、读写存储设备上的数据,以及利用网络通信。GCC语言汇编器以汇编代码的形式输出,汇编代码是机器代码的文本表示,给出程序中的每一条指令。然后用GCC调用汇编器和链接器,根据汇编代码生成可执行的机器代码。
用高级语言编写的程序可以在很多不同的机器上编译和执行,而汇编代码则是与特定机器密切相关的
gcc main.c -o main
实际上gcc命令调用了一整套程序,将源码转换成可执行代码
对于机器级编程来说,有两种抽象:
一些对C语言隐藏的处理器都是可见的:
操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理内存中的物理地址。
用 “字” 表示16位数,“双字”表示32位数,“四字”称为64位数
一个x86-64的中央处理单元(CPU)包含一组16个存储64位值的通用目的寄存器用来存储整数数据和指针。
x86-64的寄存器都是64位,标号从%rax
到%rbp
,还增加了8个新寄存器,从%r8
到%r15
多数指令会有一个或多个操作数,其类型包括三种:
将数据从一个位置复制到另一个位置的指令。
源操作数指定的值是一个立即数,存储在寄存器或者内存中。目的操作数指定一个位置,可以是寄存器或者内存地址。
规定传送指令的两个操作数不能都指向内存位置
当将较小的源值复制到较大的目的时使用零扩展数据传送指令。
“指针”其实就是地址。
间接引用指针就是将该指针放入寄存器中,然后在内存引用中使用这个寄存器,就可以访问到这个指针指向的数据。
程序栈存放在内存中的某个位置,向下增长,栈顶元素的地址就是所有栈中元素最低的。其中寄存器%rsp
指向栈顶元素的地址。
【说明】
地址减8 :
64位的操作系统中,一次可以存储64/8=8字节,即4字 8字节
pushq命令 将4字压入栈中,
每个指令类都对应有四种不同大小数据的指令。
指令leaq(load effective address)命令实际上是movq指令的变形。
指令中第一个操作数是将有效地址写入到目的操作数,同时目的操作数必须是一个寄存器
上图描述的是支持产生两个64位数字的全128位乘积以及整数除法的指令。
OF:溢出标志 最近的操作导致一个补码溢出——正溢出或负溢出
leaq指令 不改变任何条件码,用来进行地址计算
以上这些指令会设置条件码。
条件码不会被直接读取,一般常使用的方法有三种:
SET指令:
一条SET指令的目的操作数是低位单字节寄存器元素之一,或者是一个字节的内存位置。指令会将这个字节设置为0或1。
jmp无条件跳转指令
其他的指令都是有条件的,根据条件码的某种组合,或者跳转,或者执行代码序列中的下一条指令。
对于if-else语句,汇编实现通常采用对then-statement和else-statement产生各自的代码块。
使用数据的条件转移,这种方法计算一个条件操作数的两种结果,然后根据条件是否满足从中选取一个。
处理器通过流水线获得高性能,在流水线中,一条指令要经过一系列的阶段,每个阶段执行所需操作的一小部分。通过重叠连续指令的步骤来获得高性能。
当机器遇到条件跳转时,只有当分支条件求值完成之后,才能决定分支的走向。处理器通过分支预测逻辑来猜测每条指令是否会执行。错误预测一个跳转处理器要求它丢掉为该跳转指令后所有的已做工作,然后从正确位置起始的指令去填充流水线。
条件传送与条件跳转不同,处理器无需预测结果就可以执行条件传送。处理器只是读取源值,检查条件码,然后更新或者保持目的寄存器不变。
v = test-expr ? then-expr : else-expr;
利用条件控制转移,可以表示为:
if(!test-expr)
goto false;
v = then-expr;
goto done;
fales:
v = else-expr;
done:
利用条件转移则可以表示为:
v = then-expr;
ve = else-expr;
t = test-expr;
if(!t)v = ve;
语句格式
do
body-statement
while(test-expr);
可以被翻译成
loop:
body-statement
t = test-expr;
if(t)
goto loop;
语句格式
while (test-expr)
body-statement
区别于do-while循环,在第一次执行body-statement
之前,会对条件进行检验,循环有可能终止,采用两种不同的翻译方法。
跳转到中间
goto test;
loop:
body-statement
test:
t = test-expr;
if(t)
goto loop;
guarded-do
首先用条件分支,如果初始条件不成立就跳过循环,把代码换成do-while循环。
t = test-expr;
if(!t)
goto done;
loop:
body-statement;
t = test-expr;
if(t)
goto loop;
done:
语句格式
for(init-expr;test-expr;update-expr)
body-statement
跳转到中间
init-expr;
goto test;
loop:
body-statement;
update-expr;
test:
t = test-expr;
if(t)
goto loop;
guarded-do策略得到
init-expr;
t = test-expr;
if(!t)
goto done;
loop:
body-statement;
update-expr;
t = test-expr;
if(t)
goto loop;
done:
跳转表是一个数组,表项i是一个代码段的地址,这个代码段实现当索引值i时程序应该采取的动作。当开关的情况比较多,值的跨度范围较小时,就会使用跳转表。
执行switch语句的关键时通过跳转表来访问代码位置。
采用间接跳转jmp *.L4(,%rsi,8)
即先找到.L4然后向后找8个字节。
x86-64过程需要的存储空间超出寄存器大小后,会在栈上分配空间,这个过程称为栈帧
以过程P调用Q,Q执行后返回P为例
P调用Q会将Q返回后下一条执行的P的代码作为P栈帧的一部分。Q则会扩展当前栈的边界,分配其栈帧所需要的空间。
过程Q执行结束返回P过程时,P的代码可以访问寄存器%rax中的返回值。
x86-64中,通过寄存器最多传递6个整型参数,寄存器的使用是有特殊顺序的,寄存器使用的名字则取决于要传递的数据类型的大小,会根据参数在参数列表中的顺序为它们分配寄存器。
寄存器组是唯一被所有过程共享的资源。我们需要确保一个过程(调用者)调用另一个过程(被调用者)时,被调用者不会覆盖调用者稍后会使用的寄存器的值。
寄存器 %rbx %rbp 和 %r12 ~ %r15被划分为被调用者保存寄存器 当过程P调用过程Q时,Q必须保存这些寄存器的值,保证在Q返回到P时不变。
所有其他的寄存器,除了栈指针%rsp都被分类为调用者保存寄存器,任何函数都可以修改它们。过程P在某个此类寄存器中有局部变量时,调用Q,Q可以随意修改这个寄存器,保存数据是P的责任
对于T D[R][C] 即一个R行C列的二维数组
L是类型为T的字节为单位的大小,则元素D[i][j]的内存地址为:
将5*3二维数组A[i][j]复制到寄存器%eax中的汇编代码为:
A-%rid i-%rsi j-%rdx
leaq (%rsi,%rsi,2),%rax
leaq (%rdi,%rax,4),%rax
movl (%rax,%rdx,4),%eax
允许以多种类型来引用一个对象。
union U3{
char c;
int i[2];
double v;
};
对于union U3 * 的指针p,p->c、p->i[10]和P->v引用的都是数据结构的起始位置。同时一个联合的大小总是等于它的最大字段的大小。
int (*fp)(int ,int *);
代表fp是一个函数指针,形参为int 和int * ,同时该函数的返回值为int类型
int * f(int *);
代表f为一个函数,形参为int * ,同时返回值为int *类型
通常在栈中分配某个字符数组来保存一个字符串,但是字符串的长度超出了数组分配的空间。超出以后,返回指针的值以及更多的保存状态会被破坏。如果存储的返回地址为破坏了,那么ret指令可能会导致程序转跳到一个意想不到的位置。
%rbp作为帧指针(栈底指针),是一个被调用者保存寄存器,只有在栈帧长可变的情况下,才会使用%rbp栈基址指针。
AVX浮点体系结构允许数据存储在16个YMM寄存器中,它们的名字为%ymm0~%ymm15。每个YMM寄存器都是256位,对标量数据操作,只保存浮点数,只使用低32位或64位,使用%xmm来引用它们。
XMM寄存器用来向函数传递浮点参数,以及从函数返回浮点值。
2018-2019-1 20189206 《深入理解计算机系统》第三章笔记
原文:https://www.cnblogs.com/zz-1226/p/10015534.html