《程序员的自我修养——链接、装载与库》——装载

导读

  可执行文件只有装载到内存以后才能被CPU执行。早期装载的基本过程就是把程序从外部存储器中读取到内存中的某个位置,随着硬件MMU的诞生,多进程、多用户、虚拟存储的操作系统出现以后,可执行文件的装载过程变得非常复杂。现在我们通过ELF文件在linux下的装载过程,来层层拨开迷雾,看看可执行文件装载的本质到底是什么?

目录

  • 进程的虚拟地址空间
  • 装载方式
  • 进程虚拟地址空间的分布
  • Linux内核装载ELF过程简介

正文

1、进程的虚拟地址空间

  我们知道每个程序被运行起来以后,它将拥有自己独立的虚拟地址空间,这个虚拟地址空间的大小由计算机的硬件平台决定,具体的说是由CPU的位数决定的。从程序角度看,我们可以通过判断C语言程序中的指针所占的空间来计算虚拟地址空间的大小。一般来说,C语言指针大小的位数与虚拟空间的位数相同,如32位平台下的指针为32位,即4个字节。现在问题来了,32位平台下的4GB虚拟地址空间,我们的程序是否可以任意使用?很遗憾,不行,因为程序在运行的时处于操作系统的监管下,操作系统为了达到监控程序运行等一系列目的,进程的虚拟地址空间都在操作掌握之中。进程只能使用那些操作系统分配给进程的地址,如果访问未经允许的空间,那么操作系统就会捕获到这些访问,将进程的这种访问当作非法操作,强制结束进程。如:windows下的“进程因非法操作需要关闭”或linux下的“Segmentation fault”很多时候就是因为进程访问未经允许的地址。

  那么到底这4GB的进程虚拟地址空间是怎么样的分配状态呢?以linux操作系统为例子,默认情况下是划分成两部分:1GB分给操作系统,3GB分给进程。当然这种划分是可以修改的。

2、装载方式

  程序执行时所需要的指令和数据必须在内存中才能够正常运行,最简单的办法就是将程序所需要的指令和数据全都装入内存中,这样程序就可以顺利运行,这就是最简单的静态装入的办法。但是很多情况下程序所需要的内存数量大于物理内存数量,当内存的数量不够时,根本的解决办法就是添加内存。但是这种方法不太采纳,毕竟内存是昂贵并且稀有。一般用的多的还是采用动态装入,其原理就是因为程序运行时有局部性,我们将程序最常用的部分驻留在内存,而将那些不太常用的数据存放在磁盘里面。其中两种很典型的动态装载方法就是覆盖装入(Overlay)和页映射(Paging)。

3、进程的虚拟地址空间的分布

  在一个正常的进程中,可执行文件中包含的不止代码段,还有数据段、BSS等,所以映射到进程虚拟空间的往往不止一个段,当段数量增多时,就会产生空间浪费问题,毕竟ELF文件被映射时,是以系统的页长度作为单位的,那么每个段在映射时的长度应该都是系统页长度的整数倍;如果不是,那么多余的部分也将占用一个页。一个ELF文件中往往有十几个段,那么内存的浪费可想而知。

  另外,ELF文件中,段的权限往往只有为数不多的几种组合,基本上分成三种:

  • 以代码段为代表的权限为可读可执行的段
  • 以数据段和BSS段为代表的权限为可读可写的段
  • 以只读数据段为代表的权限为只读的段

  那么我们可以找到一个很简单的方案就是:对于相同权限的段,把它们合并到一起当作一个段进行映射,合并一起之后就就映射到一个VMA。

操作系统通过给进程空间划分出一个个VMA来管理进程的虚拟空间;基本原则是将相同的权限属性的、有相同映像文件的映射成一个VMA;一个进程基本上可分为如下4种VMA区域:

  • 代码VMA:权限只读、可执行;有映像文件
  • 数据VMA:权限可读写、可执行;有映像文件
  • 堆VMA:权限可读写、可执行;无映像文件,匿名,可向上扩展
  • 栈VMA:权限可读写、不可执行;无映像文件,匿名,可向下扩展

4、Linux内核装载ELF过程简介

  当我们在Linux系统的bash下输入一个命令执行某个ELF程序时,Linux系统是怎样装载这个ELF文件并且执行它的呢?

  首先在用户层面,bash进程会调用fork()系统调用创建一个新的进程,然后新的进程调用execve()系统调用执行指定的ELF文件,原先的bash进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。execve()系统调用被定义在unistd.h,它的原型如下:

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

它的三个参数分别是被执行的程序文件名、执行参数和环境变量。

  在进入execve()系统调用之后,Linux内核就开始进行真正的装载工作。在内核中,execve()系统调用相应的入口是sys_ execve(),sys_ execve()进行一些参数检查复制后,调用do_ execve()。do_ execve()会首先查找被执行的文件,如果找到,则读取文件的前128个字节,用心判断文件的格式,因为每种可执行文件的格式的开头几个字节都是很特殊,特别是开头4个字节。比如ELF可执行文件的开头4个字节为0x7F、’e’ 、‘l’、 ‘f’;而Java的可执行文件格式的开头4个字节为’c’ 、‘a’、 ‘f’、 ‘e’;如果被执行的是shell脚本,那么它的第一行往往往是“#!/bin/bash”,这时候,前两个字节’#’和’!’就构成了魔数,系统一旦判断到这两个字节,就对后面的字符串进行解析,以确定具体的解释程序的路径。

  当do_ execve() 读取了这128个字节的文件头部之后,然后调用search_binary_handle()去搜索和匹配合适的可执行文件装载处理过程。Linux中所有被支持的可执行文件格式都有相应的装载处理过程,search_binary_handle()会通过判断文件头部的魔数确定文件的格式,并且调用相应的装载处理过程。比如ELF可执行文件的装载处理过程:load_elf_binary();a.out可执行文件的装载处理过程:load_aout_binary();可执行脚本程序的装载处理过程:load_script()。

  现在我们只关心load_elf_binary(),主要步骤是:

  1)  检查ELF可执行文件格式的有效性,比如魔数、程序头表中段的数量;

  2)  寻找动态链接的“.interp”段,设置动态链接器路径;

  3)  根据ELF可执行文件的程序头表的描述,对ELF文件进行映射,比如代码、数据、只读数据;

  4)  初始化ELF进程环境,比如进程启动时EDX寄存器的地址应该是DT_FINI的地址;

  5)  将系统调用的返回地址修改成ELF可执行文件的入口点,这个入口点取决于程序的链接方式,对于静态链接的ELF可执行文件,这个程序入口就是ELF文件的文件头中e_entry所指的地址;对于动态链接的ELF可执行文件,程序入口点是动态链接器。

  当load_elf_binary()执行完毕,返回至do_execve(),再返回至sys_execve()时,上面的第5步中已经把系统调用的返回地址改成了被装载的ELF程序的入口地址了。所以当sys_execve()系统调用从内核态返回到用户态时,EIP寄存器直接跳转到了ELF程序的入口地址,于是新的程序开始执行,ELF可执行文件装载完成。

时间: 2024-11-11 07:24:08

《程序员的自我修养——链接、装载与库》——装载的相关文章

程序员的自我修养—链接、装载与库pdf

下载地址:网盘下载 内容简介 编辑 <程序员的自我修养:链接.装载与库>对装载.链接和库进行了深入浅出的剖析,并且辅以大量的例子和图表,可以作为计算机软件专业和其他相关专业大学本科高年级学生深入学习系统软件的参考书.同时,还可作为各行业从事软件开发的工程师.研究人员以及其他对系统软件实现机制和技术感兴趣者的自学教材. 媒体评论 编辑 这是一本深人阐述链接.装载和库等问题的优秀图书,读来让人愉悦,你从巾可以清晰地了解程序的前世今生,彻底理解敲人的代码如何变成程序任系统中运行.通读本书不管对于开发

程序员的自我修养-链接、装载与库-7 动态链接

动态链接 静态链接的好处:使得不同部门的开发者能够相对独立的开发和测试自己的程序模块,促进了开发效率,原先限制程序的规模也随之扩大. 缺点:浪费内存空间和磁盘空间,模块更新困难 种种罪行: 空间浪费:想想一下每个程序内部除了printf, scanf, strlen等公用库函数,还有非常多的其他库函数以及他们所需的辅助数据结构.在Linux中一个普通的c程序需要的静态库至少1MB以上. 简而言之就是,相同的目标模块每一个程序都会保留一份obj文件. 更新困难:一旦程序中有任何模块更新,整个程序都

《程序员的自我修养——链接、装载与库》——链接

导读 对于平常的应用程序开发,我们很少需要关注编译和链接过程,因为通常的开发环境都是流行的集成开发环境(IDE),比如Visual Studio.Myeclipse等.这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并在一起的过程称为构建,即使使用命令行来编译一个源代码文件,简单的一句”gcc hello.c”命令就包含了非常复杂的过程.然而,正是因为集成开发环境的强大,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我

程序员的自我修养-链接、装载与库-6 可执行文件的装载与进程

可执行文件的装载与进程 可执行文件只有装载到内存后才能被CPU执行.基本过程就是把程序从外部存储器中读取到内存中的某个位置. 程序(可执行文件)是一个静态的概念.就是一些预编译好的指令和数据组成的一个文件.进程则是一个动态的概念.很多时候,把动态库叫作运行时. 每个程序在执行时,都拥有自己独立的 虚拟地址空间. 虽然,内存中的虚拟地址空间很大,但是任意一个程序都必须在OS的监督下使用资源,不能任意访问或者占用内存.大多数时候我们在Windows下碰到的"进程因非法操作需要关闭" 和 L

程序员的自我修养-链接、装载与库-2

第二部分 静态链接 被隐藏了的过程:预处理.编译.汇编.链接(Build过程 在IDE中) 预编译: 源代码hello.cpp和相关头文件(stdio.h)被预编译器cppp预编译成一个.i文件. 预编译命令: gcc -E hello.c -o hello.i 或者 cpp hello.c > hello.i 过程:预编译主要处理那些源代码中的以'#'开始的预编译指令.比如"#include" "#define"等. 包括: 展开宏定义: 处理所有条件预编译

读《程序员的自我修养 —— 装载与动态链接》乱摘

2016.05.14 – <程序员的自我修养 -- 链接.装载与库>的装载与动态链接部分. - 余甲子 石凡 潘爱民编 个人选读笔记 - 学点表皮. 05.14 PART II 装载与动态链接 1 可执行文件的装载与进程 1.1 进程虚拟地址空间的大小 每个进程拥有自己独立的虚拟地址空间,该虚拟地址空间的大小由计算机的硬件平台决定,具体地说是由CPU的位数决定的(地址线 -- C语言中的指针所占空间).硬件决定了地址空间的最大理论上限,即硬件的寻址空间大小,如32位的硬件平台决定了虚拟地址空间

《程序员的自我修养》 第二章——编译和链接

摘自http://blog.chinaunix.net/uid-26548237-id-3839979.html <程序员的自我修养>第二章——编译和链接 2.1 被隐藏了的过程    C语句的经典,“Hello World”程序几乎是每个程序员闭着眼睛都能写出的,编译运行一气呵成,基本成了程序入门和开发环境测试的默认标准. #include <stdio.h> int main() { printf("Hello World\n"); return 0; 在L

程序员的自我修养:(1)目标文件

程序员的自我修养:(1)目标文件 1.目标文件 1.1 编译与链接 在使用像Visual Studio或Qt Creator等IDE时,通常有一个叫做"构建"的按钮.当编辑完成要运行和测试时点一下它,程序就能跑起来了,所以我们很少关心编译和链接.其实,编译和链接合并在一起就称为 构建(Build).简单的一次按键,实际背后却是异常复杂的过程: 预编译(Preprocessing) 编译(Compilation) 扫描:算法类似有限状态机(FSM),将字符转换成Token. 语法分析:分

读《程序员的自我修养》感受

这书不错,链接-装载-库 我觉得是很底层的东西.比如很多人闭着眼睛都能写出来的hello world(当然不包括brianfuck,如果你会,你真的闹残了吗= =), 其实链接编译器做了很多,不然就哪来的printf,这IO初始化也是CRT(c runtime)库完成的.堆栈的初始化,还有系统装载让程序运行等等.涉及很多. 书里后面就讲了一个CRT库,自己写一个,感觉不错,学了很多.比如malloc,free的实现,话说还是跨平台的.当然库很小,功能不多,不过写这个也可以学学算法.内存的分配,这