程序的机器级表示三(过程)
目录
过程:
- 高级语言中的函数
- 过程的调用包括将数据和控制从代码的一部分传递到另一部分
- 进入过程为局部变量分配空间,退出释放空间
- 大多数机器只提供转移控制到过程、从过程转移出控制的指令
- 数据的传递和局部变量的分配释放,都通过操纵程序栈实现
过程的作用:
- 程序变得更简短清晰
- 提高程序开发的效率
- 提高代码复用性
- 有利于程序维护
机器栈&函数调用过程栈的变化:
- 传递过程参数
- 存储返回信息
- 保存寄存器
- 本地存储
栈帧:为单个过程分配的那部分栈
函数调用时:
①函数参数从右至左依次push入栈
②返回地址push入栈(隐藏在call指令中)
③使用call调用目标函数
函数中:
①push ebp 保存旧ebp值
②mov esp,ebp 栈底指针到位,进入函数调用栈
③【可选】sub ebp,XXX 开辟栈空间
④【可选】push XXX 保存一些寄存器(调用结束后需要恢复)
函数调用完毕后:
①保存返回值 寄存器eax中
②【可选】pop恢复一些寄存器的值
③ mov esp,ebp 恢复原栈顶,回收局部变量空间
④pop ebp 恢复调用函数的栈底
⑤ret 获得返回地址,继续执行调用函数的下一条指令
- esp:栈指针寄存器(extended stack pointer),其内存放着一个指针, 该指针永远指向系统栈最上面一一个栈帧的栈顶。
- ebp:基址指针寄存器(extended base pointer).其内存放着-一个指针,该指针永远指向系统栈最上面一个栈帧的底部。(ebp在当前栈帧内位置固定, 故函数中对大部分数据的访问都基于ebp进行)
- eip:指令寄存器(extended instruction pointer),其内存放着一个指针, 该指针永远指向下一条等待执行的指令地址。可以说如果控制了 EIP寄存器的内容,就控制了进程一我们让eip指向哪里, CPU就会去执行哪里的指令。eip可被jmp、 call和ret等指令隐含地改变(事实上它一直都在改变)(ret指令就是把当前栈顶保存的返回值地址弹到eip中)
- 函数栈帧的大小并不固定,一般与其对应函数的局部变量多少有关。函数运行过程中,其栈帧大小也是在不停变化的。
栈的维护方式
(谁负责弹出形参? )
在被调函数返回时,需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致。这个弹出的工作可以由函数的调用方完成,也可以由被调用函数完成。
函数的返回值的保存:
- <5字节,eax保存
- 对于返回5~8字节数据的情况,一般采用eax和edx联合返回的方式进行的。其中eax存储返回值的低4字节,edx存储返回值的高4字节。
- 超过8字节:
- 在主调函数和被调函数中都应该有一个128字节的变量(big_thing类型)假设分别为n和b
- 那么实际上,编译器是怎么设计大尺寸返回值传递的呢?
* main函数在其栈中的局部变量区域中额外开辟一片空间,将其一部分作为传递返回值的临时对象temp。
*将temp对象的地址作为隐藏参数传递给return test函数。
* return_ test函数将数据拷贝给temp对象,并将temp对象的地址用eax传出。
* return_test返回后,main函数将eax指向的temp对象的内容拷贝给n。
寄存器使用惯例:
- 根据惯例,寄存器 %eax %edx %ecx 被划分为调用者保存寄存器。当过程 调用时, 可以覆盖这些寄存器,而不会破坏任何 所需要的数据。
- 寄存器 %ebx %esi %edi 被划分为被调用者保存寄存器。这意味着 必须在覆盖这些寄存器的值之前,先把它们 保存到栈中,并在返回前恢复它们,因为 (或某个更高层次的过程)可能会在今后的计算中需要这些值。
- 必须保持寄存器 %ebp %esp
*为什么 GCC 分配从不使用的空间?
GCC caller 参数的代码在栈上分配了 24 个字节,但是只使用了其中的 16 个。我们会 看到很多这样明显浪费的例子。 GCC 坚持一个 x86 编程指导方针,也就是一个函数使用的所有 栈空间必须是 16 字节的整数倍。包括保存 %ebp 值的个字节和返回值的 个字节, caller一共使用了 32 个字节。采用这个规则是为了保证访问数据的严格对齐 (alignment) 。
call,leave,ret
call指令: push return address,mov target,eip 压入返回地址,将目标代码首地址传递给eip
leave指令:mov ebp,esp 恢复栈指针
pop ebp 恢复帧指针
ret指令:pop eip 返回地址给eip