当前位置:   article > 正文

深入理解计算机系统学习笔记

深入理解计算机系统学习笔记

3 . 7 过程

过程是软件中一种很重要的抽象。它提供了一种封装代码的方式,用一组指定的参数和一个可 选的返回值实现了某种功能。然后,可以在程序中不同的地方调用这个函数。

不同编程语言中,过程的形式多样:函数(function)、方 法(method)、子例程(subroutine)、处理函数(handler)等等,但是它们有一些共有的特性。

过程举例说明

假设过程 P 调用过程Q,Q执行后返回到P。这些动作包括下面一个或多个机制:

传递控制。在进人过程Q的时候,程序计数器必须被设置为Q的代码的起始地址,然后在返回时,要把程序计数器设置为P中调用Q后面那条指令的地址。

传递数据。P必须能够向Q提供一个或多个参数,Q必须能够向P返回一个值。

分配和释放内存。在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须 释放这些存储空间。

3.7. 1 运行时栈

C语言过程调用机制的一个关键特性(大多数 其他语言也是如此)在于使用了栈数据结构提供的 后进先出的内存管理原则。

将栈指针减小一个适当的量可以为没有指定初始值的数据在栈上分配空间。类似地,可以通过增加桟指针来释放空间。 当X86-64 过程需要的存储空间超出寄存器能够存放的大小时,就会在栈上分配空间。这个部分称为过程的栈帧(stack fram)。

为了提髙空间和时间效率,x86-64过程只分配自己所需要的栈帧部分。实际上,许多函数甚至根本不需要栈帧。

3 .7.2 转移控制

将控制从函数P转移到函数Q只需要简单地把程序计数器(PC)设置为Q的代码的起始位置。

call和ret指令的一般形式:

call指令有一个目标,即指明被调用过程起始的指令地址。同跳转一样,调用可以是直接的,也可以是间接的。在汇编代码中,直接调用的目标是一个标号,而间接调用的目标是*后面跟一个操作数指示符。

3.7.3 数据传送

当调用一个过程时,除了要把控制传递给它并在过程返回时再传递回来之外,过程调 用还可能包括把数据作为参数传递,而从过程返回还有可能包括返回一个值。

x86-64中, 大部分过程间的数据传送是通过寄存器实现的。例如,我们已经看到无数的函数示例,参数在寄存器%rdi、%rsi和其他寄存器中传递。当过程P调用过程Q时,P的代码必须首先把参数复制到适当的寄存器中。类似地,当Q返回到P时,P的代码可以访问寄存S%rax 中的返回值。

X86-64 中,可以通过寄存器最多传递6个整型(例如整数和指针)参数。

如果一个函数有大于6个整型参数,超出6个的部分就要通过栈来传递。

3.7. 4 栈上的局部存储

到目前为止我们看到的大多数过程示例都不需要超出寄存器大小的本地存储区域。

不过有些时候,局部数据必须存放在内存中,常见的情况包括:

寄存器不足够存放所有的本地数据。

对一个局部变量使用地址运算符因此必须能够为它产生一个地址。

某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到。

3.7.5 寄存器中的局部存储空间

寄存器组是唯一被所有过程共享的资源。虽然在给定时刻只有一个过程是活动的,我们仍然必须确保当一个过程(调用者)调用另一 个过程(被调用者)时,被调用者不会覆盖调用者稍后会使用的寄存器值。为此,x86-64采 用了一组统一的寄存器使用惯例,所有的过程(包括程序库)都必须遵循。

根据惯例,寄存器%rbx、%rbp和%r12~%r15被划分为被调用者保存寄存器。当过程P调用过程Q时,Q必须保存这些寄存器的值,保证它们的值在Q返回到P时与Q被调用时是一样的。过程Q保存一个寄存器的值不变,要么就是根本不去改变它,要么就是把原始值压人栈中,改变寄存器的值,然后在返回前从栈中弹出旧值。

3.7. 6 递归过程

递归调用一个函数本身与调用其他函数是一样的。栈规则提供了一种机制,每次函数调用都有它自己私有的状态信息(保存的返回位置和被调用者保存寄存器的值)存储空间。如果需要,它还可以提供局部变量的存储。栈分配和释放的规则很自然地就与函数调用-返回的顺序匹配。

示例:

3.8 数组分配和访问

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

3. 8. 1 基本原则

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

T A[N];

示例:

 char A[12];

char *B[8];

int C[6];

double *D[5];

这些声明会产生带下列参数的数组:、

数组A由12个单字节(char)元素组成。数组C由6个整数组成,每个需要8个字节。 B 和D都是指针数组,因此每个数组元素都是8个字节。

X86-64 的内存引用指令可以用来简化数组访问。例如,假设E是一个 int 型的数组, 而我们想计算E [i],在此,E的地址存放在寄存器%rdx中,而i存放在寄存器%rcx中。 然后,指令

movl (%rdx,%rcx,4),%eax

会执行地址计算Xe+4i,读这个内存位置的值,并将结果存放到寄存器%eax中。

3 .8.2 指针运算

C语言允许对指针进行运算,而计算出来的值会根据该指针引用的数据类型的大小进行伸缩。也就是说,如果p是一个指向类型为T的数据的指针,p的值为Xp,那么表达式 P+i 的值为Xp + i,这里L是数据类型T的大小。

单操作数操作符‘&’和‘* ’可以产生指针和间接引用指针。

假设整型数组 E的起始地址和整数索引 i 分别存放在寄存器%rdx和%rcx中。下面是一些与E有关的表达式。

对于int类型,涉及到4字节操作(例如 movl)和寄存器(例如%eax)。

对于int *类型 , 涉及到 8字节操作 (例如 leaq)和寄存器(例如%rax)。

计算两个指针之间的差值 = 两个地址之差 / 该数据类型的大小。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/164532
推荐阅读
相关标签
  

闽ICP备14008679号