最近会针对几个重要的部分对CS:APP以及PPT进行重读和理解,然后发在这里,最重要的目标是,先理清整个过程或者结构,然后去理解为什么要这么做,或者说这么实现的好吃在哪,然后是一些总结之类的东西,大概就是这些,希望这遍能够细读,有更深的理解和体会,并把以前浅薄的理解全部串联起来。
虽说黑书很多地方不如人意,但是这一段相较原书还是翻译的比较准确,完全体现了原文的意思,就摘录在这里。
过程
过程是软件中一种很重要的抽象。它提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能。然后,可以在程序中不同的地方调用这个函数。设计良好的软件用过程作为抽象机制,隐藏某个行为的具体实现,同时又提供清晰简洁的接口定义,说明要计算的是哪些值,过程会对程序状态产生什么样的影响。不同编程语言中,过程的形式多样:函数(function)、 方法(method)、子例程(subroutine)、处理函数(handler)等等,但是它们有一些共有的特性。
过程中大致包含:
- 传递控制。在进入过程Q的时候,程序计数器必须被设置为Q的代码的起始地址,然后在返回的时候,要把程序计数器设置为P调用Q后面那条指令的地址。
- 传递数据。P必须能够向Q提供一个或多个参数,Q必须能够向P返回一个值。
- 分配和释放内存。在开始时,Q可能需要为局部变量分配空间,而返回前,又必须释放这些存储空间。
我们从上述文字阅读完毕之后就能清楚过程究竟是一个什么东西,最主要的目的就是去复用封装的代码,因此才有过程调用的意义,在过程中为了保证功能的实现我们需要对信息进行传递,所以可以把过程完全的看作是对编程的简化,因此我们就可以知道为什么well-disigned software会更多的使用过程调用。
控制传递
栈
我们上述讨论了为什么要有过程调用,所以就要清楚过程调用到底需要什么,我们需要相关的信息才能相关的功能实现(这也是为什么第二章先去讲什么是信息以及信息的表示和传递),我们使用的就是栈这种数据结构进行了信息的传递。C语言就靠栈这个数据结构实现了过程调用机制,这个数据结构也符合我们过程调用这个过程的实际需求。
当过程P调用过程Q时,在Q执行时,我们把P以及其之前的过程调用链挂起,然后为Q的局部变量分配空间,或者继续设置他的过程调用;当Q返回时,任何它所分配的局部存储空间都被释放。因此我们使用栈区管理存储空间以及控制和数据相关信息。
我们通过pushq和popq指令对数据进行操作,通过加减相应数值在栈上分配空间。我们注意,x86和IA32在此处不同,x86使用寄存器传递参数(六个),因此只有参数超出寄存器限制之后才会使用栈来传参,而IA32则不同,IA32不使用寄存器传参,所以所有参数都通过栈来传递,而且要将相应寄存器的内容也放到栈上保存,所以IA32会先产生一个固定大小的栈帧,使用参数的时候就直接在栈指针上加相应的长度即可(是在返回地址的指针处加)。
因此我们通读之后发现是首先向栈中放该过程想要调用的过程所需要的参数,从参数n开始压栈,一直压到参数7,然后返回地址入栈,最后下面开始就是下一个过程的栈帧,下一个过程需要保存其所需保存的寄存器值,对其进行维护,然后把该过程创建的局部变量放道栈上,如果还需要有其他的过程调用就重复上述过程,这就是一个比较完整的过程调用的过程了。(而我们大部分调用的过程甚至不需要形成栈帧,x86中)
转移控制
我们在上一节展现了过程调用中的数据流,这一部分写写控制流是怎么样的。
我们在最开始的时候就说明了控制传递的过程,从函数P到函数Q需要使用call Q指令,这时我们需要把先把程序计数器中的A压入栈中,然后把程序计数器(PC)设置为Q的起始地址,当需要返回时,就从栈中弹出最开始的地址A。(注意:地址A是调用指令之后的那条,无论是从逻辑还是从实际来看都是如此)
数据传送
我们除了使用栈来传递数据流还有一个更加重要的传递方式就是寄存器传参,这也是为什么之前我们说很多过程调用其实不需要形成栈帧,这样的话就会节省更多的时间,我们进行过程调用时会更少的进行访存这个动作,要知道任何涉及到读写内存的行为都可能带来难以容忍的开销,而且我们在编译中也更加真实的体会到了分配寄存器的重要性,合理的分配寄存器会给我们的编译之后的程序带来更好的性能,从而更好更快的完成任务。
在x86-64中,可以通过寄存器最多传递6个整型参数。寄存器的使用是有特殊顺序的,寄存器使用的名字决定传递数据类型的大小,根据参数在参数列表中的顺序为他们分配寄存器。(这里提一句,栈中的参数如果大小不定则使用最低的几字节)
再说两句栈上的局部存储
有些时候,局部数据需要放在内存中:
- 寄存器不足够存放所有的本地数据
- 对一个局部变量使用地址运算符'&',因此必须为其产生一个地址
- 某些局部变量是数组或者结构,因此必须能够通过数组或结构引用被访问到
当有上述情况的后两种时就需要为其分配栈帧。
寄存器中的局部存储空间
寄存器组时唯一被所有过程共享的资源,虽然在给定时刻只有一个过程是活动的,我们仍然必须确保当一个过程(调用者)调用另一个过程(被调用者)时,被调用者不会覆盖调用者稍后会使用的寄存器值。
根据惯例,寄存器%rbx、%rbp和%r12-%r15被划分为被调用者保存寄存器,当过程P调用过程Q时,Q必须保存这些寄存器的值,保证它们的值在Q返回P时与Q被调用时是一样的。过程Q保存一个寄存器的值不变,要么是根本不去改变他,要么是把原始值压入栈中,改变寄存器的值,然后再返回前从栈中弹出旧值。因此P可以安全地把值存在被调用者保存寄存器上而不用担心值被破坏。
所有其他的寄存器,除了栈指针,都分类为调用者保存寄存器,这就意味着任何函数都能修改他们。
递归过程
就是连续嵌套的过程调用,调用到最后一层依次返回(所以你知道为什么递归会很慢了把)