2018-2019-1 20189221 《深入理解计算机系统》第 3 周学习总结

2018-2019-1 20189221 《深入理解计算机系统》第 3 周学习总结

第 3 章 程序的机器级表示

计算机执行机器代码,用字节序列编码低级的操作,包括处理数据、管理内存、读写存储设备上的数据,以及利用网络通信。编译器基于编程语言的规则、目标机器的指令集和操作系统遵循的惯例,经过一系列的阶段生成机器代码o GCCC语言编译器以汇编代码的形式产生输出,汇编代码是机器代码的文本表示,给出程序中的每一条指令。然后GCC调用汇编器和链接器,根据汇编代码生成可执行的机器代码。在本章中,我们会近距离地观察机器代码,以及人类可读的表示一汇编代码。

3.1 历史观点

3.2 程序编码

  • 程序计数器(通常称为“PC”,在x86-64中用%rip表示)给出将要执行的下一条指令在内存中的地址。
  • 整数寄存器文件包含16个命名的位置,分别存储64位的值。这些寄存器可以存储地址(对应于C语言的指针)或整数数据。有的寄存器被用来记录某些重要的程序状态,而其他的寄存器用来保存临时数据,例如过程的参数和局部变量,以及函数的返回值。
  • 条件码寄存器保存着最近执行的算术或逻辑指令的状态信息。它们用来实现控制或数据流中的条件变化,比如说用来实现if和while语旬。
  • 一组向量寄存器可以存放一个或多个整数或浮点数值。

其中一些关于机器代码和它的反汇编表示的特性值得注意:

  • x86-64的指令长度从1到15个字节不等。常用的指令以及操作数较少的指令所需的字节数少,而那些不太常用或操作数较多的指令所需字节数较多。
  • 设计指令格式的方式是,从某个给定位置开始,可以将字节唯一地解码成机器指令。例如,只有指令pushq %rbx是以字节值53开头的。
  • 反汇编器只是基于机器代码文件中的字节序列来确定汇编代码。它不需要访间该程序的源代码或汇编代码。
  • 反汇编器使用的指令命名规则与GCC生成的汇编代码使用的有些细微的差别。在我们的示例中,它省略了很多指令结尾的‘q’。这些后缀是大小指示符,在大多数情况中可以省略。相反,反汇编器给oall和r圭t指令添加了‘q,后缀,同样,省略这些后缀也没有问题。

3.3 数据格式

C语言数据类型在x86-64中的大小:

3.4 访问信息

一个x86-64的中央处理单元(CPU)包含一组16个存储64位值的通用目的寄存器。这些寄存器用来存储整数数据和指针。
16个寄存器:

操作数格式。操作数可以表示立即数(常数)值、寄存器值或是来自内存的值。比例因子s必须是1、 2、 4或者8

零扩展数据传送指令:

符号扩展数据传送指令:

数据传送示例:

入栈和出栈指令:

3.5 算术和逻辑操作

整数算术操作:

加载有效地址(load effective address)指令Ieaq实际上是movq指令的变形。它的指令形式是从内存读数据到寄存器,但实际上它根本就没有引用内存。
个执行算术操作的函数示例:

特殊的算术操作:

3.6 控制

最常用的条件码有:
CF:进位标志。最近的操作使最高位产生了进位。可用来检查无符号操作的溢出o
zF:零标志。最近的操作得出的结果为00
sF:符号标志。最近的操作得到的结果为负数o
OF:溢出标志。最近的操作导致一个补码溢出一正溢出或负溢出。

条件码通常不会直接读取,常用的使用方法有三种:
1)可以根据条件码的某种组合,将一个字节设置为0或者1,
2)可以条件跳转到程序的某个其他的部分,
3)可以有条件传送数据。

set指令:

jmp指令:

条件控制来实现条件分支:

C语言提供了多种循环结构,即do-While、 While和foro 汇编中没有相应的指令存在,可以用条件测试和跳转组合起来实现循环的效果。

do-while

while
jump to middle

auarded-do

switch(开关)语句可以根据一个整数索引值进行多重分支(multiway branching)。在处理具有多种可能结果的测试时,不仅提高了C代码的可读性,而且通过使用跳转表(jump table)这种数据结构使得实现更加高

3.7 过程

过程是软件中一种很重要的抽象。
提供了一种封装代码的方式:
用一组指定的参数和一个可选的返回值实现了某种功能。
在程序中不同的地方调用这个函数。
设计良好的软件用过程作为抽象机制,隐藏某个行为的具体实现,同时又提供清晰简洁的接日定义,说明要计算的是哪些值,过程会对程序状态产生什么样的影响。不同编程语言中,过程的形式多样:函数(function),方法(method) ,子例程(subroutine) ,处理函数(handler)等等,但是它们有一些共有的特性。
要提供对过程的机器级支持,必须要处理许多不同的属性。为了讨论方便,假设过程P调用过程Q, Q执行后返回到po 这些动作包括下面一个或多个机制:
传递控制。在进人过程Q的时候,程序计数器必须被设置为Q的代码的起始地址,然后在返回时,要把程序计数器设置为p中调用Q后面那条指令的地址。
传递数据o p必须能够向Q提供一个或多个参数, Q必须能够向p返回一个值。
分配和释放内存。在开始时, Q可能需要为局部变量分配空间,而在返回前,又必须释放这些存储空间。

C语言过程调用机制的一个关键特性(大多数其他语言也是如此)在于使用了栈数据结构提供的后进先出的内存管理原则。在过程P调用过程Q的例子中,可以看到当Q在执行时, P以及所有在向上追溯到p的调用链中的过程,都是暂时被挂起的。当Q运行时,它只需要为局部变量分配新的存储空间,或者设置到另一个过程的调用。另一方面,当Q返回时,任何它所分配的局部存储空间都可以被释放。因此,程序可以用栈来管理它的过程所需要的存储空间,栈和程序寄存器存放着传递控制和数据、分配内存所需要的信息。当P调用Q时,控制和数据信息添加到栈尾。当p返回时,这些信息会释放掉。

将控制从函数p转移到函数Q只需要简单地把程序计数器(PC)设置为Q的代码的起始位置。不过,当稍后从Q返回的时候,处理器必须记录好它需要继续P的执行的代码位置。在x86-64机器中,这个信息是用指令call Q调用过程Q来记录的。该指令会把地址A压人栈中,并将PC设置为Q的起始地址。压人的地址A被称为返回地址,是紧跟在call指令后面的那条指令的地址。对应的指令ret会从栈中弹出地址A,并把PC设置为A。

call和ret函数说明:

当调用一个过程时,除了要把控制传递给它并在过程返回时再传递回来之外,过程调用还可能包括把数据作为参数传递,而从过程返回还有可能包括返回一个值o x86-64中,大部分过程间的数据传送是通过寄存器实现的。

到目前为止我们看到的大多数过程示例都不需要超出寄存器大小的本地存储区域。不
过有些时候,局部数据必须存放在内存中,常见的情况包括:

  • 寄存器不足够存放所有的本地数据。
  • 对一个局部变量使用地址运算符‘&’,因此必须能够为它产生一个地址。
  • 某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到。

3.8 数组分配和访问

C语言中的数组是一种将标量数据聚集成更大数据类型的方式o C语言实现数组的方式非常简单,因此很容易翻译成机器代码o C语言的一个不同寻常的特点是可以产生指向数组中元素的指针,并对这些指针进行运算。在机器代码中,这些指针会被翻译成地址计算

对于数据类型丁和整型常数N,声明如下:

T A[Ⅳ];

起始位置表示为工AO 这个声明有两个效果。首先,它在内存中分配一个L●N字节的连续区域,这里L是数据类型丁的大小(单位为字节)。其次,它引人了标识符A,可以用A来作为指向数组开头的指针,这个指针的值就是工Ao 可以用0-N-1的整数索引来访间该数组元素。数组元素;会被存放在地址为工A+L上的地方。

例,

char A[12] ;
char *B[8];
int C[6] ;
double *D[5] ;

扩展一下前面的例子,假设整型数组E的起始地址和整数索引;分别存放在寄存器%rdx和%rcx中。下面是一些与E有关的表达式。我们还给出了每个表达式的汇编代码实现,结果存放在寄存器%eax(如果是数据)或寄存器宅rax(如果是指针)中。

嵌套数组中按优先顺序排列的数组元素:

计算定长数组的矩阵乘积的元素i,k。

编译器会自动完成这些优化

ISOC99引人了一种功能,允许数组的维度是表达式,在数组被分配的时候才计算出来。
在变长数组的C版本中,我们可以将一个数组声明如下:

int A[expr1][expr2]


优化后的C代码:

3.9 异质的数据结构

C语言的struct声明创建一个数据类型,将可能不同类型的对象聚合到一个对象中。用名字来引用结构的备个组成部分。类似于数组的实现,结构的所有组成部分都存放在内存中一段连续的区域内,而指向结构的指针就是结构第一个字节的地址。编译器维护关于每个结构类型的信息,指示每个字段(field)的字节偏移。它以这些偏移作为内存引用指令中的位移,从而产生对结构元素的引用。

struct rec [
	int i;
	int j;
	int a[2];
	int *p;
}

联合提供了一种方式,能够规避C语言的类型系统,允许以多种类型来引用一个对象。联合声明的语法与结构的语法一样,只不过语义相差比较大。它们是用不同的字段来引用相同的内存块。

字段的偏移量、数据类型S3和U3的完整大小:

无论是否对齐,x86-64都能正常工作,但Intel建议对齐数据以提高系统的性能。
对齐原则:任何K字节的基本对象的地址都必须是K的倍数

3.10 在机器级程序中将控制与数据结合起来

指针是c语咅的一个核心特色.它们以一种统一方式.对不同数据结构中的元素产生引用,介绍一些指针和它们映射到机器代码的关键原则.

  • 每个指针都对应一个类型.这个类型表明该指针指向的是哪一类对象。
  • 每个指针都有一个值,这个值是某个指定类型的对象的地址.
  • 指针用'&'运算符创建.这个运算符可以应用到任何lvalue类的C表达式上.lvalue意指可以出现在赋值语句左边的表达式,
  • *操作符用于间接引用指针,它的类型与该指针的类型一致。
  • 数组与指针緊密联系,一个数组的名字可以像一个变量一样引用(但是不能修改).数组引用与指针运算和间接引用有一样的效果
  • 将指针从一种类型强制转换成另一种类型.只改变它的类型,而不改变它的值.强制转换的一个效果是改变指针运算的伸缩。
  • 指针也可以指向函数

内存越界和缓冲区溢出

3.11 浮点代码

处理器的浮点体系结构包括多个方面,会影响对浮点数据操作的程序如何被映射到机器上.包括:

  • 如何存储和汸问浮点数值。通常是通过某种寄存器方式来完成。
  • 对浮点数据操作的指令.
  • 向闲数传递浮点数参数和从函数返回浮点数结果的规则。
  • 函数脚用过程中保存布存器的规则——例如,一些寄存器被栴定为调用荇保存.而 其他的被指定为被调用者保存。

媒体寄存器:


浮点传输指令

双精度浮点转换指令

三精度浮点转换指令

标量浮点算术运算

浮点代码的条件分支说明:

C语言:

汇编代码:

3.12 小结

3.64

[S * T = 65 T = 13 S * T * R * 8 = 3640 ]

[R:7 S:5 T:13 ]

3.71


#include<stdio.h> 
#define MAX 10 
void good_echo() 
{ 
char buffer[MAX]; 
while (fgets(buffer, MAX, stdin) != NULL) { 
printf("%s", buffer); 
if (ferror(stdin)) { 
printf("
Error
"); 
return; 
} 
} 
} 

int main() 
{ 
good_echo();
system("pause");
}

原文地址:https://www.cnblogs.com/gdman/p/10005287.html