程序加载与运行过程

开发者福利!热门AI工具限时免费用 购周边即赠Coding Plan Lite,Claude Code、Cursor等20+工具畅享,效率翻倍! 阅读详情

当我们在shell环境中输入ls命令时程序是怎么运行起来了呢?在底层做了哪些事情才输出了我们想要的结果?我们通过对程序的加载与运行过程分析来梳理整个流程,让我们对linux程序的运行过程理解更透彻。

整体流程

  1. 当我们在bash下输入ls命令时,bash进程首先通过fork系统调用创建子进程。如果我们不做任何改变,fork出的子进程将执行父进程后面的代码。
  2. 在子进程中我们调用execve系统调用执行我们想要执行的ls进程。原先的bash进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。

execve系统调用流程

execve函数的形式如下:

int execve(const char *filename, char *const argv[], char *const envp[]);

 它一共有三个参数,分别是需要执行程序的文件名、执行参数和环境变量。进入execve系统调用后,linux内核就开始进行真正的装载工作。

execve系统调用的函数调用流程如下:

execve--->sys_exeve--->do_execve--->search_binary_handle--->load_elf_binary
  • 在do_execve函数中,首先读取可执行文件的头结构,即前128字节。通过该头结构可判断可执行文件的格式,毕竟linux支持多种格式可执行文件。

elf文件的头4个字节分别为0x4f、e、l、f,如下:

  • 根据获取的可执行文件格式,调用search函数获取该可执行文件对应的状态处理接口。如elf格式文件的装载处理接口为load_elf_binary。

load_elf_binary函数的主要步骤如下:

  1. 检查elf可执行文件格式的有效性,如魔数、程序头中段的数量等。
  2. 解析.interp段获取动态链接器的位置,设置动态链接器的路径。
  3. 根据可执行文件的程序头表的描述,对elf文件进行映射,如代码、数据、只读数据。
  4. 初始化elf进程环境。
  5. 将系统调用的返回地址修改为可执行文件的入口地址,移交cpu的执行权。针对可执行文件类型的不同,其入口地址有两类。
  • 若elf可执行文件为静态链接可执行文件,则入口地址即为可执行文件中头结构的entry point address指向地址(即为可执行文件中的_start函数)。

elf文件头结构entry point address:

 入口地址0x08048615对应的函数_start:

  • 若可执行文件为动态链接可执行文件,入口地址为动态链接器。

可执行文件的.interp段中保存了动态链接器的路径,如下:

当load_elf_binary执行完毕,依次返回至do_execve、sys_execve时,由于上面的步骤5已经将系统调用的返回地址修改为可执行文件的入口地址,因此当完成系统调用从内核态返回到用户态时,cpu将执行elf程序的入口地址,即开始执行新程序,至此elf可执行文件的装载完成。

execve流程总结如下:

从操作系统角度看可执行文件装载

从操作系统角度看可执行文件的加载,该过程主要分如下三步:

  1. 创建进程的虚拟地址空间。通常就是创建进程的页表,此时还不需要建立虚拟内存到物理内存的映射,到真正使用时再建立映射关系。
  2. 根据elf可执行文件的程序头(program header)建立可执行文件到虚拟内存的映射,这种映射关系保存到进程task_struct结构体中的vma_area_struct结构中。每一段虚拟内存段称为虚拟内存区域(VMA,virtual memory area)。这一步也是真正意义上的装载。              相关结构体层次关系:                                                                                                                     task_struct(描述进程)                                                                                                           --->  mm_struct(描述进程的用户态内存空间,包括代码段、数据段位置等信息)                 ---> vma_area_struct(描述一个虚拟内存段,包括该虚拟内存段的起始地址、大小,以及对应的文件和文件中偏移。mmap时通常需要创建一个独立的vma_area_struct结构进行映射)                                                                                                                                            只有完成了这种elf文件到虚拟内存的映射关系,当程序运行发生缺页异常时,陷入内核后操作系统才能根据这种映射关系找到该地址对应的具体elf文件位置,这样内核分配了物理内存后将该部分文件读取到内存中,并建立虚拟内存到对应物理内存的映射关系。最后返回到用户空间后程序才能继续执行下去。
  3. 将CPU的指令寄存器设置为elf可执行文件的入口地址,即操作系统将控制权交给可执行文件的入口,开始运行新的程序。

操作系统角度看可执行文件加载流程总结如下:

动态链接器链接步骤

针对动态链接的可执行文件,其运行时需要动态链接器先帮忙完成依赖的动态库加载与重定位,这样该可执行程序才能开始运行。动态链接器的链接步骤如下:

  1. 实现动态链接器自举,完成动态链接器自身的全局变量和静态变量重定位工作。
  2. 根据可执行文件的.dynamic段递归获取所有依赖的动态库,并将这些动态库映射到虚拟地址空间,将它们的动态符号表合并到全局符号表。(类似于静态链接的“空间与地址分配”)
  3. 完成引用的外部符号的解析与重定位,根据动态库的.init段进行初始化,如C++中的全局/静态对象的构造函数。类似的.finit段中代码用来实现C++全局对象的析构操作。(这一步类似于静态链接的“符号解析与重定位”)
  4. 完成了上面3步后,动态链接器将控制权交给elf可执行文件的入口,即elf可执行文件头结构的Entry point address地址。(该地址实际指向_start函数)

动态链接器的链接步骤总结如下:

进程自身执行过程

当历经千辛万苦终于将控制权转交到elf可执行文件的入口地址后,可执行程序终于要开始正式运行了。在glibc中,elf可执行程序的入口地址为_start函数。这个入口地址由ld连接器的默认脚本指定,我们可以通过相关参数进行设置。

_start函数由汇编实现,其主要完成了_libc_start_main函数的调用。__libc_start_main函数的形式如下:

 该函数总共有7个参数,含义分别如下:

  • 参数main:即我们通常写的main函数入口地址。
  • 参数argc:main函数的参数个数。
  • 参数argv:main函数的参数列表。main函数的环境变量参数列表紧跟其后,因此可以通过argv顺带获取。
  • 参数init:执行main函数前调用,用于完成一些初始化工作,实际填充的是__libc_csu_init函数。如执行调用C++的构造函数、声明为__attribute((constructor))类型的构造函数。
  • 参数fini:执行完main函数调用,用于完成一些结束收尾工作,时机填充的是__libc_csu_fini函数。如执行调用C++的析构函数、声明为__attribute((destructor))类型的析构函数。
  • 参数rtld_fini:和动态加载相关的收尾工作。该函数和fini函数一样在main函数执行完后调用。rtld是runtime loader缩写。(即为动态链接器的析构函数)
  • stack_end:栈底地址,即栈的最高地址(sp寄存器)。

__libc_start_main函数的主要执行流程如下:

  1. 调用__cxa_atexit函数(等同于atexit函数)将rtld_fini函数添加到对应链表中,用于在exit函数中调用。(main函数结束后会调用exit函数)
  2. 执行相关运行环境的初始化工作,如堆、I/O、线程等等。
  3. 调用__cxa_atexit函数将fini函数添加到对应链表中,用于在exit函数中调用(main函数结束后会调用exit函数)
  4. 调用init函数执行进程相关的构造函数。
  5. 调用main函数,执行用户的流程。
  6. 在main函数执行完后调用exit函数。在exit函数中逐个调用所有相关的析构函数,如上面步骤1、3通过__cxa_atexit函数注册的rtld_fini、fini函数。

相关函数调用流程如下:

elf可执行文件进程自身执行过程总结如下:

 

 

【Educoder作业】冯·诺依曼体系结构及工作原理理解 [Educoder作业]冯·诺依曼体系结构及工作原理理解 所有的那个实际场景模拟的题就不更了,没难度,趣味性更高。 这个题和我们之前的一篇博客里面的题只一模一样的,链接。 我们这里就不做任何讲解了,要说的那一篇里已经说过了。几天过去,有些程序的实现现在看来有些冗余,这个博客就做一个代码的精简。 T6 软件模拟 - 模拟硬件初始化 #初始化 def init(): ########## Begin ########## mem = [0] * 1000 #主存,1000个单元 阅读详情

相关推荐

操作系统作业2-进程控制

1 2. 3. 思考题: (1)系统是怎样创建进程的? 1.申请空白PCB(进程控制块); 2.为新进程分配资源; 3.初始化PCB; 4.就新进程插入就绪队列; (2)当首次调用新创建进程时,其入口在哪里? fork()函数被调用一次,但返回两次;两次返回区别在于:子程序返回值是0,而父进程返回值是子进程的ID。子进程和父进程运行相同的代码,但是有自己的数据空间。 (3)程序的多次运行结果为什么不同?如何控制实验结果的随机性? 多个进程的并发执行,每个进程都有先获取cpu的可能性,故而哪一个进程先执行是随

Felinus_lll的博客 1720

第七课 程序加载(5)

gdb的实验跟踪过称: 查看hello的入口地址:readelf -h hello输出图片为: 他的入口地址为 0x8048d0a使用dgb跟踪start_kernel函数的时候,指明了入参中new_ip值为: 由跟踪值和分析值可知两者是一致的。

zhaolianxun1987的专栏 500

程序加载:CPU执行前的必要步骤

需要注意的是,实际的程序加载过程可能更加复杂,涉及到操作系统、编程语言和硬件架构等多个层面的细节。需要注意的是,实际的程序加载过程可能更加复杂,涉及到操作系统、编程语言和硬件架构等多个层面的细节。在计算机编程中,程序加载是指将程序从存储介质(如硬盘或闪存)加载到计算机的主内存中,以便被CPU执行的过程。解析和重定位:在加载程序代码时,计算机系统需要解析程序中的符号引用,并进行适当的重定位。解析和重定位:在加载程序代码时,计算机系统需要解析程序中的符号引用,并进行适当的重定位。

2301_79331230的博客 482

深入理解计算机系统_程序加载过程运行过程

可执行目标文件的加载运行过程

weixin_43916755的博客 2420

程序加载运行过程中的资源分配管理

程序加载 程序的内存空间 程序入口地址 BSS段初始化 程序运行过程中的堆栈管理 栈内存管理 变量的作用域: 栈溢出攻击原理 Linux堆内存管理 查看进程内存布局 内存分配器 内存块合并 top chunk

Afra 985

Java的加载执行原理详解 Java程序从编写到最终运行经历了哪些过程

前言 Java程序从编写到最终运行大概可概括为3个阶段:编写、编译、运行阶段。 一、编写阶段 程序员在硬盘某个位置新建一个xxx.java文件 使用记事本或者其他文本编辑器例如EditPlus打开xxx.java文件 在xxx.java文件中编写符合Java语法规则的源代码 保存源代码 二、编译阶段 使用编译器(javac【JDK安装后自带】)将 xxx.java文件编译为 xxx.class文件。如果 xxx.java文件中编写的源代码是符合语法规则的,编译会通过并生成xxx.class字节码文件;

二十四日的博客 1078

java文件加载运行过程_Java程序的开发过程运行过程

原标题:Java程序的开发过程运行过程图1-9:Java的加载执行 ● Java程序从开发到运行包括编译和运行两个阶段,这两个阶段可以在不同的操作系统中完成,例如在windows环境下进行编译,在Linux环境下运行,这是因为有JVM机制的存在,做到了一次编译到处运行(跨平台/可移植)。● 编译阶段需要使用javac.exe(安装JDK之后该命令就存在了)命令,运行阶段需要使用java.exe...

weixin_31319027的博客 150

运行Java程序后,都发生了什么?

当一个字节码文件在Java命令执行之后运行时,涉及到两个主要方面的过程加载和执行。

weixin_75202470的博客 383

Linux程序加载运行和终止流程

详细解释Linux程序运行流程,包括程序加载、动态连接器的加载和共享库的加载、符号解析和重定位、初始化和终止。

8059

【汇编语言】第一个程序(四)—— 谁在幕后启动程序 : 探讨可执行文件的装载执行

在本文中我们探讨了一个汇编源程序形成的可执行程序,是如何转载到内存并运行

2301_80191662的博客 1417

Java程序编译和运行过程

转载:https://www.cnblogs.com/qiumingcheng/p/5398610.html ava程序编译和运行过程 Java整个编译以及运行过程相当繁琐,本文通过一个简单的程序来简单的说明整个流程。 如下图,Java程序从源文件创建到程序运行要经过两大步骤:1、源文件由编译器编译成字节码(ByteCode) 2、...

u012102536的博客 554

java 程序编译和运行过程详解

java整个编译以及运行过程相当繁琐,我就举一个简单的例子说明: Java程序从源文件创建到程序运行要经过两大步骤: 1、源文件由编译器编译成字节码(ByteCode);  2、字节码由java虚拟机解释运行。因为java程序既要编译同时也要经过JVM的解释运行,所以说Java被称为半解释语言 ( "semi-interpreted" language)

蜜蜂采蜜 3万+

绝对干货(软件程序运行过程

如果你的电脑上安装了QQ,你希望和好友聊天,会双击QQ图标,打开QQ软件,输入账号和密码,然后登录就可以了。 那么,QQ是怎么运行起来的呢?首先,有一点你要明确,你安装的QQ软件是保存在硬盘中的。双击QQ图标,操作系统就会知道你要运行这个软件,它会在硬盘中找到你安装的QQ软件,将数据(安装的软件本质上就是很多数据的集合)复制到内存。对!就是复制到内存!QQ不是在硬盘中运行的,而是在内存中运行的。...

weixin_33998125的博客 639

深入分析arm的程序启动过程内存分配和加载区域运行区域的

ZI-data:Zero Initialie data,即0初始化数据,它指初始化为"0值"的可读写数据域,它RW-data的区别是程序运行时这些数据初始值全都为0,而后续运行过程RW-data的性质一样,它们也常驻在RAM区,因而应用程序可以更改其内容。这4个程序状态区域中,Code、RO和RW是存储在Flash中的,因为RW,例如初始化非零的全局变量,确保每次上电是相同的,所以也存储在Flash中,而ZI和堆栈初始化是0的,就没必要存储在Flash中。这就涉及到程序加载时域和运行时域。

DSCjxgzzd的博客 1305

java程序运行过程以及JDK、JRE、JVM的区别联系

本文讲述了java程序运行过程以及JDK、JRE、JVM的区别联系

SSS4362的博客 458

Java程序运行机制及其运行过程

java两种核心机制:java虚拟机跟垃圾回收机制。本文主要讲的是jvm运行java程序。 (一)终端中如何运行一个java程序(这个是我在mac下运行的,windows下原理是一样的,大同小异) 做这个事情的前提下,一定是jdk已经安装好了并且没任何问题。 首先要想运行java类,应先有个java类 1.创建个名为java的文件夹,在文件夹下面创建个以.java结尾的文件(我是用sublime编辑的,其他编辑器也行),这儿以HelloWorld为例 2.通过命令编译文件 192:libe

zl17822307869的博客 167

【编程技术】程序从编码到加载运行过程

链接器核心任务:符号解析:确保所有函数和变量有定义。地址重定位:调整代码中的跳转地址和数据地址。段合并:将多个目标文件的.text(代码段)、.data(数据段)合并。

weixin_43510208的博客 864

Java程序编译和运行过程

    Java整个编译以及运行过程相当繁琐,本文通过一个简单的程序来简单的说明整个流程。                  如下图,Java程序从源文件创建到程序运行要经过两大步骤:1、源文件由编译器编译成字节码(ByteCode)  2、字节码由java虚拟机解释运行。因为java程序既要编译同时也要经过JVM的解释运行,所以说Java被称为半解释语言( "semi-interpreted...

程序狗的成长之路 463
上一篇: linux系统下常见数据类型字节数
下一篇: 疑难问题定位案例复盘(一)
sy4331
博客等级 码龄12年 7粉丝 40原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值