CSAPP笔记(上)


1. x86-64机器码可见的寄存器

  1. 程序计数器:给出将要执行的下一条指令在内存中的地址;
  2. 整数寄存器文件:包含16个命名的位置,分别存储64位的值;这些寄存器可以存储地址空间,比如C语言中的指针,也可以存储整数数据;有的寄存器被用来存储比较重要的程序状态,而其他的寄存器可以用来保存临时的数据,比如说过程的参数以及局部变量和函数的返回值;
  3. 条件码寄存器:保存着最近执行的算数或者是逻辑指令的状态信息,他们用来实现控制数据流中的条件变化,比如说用来实现if和while语句;
  4. 向量寄存器:一组向量存储器可以存储一个或者多个整数或浮点数的值;

2. 过程及过程调用

  1. 过程是软件中很重要的一种抽象,过程提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能。然后我们就可以在程序的不同地方对该过程进行调用。用过程进行抽象可以隐藏某个行为的具体实现,并可以提供清晰简洁的接口定义,用以说明过程计算的是哪些值,过程会对程序的状态产生什么样的影响等。在不同的编程语言中对过程有不同形式的体现:函数,方法,处理函数等,但他们都具有一致的特性;

  2. 当过程P想要调用过程Q的时候,他们需要包括下面的几个机制:

    • 传递控制:在进入过程Q的时候,程序计数器必须被设置为Q的代码的起始位置;然后在返回的时候把程序计数器的值设置为P接下来要执行的指令的内存地址;
    • 传递数据:在过程调用的时候,P需要向Q传递数据,比如说参数信息,同时Q能够向P返回一个值;
    • 分配和释放内存:在开始的时候Q可能会需要为局部变量分配内存空间,而在过程Q返回之前,又必须释放掉之前分配的内存空间;
  3. 运行时栈:在大多数的语言中,过程调用机制的实现的一个关键特性就是利用了栈这一数据结构后进先出的特性。比如在过程P调用过程Q的例子中,当Q在执行的时候,在Q之上的调用链中的过程都是暂时被挂起的。Q会被放到栈顶,他只需要为局部变量分配新的存储空间,或者是设置到另一个过程的调用。当Q返回的时候,任何它所分配的局部存储空间都可以被释放。需要注意的是,过程中的信息不仅仅是存放在栈上面的,它们还可会被存放于寄存器之中;x86-64的栈空间是向低地址方向增长的,栈底位于高地址,栈指针%rsp指向栈顶元素。当x86-64程序的过程需要的存储空间超过寄存器能够存放的大小的时候,过程就会在栈上分配空间,被分配的空间称为过程的栈帧。在栈这一数据结构中,当前正在执行的过程的栈帧做事位于栈顶。当过程P调用过程Q的时候,会把过程Q的栈帧压入栈顶,同时指明当Q执行完毕返回之后要从P程序的哪一个位置继续执行代码。我们通常都会把返回地址当做是P的栈帧的一部分,因为返回地址存放的是与P相关的状态信息。过程调用返回后需要继续执行的P的代码位置是用指令call Q调用过程Q开记录的,假设P在Q返回后需要继续执行的指令地址是A,那么call指令就会将A压入栈中(同时将PC设置为Q的起始地址,压入的地址A被称为返回地址,是紧跟在call指令后面的那条指令的地址),对应的ret会从栈中弹出地址A,并把PC的值设置为A。过程Q的代码会扩展当前栈的边界用于分配Q的栈帧所需要的存储空间。在Q的栈帧之中,Q可以保存寄存器的值,也可以分配局部变量的空间,也可以为它调用的过程设置参数。大多数过程的栈帧都是定长的,在过程开始的时候就已经分配好了,但是仍存在需要变长帧的过程。当过程P要向过程Q传递参数的时候,x86-64允许P通过寄存器传递最多6个参数,如果需要传递更多的参数,P就需要在自己的栈帧中存储好那些高于6个的参数。为了提高过程执行时的空间效率以及时间效率,x86-64中的过程只会自己所需要的栈帧的部分。其实许多的过程都不会有6个参数,那么所有的参数都可以通过寄存器进行传递,因此下图中的某些栈帧部分可以省略,对于某些过程来说甚至都不需要分配栈帧。当所有的局部变量都可以保存在寄存器中并且该过程不会调用其它的任何过程的时候,x86-64就可以不为过程分配栈帧。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    %rsp是栈指针,%rip是程序计数器。
    在这里插入图片描述
    在x86-64中,大部分的过程间数据传送是通过寄存器来实现的。当过程P调用过程Q的时候,P的代码首先吧数据复制到适当的寄存器之中,并且当Q返回到P的时候,P也可以通过访问寄存器%rax来获取其中存储的过程Q返回值。x86-64中可以通过寄存器传递最多6个参数,寄存器的名字取决于参数的大小,如下图所示:
    在这里插入图片描述
    如果一个过程的参数大于6个,那么超出6个的部分就要通过栈来传递,比如P调用Q需要传递n > 6个参数,那么P的栈帧必须要能够容纳超出6的参数,参数1~6存储在寄存器之中,其余的参数存储在栈上,注意:参数7会被存储在栈顶,参数是被逆序存储的。通过栈传递参数时,所有的数据大小都向8的倍数对齐。参数到位后,程序就可以执行call指令将控制转移到Q了。
    在这里插入图片描述
    在这里插入图片描述
    虽说局部数据可以存放在寄存器之中,但是在以下的几种情况下局部数据必须存储在内存中,常见的情况包括:

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

    下图所示的call_proc给出了一个必须在栈上分配局部变量存储空间的函数,同时还要向有8个参数的函数proc传递值;该函数创建一个栈帧;
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在call_proc的汇编代码之中的一大部分(2~15行)都是在为调用proc做准备。其中包含为局部变量和函数参数建立栈帧,将函数参数加载至寄存器,如上图所示,局部变量会在栈上被分配,同时值为4的参数7以及指向x4的位置的指针的参数8会被存放在栈中;当程序返回call_proc的时候代码会取出4个局部变量,并执行最终的计算。在程序结束前把栈指针增加32释放这个栈帧;
    寄存器组是唯一被所有的过程共享的资源,虽然在给定的时间内只有一个过程是在活动的,但是我们仍然必须确保当一个过程调用另一个过程的时候,被调用者不会覆盖调用者稍后会使用到的寄存器的值,为此,x86-64采用了一组统一的寄存器使用习惯,所有的过程都必须遵守。其中寄存器%rbx,%rbp和%r12~%r15被划分为被调用者保存寄存器。当过程P调用Q的时候,Q必须保存这些寄存器的值,保证它们的值在Q返回到P时与Q被调用的时候是一样的。过程Q保存一个寄存器的值不变,要么是根本不会去改变其中的值,要么是把原始的值压入到了栈中,改变寄存器的值,之后在返回之前从栈中弹出旧值。被压入寄存器中的值会在栈帧中创建标号为“保存的寄存器”的一部分。有了以上的惯例,P的代码就能够安全的把值存放到被调用者保存寄存器中(当然要先把之前的值保存在栈中),调用Q,然后继续使用寄存器中的值,不必担心只会被破环。所有其他除栈指针%rsp之外的寄存器都被分类为调用者保存寄存器。这就意味着任何的过程都能够修改它们。过程P在某个此类寄存器中存在有局部数据,然后调用过程Q,由于Q可以随意修改这个寄存器,所以说P在调用之前应该保存好这个数据。同时,递归调用一个过程与调用其它过程是一样的,栈提供了一种机制,每次函数调用都有它自己私有的状态信息(保存的返回位置 和被调用者保存寄存器的值)存储空间,因此多个未完成调用的局部变不会相互影响。如果需要,它还可以提供局部变量的值。栈分配和释放的规则很自然地就与函数的调用与返回的顺序相匹配,这种实现函数调用与返回的方法甚至对更复杂的情况也适用,包括相互递归调用。

3. 数组相关

  1. x86-64的内存引用指令可以简化数组的访问。例如,假设E是一个int型的数组,而我们想要计算E[i],在此,E的地址存放在寄存器%rdx之中,而i索引值粗放在寄存器%rcx之中,然后会有以下的汇编指令: movl (%rdx,%rcx,4),%eax,会执行地址计算address(E) + 4i,读取这个内存位置的值,并将读取的结果存放到寄存器%eax之中,允许的伸缩因子1,2,4,8覆盖了所有基本简单数据类型的大小;
  2. C语言的编译器能够优化定长多维数组上的操作代码,具体来说就是在某些情况下去掉相应的整数索引,并把所有的数组应用都替换为指针间接引用;指针间接引用更具备灵活性并且指针会更有效率;对于变长数组,GCC能够识别出程序访问多维数组的元素的步长,然后生成的代码会避免直接应用等式&D[i][j] = address(D) + L(C*i + j)会导致的乘法。不论是生成基于指针的代码还是生成基于数组的代码,这些优化都能够显著的提高程序的性能;以下分别是定长数组的优化情况以及变长数组的优化情况;
    在这里插入图片描述
    在这里插入图片描述

4. 数据对齐

  1. 许多计算机系统对基本数据类型的合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(通常是2, 4或8)的倍数。这种对齐限制简化了形成处理器和内存系统之间接口的硬件设计。例如,假设一个处理器总是会从内存中取出8个字节的数据,则地址必须为8的倍数。如果我们能够保证所有的double类型数据的地址对齐成8的倍数,那么就可以用一个内存操作来进行读操作或者是写操作。否则我们可能需要进行两次的内存访问,因为对象可能分别放在两个8字节的内存块中。无论是否进行对齐,x86-64的硬件都能够正常的工作,但是对齐数据可以提高内存系统的性能。
  2. 对齐的原则是任何K字节的基本对象的地址必须是K的倍数,所以这条对齐原则会得到如下的对齐:
    在这里插入图片描述
    确保每种数据类型都是按照指定的方式来组织和分配,即每种类型的对象都满足它的对齐限制,就可以保证实施对齐。编译器会在汇编代码中放入命令来指明全局数据所需的对齐。对于包含结构的代码,编译器可能会需要在字段的分配中插入间隙,以保证
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值