编译 链接 加载

本篇文章是组内分享的小结,主要介绍源代码 -> 可执行程序 -> 执行这一过程。也就是源代码是如何转化为可执行程序,然后可执行程序又是如何执行的。在用java或python时,只需要java ClsName或者python a.py就可以执行相应的程序,实际上它们都是依托于底层的虚拟机。本文主要介绍的是操作系统级别的连接、加载、执行等,而不是虚拟机语言的执行。这里只对链接、加载进行一个简介,详细内容推荐大家去看《深入理解计算机系统》《程序员的自我修养》,第二本要比第一本讲的更加详细,但稍显啰嗦,如果只是了解建议阅读第一本的第七章。

先看两个示例程序,后续会以它们为例:

[cpp] view plaincopyprint?

  1. // foo.c
  2. #include <stdio.h>
  3. int a = 10;
  4. int b;
  5. void bar(int c);
  6. int
  7. main(){
  8. bar(a);
  9. printf("...");
  10. }

[cpp] view plaincopyprint?

  1. // bar.c
  2. void bar(int c){
  3. // ...
  4. }

通常c程序是由多个模块组成的,每个模块对应一个c文件,会被编译成可连接目标文件,然后由连接器将所有的模块组合成一个可执行程序。可以通过下面命令完成编译动作:

[plain] view plaincopyprint?

  1. > gcc -c foo.c

编译之后当前目录会生成foo.o,就是对应的可连接目标文件。实际上由源代码转化成目标文件是由多个步骤组成的:

预处理(cpp):完成宏替换、文件引入,以及去除空行、注释等为词法分析准备。

编译(cc):将预处理后的代码编译成汇编代码,由于加入了汇编器这一层,隔离了底层硬件的不同实现,提高了移植性。

汇编(as):将汇编代码转化成机器码,也就是01序列。

我们知道,一个程序是由代码和数据组成的,目标文件必须以某种方式组织这些信息,以便链接器和加载器从文件中去识别相应的信息。在Linux下,目标文件的格式是ELF(Executable Linkable Format),可以用来描述可链接目标文件、可执行目标文件盒共享目标文件。下面就来看看可链接目标文件中主要包含什么内容。目标文件以节(Section)组织数据,同时具有一个节头部表(Section Header)用来描述所有的节。主要的节包括:

.data:已初始化的全局变量和静态局局变量。foo.c中的全局变量a就是存在.data节中。

.bss:未初始化的全局变量和静态局部变量,这个节在载入内存时会被清0,所以未初始化的全局变量和静态局部变量默认值是0。foo.c中的全局变量b存在.bss节。

.text:编译后的机器代码。所有的函数编译后的二进制代码会存在.text节中,比如main函数。

.string:用来存储目标文件中用到的字符串以及字符串常量。

.symtab:符号表。符号就是目标文件中的全局变量和函数,符号表描述目标文件中的所有符号,这个是链接器进行链接的基础。符号分为:

导入符号:当前模块引用其他模块中定义的符号,比如:在foo.c中使用的bar.c中定义的bar函数,那么foo.o的符号表就包含导入符号bar。

导出符号:就是当前模块定义的符号,可以被其他模块引用。这些导出符号就是模块中定义的初始化的全局变量和非静态函数。

目标文件中其实还有很多个节,这里只介绍上面几个主要的节。

多个c文件分别编译成可链接的目标文件后,要生成可执行文件那么还需要进行链接。链接就是解决多个模块的引用和库调用,然后进行重定位以便生成可执行文件。链接过程最重要的就是符号解析,就是将模块中的导入符号找到其定义的地方,然后将符号替换为指针。

在链接时,符号可以分为强符号和弱符号:

强符号:就是初始化的全局变量和非静态函数。比如,foo.c中的全局变量a和函数main以及bar.c中的函数bar。

弱符号:未初始化的全局变量。比如,foo.c中的全局变量b。

链接时,如果遇到重名的强符号(比如在foo.c和bar.c中都定义了int a = 1;),会报错“duplicated symbols”,具体名称记不清了。如果遇到重名的弱符号,链接的行为取决于具体实现,这里不再深入讨论。

链接器需要把多个可链接目标文件组合形成一个可执行目标文件,它会收集各个模块中相同类型的节然后组成可执行文件的对应的节,比如:收集foo.o和bar.o的.data节,然后合并在一起组成可执行文件的.data节。链接器还需要完成重定位,因为在合并节时,原来模块节的地址会改变,所以重定位就是修改模块中指针的地址。

完成连接之后,在磁盘上就会生成可执行目标文件。要执行一个程序时,必须要把可执行目标文件载入内存。我们知道,进程是程序执行的容器,每个运行的程序都有自己的内存地址空间,需要将可执行目标文件中数据和代码节载入到进程的地址空间。下面看一下进程的地址空间:

每个进程都有自己私有的虚拟内存地址空间,在32bit机器上,地址空间的大小是4GB,高地址的1GB的内存空间被映射为内核空间,用于提供内核服务。用户栈就是函数调用栈用于实现函数调用,在栈上为局部变量分配空间。共享库用于实现类似C标准库的代码和数据。堆用于动态内存分配。剩下的数据区和代码区是与可执行文件相关的,需要从磁盘加载。

加载器载入可执行目标文件时需要虚拟存储器的支持,通过mmap的文件映射方式将可执行目标文件中的.data节和.bass节映射到进程地址空间中的数据区,将.text节映射到代码区。栈和堆采用的是mmap的匿名映射,也就是没有提供文件参数。地址空间中的每个被占用的区域就是一个VMA(Virtual Memory Area),这些VMA会通过链表和红黑树组织起来,采用链表是为了便于顺序遍历,红黑树是为了根据地址快速检索到对应的VMA。当我们通过一个地址p访问内存时,os会进行地址合法性检查,第一必须保证p包含在某个VMA中;第二对于每个VMA都有一个读、写和执行的权限,进程必须具备相应的权限才能执行操作。如果不满足上述两点,就会抛出“Segment Fault”错误。

在完成映射之后,接下来开始执行程序,首先执行的是_start函数这个是属于glibc的库函数,完成程序的初始化,为程序的运行准备,接下来就会调用main函数,这是通过符号表完成main函数入口的定位。经过mmap实际上文件并没有载入内存,当第一次访问时会从磁盘加载对应的内容,这是由虚拟存储器机制完成的,对进程是透明的。

用户栈中的元素就是一个个函数调用对应的栈帧,当前栈帧有CPU寄存器%esp和%ebp标识,%esp是栈指针指向栈顶,%ebp是帧指针,在%esp和%ebp之间的区域就是当前函数对应的栈帧。栈帧存放的是函数的实参已经局部变量。

运行时堆是动态内存分配的区域,指针sbrk指向堆顶,可以通过改变sbrk进行动态内存的分配和释放。C标准库中的malloc和free底层就是基于sbrk指针,当然我们也可以通过sbrk实现内存分配器,不过还要设计自己的内存分配算法,通常建议还是使用标准库进行内存分配。

上面就是小组分享的全部内容,由于完全裸讲,没有充分准备,想到哪讲到哪,所以难免会有纰漏,见谅啊~~

时间: 2024-10-13 06:44:13

编译 链接 加载的相关文章

AutoSharedLibrary -- 基于模板元编程技术的跨平台C++动态链接加载库

基于模板元编程技术的跨平台C++动态链接加载库.通过模板技术,使用者仅需通过简单的宏,即可使编译器在编译期自动生成加载动态链接库导出符号的代码,无任何额外的运行时开销. ASL_LIBRARY_BEGIN(TestLib) ASL_SYMBOL(Proc_test1, test1, false) ASL_SYMBOL(Proc_test2, test2, true) ASL_LIBRARY_END() TestLib theLib; try { theLib.Load("./1.so"

链接加载文件gcc __attribute__ section

在阅读源代码的过程中,发现一个头文件有引用: /** The address of the first device table entry. */ extern device_t devices[]; /** The address after the last device table entry. */ extern device_t devices_end[]; /** The address of the first "driver_t". */ extern driver_

jvm内存模型,java类从编译到加载到执行的过程,jvm内存分配过程

一.jvm内存模型 JVM 内存模型主要分为堆.程序计数器.方法区.虚拟机栈和本地方法栈 1.堆 1.1.堆是 JVM 内存中最大的一块内存空间. 1.2.该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中. 1.3.堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成. 2.程序计数器(Program Counter Register) 程序计数器是一块很小的内存

Java--自定义Class并且在内存中编译,加载,实例化

本文的目的: 使用者在程序运行期间,可以动态的写Java Class,不需要生成任何.Class文件就可以完全在内存中编译,加载,实例化. 1.需要用到的组件介绍 1)JavaCompiler:用于编译Java Code. 2)CharSequenceJavaFileObject:用于保存Java Code,提供方法给JavaCompiler获取String形式的Java Code. 3)ClassFileManager:用于JavaCompiler将编译好后的Class文件保存在指定对象中.

简单的Linux 驱动模块编译,加载过程

简单的Linux 驱动模块编译,加载过程 2010-03-14 14:48:24|  分类: Driver |  标签: |字号大中小 订阅 本文记录我的第一个Linux设备驱动程序的编译过程.遇到问题的解决方法. 环境:2.4.18-14的内核,Linux内核源码:2.4.18.       Linux内核源码路径:/usr/src/linux(这个源码是从kernel.org网站download的2.4.18版本)        按照<linux设备驱动开发详解>一书中的步骤实现经典例子&

commonJs的运行时加载和es6的编译时加载

参考 : https://www.cnblogs.com/jerrypig/p/8145206.html 1.commonJs的运行时加载 2.ES6编译时加载 原文地址:https://www.cnblogs.com/wfblog/p/9589934.html

链接,加载,装载(二)

编译驱动程序(compiler driver) 这代表用户在需要时调用语言预处理器.编译器.汇编器和链接器. 预处理器  cpp c编译器 cc1 汇编器 as 链接器 ld 值得留意得是驱动程序经过相同的程序生成xxx.o,最后,它运行链接器程序ld,将main.o文件和xxx.o以及一些必要的系统目标文件组合起来,创建一个可执行目标文件. 像unix ld这样的静态链接器(static linker)以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件

第四十天:编译可加载模块

linux刚刚开始的时候仅仅支持intel 386 ,后来不断的被移植到越来越多的平台上,包括ARM ,POWERPC,所有的代码设备驱动代码都编译到内核中,这明显不现实,这时候就需要通过内核模块的形式来加载驱动.当然模块不一定是驱动,也可以是为驱动提供某种功能. 现在先编写一个简单的linux模块. 1 #include <linux/init.h> 2 #include <linux/module.h> 3 4 MODULE_LICENSE("GPL");

QT5.4 vs2013静态编译之加载静态插件sqlite

1. 很多同学在静态编译QT5完成后, sqlite的驱动老是加载不进去, 原因可能是因为你没有如下操作: #include <QtPlugin> Q_IMPORT_PLUGIN(QSQLiteDriverPlugin) 记得是 QSQLiteDriverPlugin 而不是 qsqlite 当然你在编译的时候的configure 要把插件编进去-plugin-sql-sqlite