您的位置:首页 > 运维架构 > Linux

浅析linux系统函数调用的工作机制

2015-07-01 16:53 197 查看
赵建清+原创作品转载请注明出处+《Linux内核分析》MOOC课程http://mooc.study.163.com/learn/USTC-1000029000

本文以汇编语言为工具,分析linux系统函数调用的基本工作机制。首先说明进程的内存布局,然后使用一个实例说明在进程运行过程中栈帧的动态变化过程。
进程是一个可执行程序的实例。从内核角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码使用的变量,而内核数据结构用于维护进程状态信息。

每个进程分配的内存由很多部分组成,主要有:

文本段:包含了进程运行的机器语言指令。

初始化数据段:包含显式初始化的全局变量和静态变量。

未初始化数据段:包含了未进行显式初始化的全局变量和静态变量。

栈:是一个动态增长和收缩的段,由栈帧组成。系统为每个当前调用的函数分配一个栈帧。栈帧中存储了函数的局部变量、形参和返回值。

堆:进程运行时动态内存分配的区域。

下图给出x86-32体系结构中的内存布局。



栈帧是为传递的参数、子例程的返回地址、局部变量和保存的寄存器保留的堆栈空间。栈帧是按以下步骤创建的:

如果有传递的参数,则压入堆栈。

子例程被调用,子例程的返回地址压入堆栈。

子例程开始执行时,EBP被压入堆栈。

EBP设为ESP的值,从这时开始,EBP就被作为寻址所有子例程参数的基址指针。

如果有局部变量,ESP减去一个数值,以便在堆栈上为局部变量保留空间。

如果任何寄存器需要保存,则压入堆栈。

以下用于分析的简单C语言程序main.c:



使用如下命令编译成汇编代码,然后删除汇编代码中用于链接的辅助信息:

gcc –S –o main.s main.c -m32

处理后的汇编代码为:



我们从程序入口main函数开始分析栈帧的变化过程,程序从main.s第17行开始运行,执行完第21行后堆栈内容如下:



接着运行第22行的call指令,call指令指示处理器在新的内存地址执行指令,以实现对函数的调用。具体操作是把返回地址压入栈并且把被调用过程的地址复制到指令指针寄存器中。执行第22行后,将函数f的调用地址存入指令指针寄存器EIP,栈帧内容如下:



然后开始执行函数f,执行第9行和第10行用于建立一个栈帧,并且将EBP设为这个栈帧的基址:



第11行ESP值减4,指向内存中低地址的下一下字;第12行使用寄存器相对寻址方式将函数的形式参数x复制到到寄存器EAX;第13行将EAX中的数即传入的参数复制到ESP所指向的内存,用做调用函数g时的参数。执行完第13行以后的栈帧内容如下:



第14行将调用函数g,执行完以后栈帧内容如下:



执行第14行后开始调用函数g,第2行和第3行用于建立函数g的栈帧,并且将EBP设为这个栈帧的基址。执行完第3行以后栈帧内容如下:



第4行使用寄存器相对寻址方式将所传入参数复制到通用寄存器EAX;第5行将EAX中的值加上立即数314,相加后的和仍保存在EAX中。执行完第5行后的栈帧内容如下:



第6行的popl指令后用于恢复上一个栈帧的基址,准备从函数g中返回,执行第6行后栈帧内容如下:



接着执行第7行的ret指令,函数g执行返回到函数f中。ret指令作用是使处理器返回到程序中子例程被调用的地方继续执行。具体操作是从栈上弹出由call指令保存的返回地址到指令指针寄存器EIP。执行完ret指令后栈帧内容如下:



ret指令执行后函数g调用完毕,返回到第15行。leave指令用于释放一个子例程的栈帧,等价与以下两条指令:

movl %ebp, %esp

popl %ebp

执行第15行的leave指令后,栈帧内容如下:



接着执行第16行的ret指令,从函数f返回到主函数main,执行后栈帧内容如下:



调用函数f返回后,返回结果保存在通用寄存器EAX中。开始执行第23行,函数f的返回结果加上5,结果仍保存在寄存器EAX中。执行第23行后,EAX的值为加法的和:



最后执行第24行leave指令和第25行的指令,清理函数main的栈帧并且返回,返回值保存在通用寄存器EAX中。

掌握函数调用过程中栈帧的运行方式非常重要,几乎所有的高级语言都使用堆栈传递参数。本文通过一个简单的例子说明了程序运行过程中堆栈的动态变化过程,包括如何建立一个函数的栈帧,以及如何从调用函数中返回。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: