相信绝大多树科班的人的第一行代码都是下面这个hello world
程序。当我们用window
下的visual studio
, 还是dev
等集成开发环境(IDE
)。都可以通过一个简单的按钮运行起来(程序没有bug
),那么这个按钮的背后究竟做了什么不为人知的工作,就十分值的我们去了解学习。
#include <stdio.h> int main() { printf("hello world \n"); return 0; }
Linux
系统是目前开发程序开发的最常用的系统。在linux
中开发过程中一般是使用gcc
来编译上述程序。
$ gcc hello.c $./a.out hello world
实际上程序从我们编写出来的代码段到计算机可以执行的二进制程序主要经历四个过程,分别是预处理、编译 、汇编和链接等过程,如下图。
最终通过这整一个流程我们就生能在执行机器上面运行的目标文件。好了,回到这一篇文章的正题。目标文件里面存放的是什么东西。它们又是如何进行组织的?下面就是围绕这些问题进行总结解析。
我们知道在在window
系统下不同的文件对应着不同的文件格式。而Linux
目标文件也有其文件格式–ELF格式,其主要是COFF
(common file formt
)格式的变种。在Linux
中主要的ELF文件格式有如下几种:
ELF 文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件 | 包含代码和数据,可以用来链接成可执行文件或共享文件,静态链接库也可以规则此类 | Linux中.o文件 |
可执行文件 | 可以直接执行的程序 | 如:/bin/bash文件 |
共享目标文件 | 一种是链接成目标文件,另外一种是作为进程印象的一部分 | linux 下的.so 文件 |
核心转储文件 | 进程意外终止,会将一些关键的信息存储到该文件中 | Linux下的core dump 文件 |
总的来说,程序源代码在编译以后主要是分为了两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss 段属于程序数据。bss段只是为未初始的全局变量和局部变量预留位置而已,它并没有内容,因此,不占用空间。基本结构如下:
如上图,ELF文件的开头是一个文件头,其主要包含的信息是文件的类型、入口地址、目标硬件、目标操作系统和一个段表信息等信息,段表就是描述文件中各个段的数组。那为啥要将程序划分成不同的段呢,主要集中在一下几点原因:
.o
文件section相关的信息$ objdump -h hello.o
size hello.o //查看代码各个section 的大小
objdump -x -s -d hello.o
因此应用程序也可以在文件中新建一个特殊的段来进行使用。为了满足Linux
系统的硬件内存和IO地址布局,GCC
提供了一个扩展机制。使得程序员可以指定变量或者函数所处的段。
__sttribute__ (section("qin")) int global = 42; __sttribute__ (section("qin")) void global () { }
readelf -h hello.o
ELF文件中用到很多字符串,比如段名、变量名等。因为字符串的长度往往不是固定的结构所以表示起来是比较困难。一个比较通用的方法是字符串全部集中在一个表中。使用字符串在表中的偏移表示字符串。strtab:字符串表 。shstrtab:段字符串表。
符号表
readelf -s helll.o
__cplusplus
编译会默认使用。#ifdef __cplusplus extern "C" { #endif int func(); int var; #ifdef __cpulsplus } #endif
强符号和弱符号,在程序编写的过程中,我们经常能碰到符号重复的问题。对于C/C++语言编译默认定义的符号都是强符号。针对强弱符号,编译器会根据一下规则进行处理:
gcc
中可以使用一下关键字来定制强弱符号和强弱引用。__attribute__((weak)) weak = 2; //弱符号 __attribute__((weakref)) void foo(); //弱引用 int main () { if(foo) foo(); }
这中弱符号和弱引用对于库来说十分有用。比如库中定义的弱符号可以被用户定义的强符号覆盖,从而使得程序可以使用自定义版本库函数。