程序的动态执行过程

来源:互联网 发布:js math 向下取整 编辑:程序博客网 时间:2024/05/01 06:26

1.程序的开始



放在外存上的二进制文件是如何开始运行的,从什么地方开始运行,是一个值得深究的问题。对于编译型的语言,从源代码到可执行文件,一般都会经过预处理,汇编,编译,链接等过程,最后生成可执行文件。对于C语言,逻辑层面上,main函数是程序的入口。在L/Uinux系统下,把二进制可执行文件从外存读入内存,并跳转到main函数的工作则是内核的exec系统调用来完成。exec函数的作用是用新的程序替换系统当前进程的执行程序,包括代码,数据,堆和栈。简而言之,exec用来执行新的程序。


1.1 exec系统调用


Linux Kernel exec系统调用申明在include/linux/syscalls.h文件中

三个参数的含义分别是:

新程序的绝对路径(ex: /usr/bin/ls)

新程序用到的参数

环境变量指针


不同体系结构下,kernel_execve的实现也不同,arm体系结构下的代码实现在arch/arm/kernel/sys_arm.c文件中。

kernel_execve处理系统调用从用户态到内核态切换相关工作,并调用do_execve函数处理执行程序的任务。

do_execve函数定义在fs/exec.c文件中,在do_execve函数中又封装了一层,最终调用do_execve_common函数,do_execve_common函数也定义在fs/exec.c文件中。整个过程的实现涉及到很多的细节问题。

后面在写一篇详细分析一下exec涉及到的内容。
粗略概括起来做了如下几件事情:
1. 对新程序执行进程的证书,权限等信息的检查和控制
2. 可执行文件的检索,读取等
3. 可执行文件个格式的检测和匹配,例如shell脚本或者elf文件
4. 更新进程的运行环境,例如堆栈的分配
5. 可执行文件的加载和执行


总结起来,exec函数是操作系统提供给用户态程序的一个门。用户态的程序可以通过这个门执行。当然也可以直接从内核态来运行用户态的程序Linux内核的khelper就是一个典型的例子。


2. 程序的结束


进程的退出,比较关键的问题是资源的回收。APUE列出了8种进程退出的方式:

a) 从主函数返回
b) 调用 exit
c) 调用 _exit或者_Exit
d) 从最后一个执行线程返回
e) 调用 pthread_exit
f)  调用abort
g) 接收到信号,例如Ctrl+c
h) 最后一个执行线程的cancellation 请求

这八种退出方式可以分成三类,正常返回,线程退出返回,异常返回。进程主动退出唯一的方式就是通过调用exit函数族。


2.1 exit系统调用

exit系统 调用的实现是do_exit函数。do_exit函数定义在kernel/exit.c文件中。do_exit函数主要用于释放进程的代码段和数据段的内存页,向父进程发送子进程退出信号(SIGCHLD),关闭当前进程打开的文件描述符,释放终端设备等资源。



3. 程序的运行环境

系统环境变量是程序运行是可以获取到的系统信息资源,每个系统变量都被保存为一个以NULL结尾的字符串,并且有getenv和setenv函数做存储访问。在windows环境下也有类似的东西。exec系统调用会同时把命令行参数和环境变量写入用户进程的地址空间。


3.1 程序在进程地址空间的内存布局


C程序在编译完成后就划分成了代码段,数据段,BSS段,在运行时又有栈和堆。C程序在进程地址空间的典型布局如图:



3.2 Linux堆栈的形态


Linux系统的堆栈存在四种形态,分别是:
a)系统初始化是建立的临时堆栈,提供给从实模式切换到保护模式后的代码使用,一般情况下,系统启动完成之后就会释放掉。开机log中常见的Free 128KB指的就是这段内存。
b)内核程序自己使用的堆栈,譬如驱动程序,内核代码的函数等都只是用大小一页(4KB)的固定堆栈,并且内核堆栈保存在固定的地址,也不会因为系统内存不存被swap守护进程换出到外存
c)用户进程堆栈,系统处理用户态,理论上虚拟地址空间的内存都可以最为堆栈使用
d)用户进程发起系统调用后,系统切换到内核态,内核系统调用代码使用的堆栈。内核态堆栈和具体的用户进程相关,每个进程都有自己独立的内核态堆栈


3.3 用户态堆栈运行时节奏


代码和数据在C语言中是对立的概念。代码和数据的区别可以理解为编译时和运行时的分界线。编译器的绝大部分工作都跟翻译代码有关,而必要的数据存储管理都是在运行时进行的。一个有经过编译链接后生成的ELF或者a.out文件是没有记录局部变量的值的,也不存在堆栈段,堆栈段是运行时的数据结构。函数是C语言的工作的基本单位,函数的参数和局部变量的操作,函数的调用/返回等行为用符合堆栈这种数据结构的特性。因此理解运行是堆栈的变化过程就是对C语言语境的深刻理解。

一个C函数通常有几个基本要素:参数,auto变量,临时变量,调用子函数,返回到主函数。在进程的地址空间中,堆栈的生长是随着函数调用而生长的。堆栈的生长方向由CPU的体系结构决定。函数的基本要素都保存在堆栈中,每个函数在被调用的过程中都会在堆栈中创建相应的stack frame来保存这些基本要素,返回后,占用的堆栈空间则别释放掉。

活动记录的基本描述如下:

局部变量(local variables)参数(argument)指向主调函数活动记录指针返回地址(return address

一个简单递归调用的函数如下:

a(int i){   if(i>0)      a(--i);   else      printf(....);}main(){  a(1);}

对应的堆栈变化如下:


所以,C语言函数的调用,就像夹带着一个stack frame,从程序的一个地方跳到另外一个地方。而在同一个进程中执行多个不同的线程,则只需要为每个线程分配不同的堆栈即可。