揭秘程序背后的故事1

Nov 25, 2017


要点

  • 源程序到目标程序需要经过哪些步骤
  • 目标程序又该如何变成可执行文件
  • 可执行文件如何演变成操作系统中的进程

应用程序和操作系统、编译程序的关系


  1. 首先操作系统属于系统层面的软件,应用程序属于应用软件,建立在操作系统上的程序

  2. 操作系统是管理硬件资源和软件资源的程序(进程调度来使用cpu资源、当多个进程竞争同个硬件时进行控制),有效使用计算机资源,提高系统吞吐量和效率

  3. 内核是实现驱动程序(控制硬件设备,例如网络设备,存储设备等硬件)提供接口的实现,相应的硬件需要对应的驱动程序进行控制,并且驱动程序给操作系统提供访问硬件的接口。简单说就是操作系统可以通过内核操作硬件,而应用程序可以通过调用系统接口进行控制硬件,其中底层驱动接口对应用是透明的。

  4. 编译程序也属于系统程序建立在操作系统上,是将高级语言编译成低级语言(机器语言),和其他程序不一样的是编译程序会根据cpu型号,译成对应的cpu指令,有关于编译程序和操作系统的关系:(其中也会根据操作系统将应用程序对系统调用译成对应需要调用操作系统的指令,其中操作系统自己的指令也在内存中,可以被cpu获取到),可以看出 应用程序仅仅是计算指令的话那么就很少依赖于操作系统,如果是系统的调用,例如读取socket中的数据就需要强依赖于操作系统,一开始是用户态(cpu执行应用程序指令)然后调用了系统接口然后转到内核态(cpu执行操作系统指令),用户态会一直阻塞直到内核获取到数据后返回到用户态。

从程序到进程的流程图


下面就是以C语言为例: 其中包括编译和汇编、链接、加载这几个过程,将高级语言编译成中间语言(汇编程序)然后汇编成二进制机器代码,最后被加载器加载到内存中。

  1. 编译器:由于cpu等硬件都只能识别0101二进制码,所以可以大胆的猜想出编译器天生就和cpu有着千丝万缕的关系,同时由于编译器是系统程序,也和操作系统有着后天的关系。
  2. 目标文件: 里面是一个由二进制组成的文件,大概分成代码段和数据段,其中都是以一个模块或方法中程序的地址都是以 模块(函数)名为基准的逻辑地址 例如 int x = 1 ,y=1 a(){ return x+y} 将变量名x替换成地址编号(给1分配的)0x1002,例如cpu需要变量a的数据则拿到这个0x1002地址就可以获取到,y对应于0x1003 然后方法符号a对应的地址为方法入口地址 0x0000那么肯定需要 需要load指令 add指令,这些指令的地址都是以0x0000递增分配的,然后 x和y应该对应于 0x1002和0x1003

props

编译过程


1、 编译

编译、优化阶段,编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码

2、汇编

汇编实际上指汇编器(as)把汇编语言代码翻译成目标机器指令的过程。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段:

  • 代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
  • 数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

3、 目标文件(Executable and Linkable Format)

  • 可重定位(Relocatable)文件:由编译器和汇编器生成,可以与其他可重定位目标文件合并创建一个可执行或共享的目标文件;
  • 共享(Shared)目标文件:一类特殊的可重定位目标文件,可以在链接(静态共享库)时加入目标文件或加载时或运行时(动态共享库)被动态的加载到内存并执行;
  • 可执行(Executable)文件:由链接器生成,可以直接通过加载器加载到内存中充当进程执行的文件。 以下是ELF目标文件的结构props

4、 静态库和动态库

  • 静态库(static library)就是将相关的目标模块打包形成的单独的文件。使用ar命令。

静态库的优点在于

1、 程序员不需要显式的指定所有需要链接的目标模块,因为指定是一个耗时且容易出错的过程;

2、 这样就减小了可执行文件在磁盘和内存中的大小。(相比自己将库函数嵌入到源程序)

  • 动态库(dynamic library)是一种特殊的目标模块,它可以在运行时被加载到任意的内存地址,或者是与任意的程序进行链接。

    动态库的优点在于:

1、更新动态库,无需重新链接;对于大系统,重新链接是一个非常耗时的过程;

2、运行中可供多个程序使用,内存中只需要有一份,节省内存

链接过程


以下属于静态链接过程如图: props

1、静态链接

在执行前链接阶段,就会根据目标文件中引用其中模块或库文件,符号路径或到 /lib /usr/lib /usr/local/lib中搜索到对应库文件(目标文件),然后嵌入到当前的目标文件中。其中代码段合并,数据段也会进行合并 因此对应符号表中符号定义地址需要改变,同时更新引用外部的符号为对应的地址

  • 符号解析:将目标文件中的每个符号的引用刚好和一个符号定义联系起来
  • 重定位:把符号定义和地址对应起来,然后修改所有对符号的引用为对应的虚拟内存的地址

其中缺点是:当库文件改变时,需要重新链接目标文件,其中非常耗时

2、 装入时动态链接

在此种方式下,函数的定义在动态链接库或共享对象的目标文件中。在编译的链接阶段,动态链接库只提供符号表和其他少量信息用于保证所有符号引用都有定义,保证编译顺利通过 其中装入链接是在加载可执行文件时进行的,如果已经被另外一个程序加载了,则直接使用,否则根据目标文件记录的共享对象的符号定义来加载共享库,然后完成重定位(就是将引用该符号的定位替换成对应的地址) 在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间,动态程序根据可执行程序调用执行相应方法代码

其中优点:保证了程序的共用醒,减少了磁盘和内存空间,当共享对象改变时无需重新链接

3、运行时动态链接

在运行的时候,调用了某个共享对象的某个方法时,进行链接对应的共享对象到内存中,例如报警模块,其中只有等到有报警时才会执行,否则不执行。

其中优点:加快了加载速度,提高了程序执行效率,节省大量内存空间

加载过程


其中将硬盘中的程序加载(拷贝)到内存中,然后形成操作系统中的进程的过程

  • 绝对装入方式

就是在编译链接的时候将代码和数据对应的地址写成内存的地址,其中需要对系统资源情况和内存结构十分了解

  • 可重定位装入方式

由于可执行程序中的地址时逻辑地址,也就是在该执行文件中有效并唯一,当装入到内存中,会根据当时内存使用情况,分配到适当的空间,其中会将部分指令地址修改成 BA(装入内存首地址)+VA(逻辑地址) = MA(实际内存的地址),其中缺点是程序不能在内存中移动,由于指令地址已经写成了内存物理地址,如果移动,导致指令寻址混乱

  • 动态运行时装入方式

此时需要借助于 基址寄存器 BR 和 逻辑基存器 VR、物理地址寄存器 MR。其中加载的可以执行程序逻辑地址不变,其中加载到内存中,首先操作系统会将内存首址存放在BR中,当cpu获取指令地址时 执行要将 逻辑地址放入VR最后相加结果放入到MR中。其中好处时可以任意移动程序段(只需改变BR地址就行),同时也可以加载部分程序段(为虚拟存储技术打下基础)

其中以下为Linux进程运行时的内存映像

props

加载器跳转到程序入口点(即符号_start 的地址),执行启动代码(startup code),启动代码的调用顺序如所示:

props

处理目标的常用工具


UNIX系统提供了一系列工具帮助理解和处理目标文件。GNUbinutils 包也提供了很多帮助。这些工具包括:

  • AR :创建静态库,插入、删除、列出和提取成员;
  • STRINGS :列出目标文件中所有可以打印的字符串;
  • STRIP :从目标文件中删除符号表信息;
  • NM :列出目标文件符号表中定义的符号;
  • SIZE :列出目标文件中节的名字和大小;
  • READELF :显示一个目标文件的完整结构,包括ELF 头中编码的所有信息。
  • OBJDUMP :显示目标文件的所有信息,最有用的功能是反汇编.text节中的二进制指令。
  • LDD :列出可执行文件在运行时需要的共享库。