下面是我阅读《深入了解计算机系统》时,一些自己认为重要的总结。期间会把课本上的一些实例拿来分享,使大家了解一些比较基础的东西。很多时候我们不知道程序为什么只能有一个main函数,及return和exit的区别,但是不清楚为什么是这样的,下面我们就简单的来了解下!
链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可以被加载拷贝到存储器执行。
静态链接:
了解编译器是如何运作的,请看前面的章节:
CPU与编译器概论(读书笔记)。
下面就以具体事例做介绍:
//main.c
#include <stdio.h>
void swap();
int buf[2] = {1,2};
int main()
{
swap();
return 0;
}
//swap.c
extern int buf[];
int *bufp0 = &buf[0];
int *bufp1;
void swap()
{
int temp;
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
Unix ld 程序这样的静态连接器(static linker)以一组可重定位目标文件(.o文件)和命令行参数作为输入,声称一个完全链接的可以加载和运行可执行程序作为输出。具体看下图:
目标文件:
目标文件的三种形式:
1、可重定位目标文件 :包含二进制代码和数据 ,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。(这里指的是.o文件,即没有链接的文件
2、可执行目标文件:包含二进制代码和数据,其形式可以被直接 拷贝到存储器并执行。可执行可链接文件
3、共享目标文件:一种特殊类型的可重定位目标文件,可以在加载和运行时被动态地夹在到存储器并链接。带有共享库的可执行文件
可重定位目标文件:
下面展示典型Elf可重定位目标文件的格式:
符号及符号表:
每个可重定位目标模块M都有一个符号表,它包含了M所定义和引用的符号的所有信息,在连接器的上下文中有三种不同的符号:
1、由M定义并被其他模块引用的全局符号
2、有其他模块定义被M模块引用的全局符号
3、只被模块M定义和引用的本地符号(局部变量符号)
关于符号表这里就不说了,想深入了解的话自己去看这本书,可以使用下面的readelf工具来查看符号表信息
name:字符串表中字节的偏移,指向以Null结尾的字符串的名字。
value:符号的地址,对于可重定位目标文件来说指距定义目标的节的起始位置的偏移。对于可执行目标来说是一个绝对运行的地址。
size:目标的大小
type:通常要么是数据,要么是函数,符号表还可以包含各个节的条目,以及对应原始原文件路径名的条目。
bind:表示符号是本地还是全局的
NDX:ABS,表示不能被重定向的符号,UNDEF,为定义的符号,Common 未被分配位置的未初始化的目标。
符号解析
连接器如何解析多重定义的全局符号
首先先了解下什么是强符号?什么是弱符号?
强符号:函数或者已经初始化的全局变量。
弱符号:未初始化的全局变量。
对于本节中得代码中,buf,bufp0,main,swap是强符号,bufp1是弱符号,根据强弱符号的定义,Unix连接器使用下面的规则来处理多重定义的符号:
1:不允许有多个强符号。
2:如果有一个强符号和多个弱符号,那么选择强符号
3:如果有多个弱符号,那么从弱符号中任意选择一个。
具体下面例子:
与静态库链接
创建静态库,我们使用Ar工具,具体如下:
gcc -c swap.c
ar rcs libswap.a swap.o
gcc main.c ./libswap.a
另外需要注意的是: 如果库不是相互独立的,那么他们必须排序,比如假设foo.c调用libx.a,libz.a中的函数,而这两个库又调用了liby.a中的函数。那么在编译时必须这么写:
gcc foo.c libx.a libz.a liby.a
如果foo.c调用libx.a的函数,该库又调用liby.a中的函数,而liby.a有调用libx.a的函数,那么编译时需这么写
gcc foo.c libx.a liby.a libx.a
可执行目标文件
我们已经知道连接器是如何将多个目标模块合并成一个可执行目标文件的,可以执行目标文件是一个二进制文件,且这个而进行文件包含加载程序到存储器并运行它所需要的所有信息。图7-11概括了一个典型的Elf可执行文件的各类信息。
这里我们可以使用 OBJDUMP 工具来显示可执行程序的一些数据。
加载可执行目标文件
每个程序都有一个运行时的存储器镜像,在32位Linux系统,代码段总是从地址0x08048000处开始,数据段是接下来的下一个4Kb对齐的地址。运行时在读/写段之后接下来的第一个4Kb对其的地址处,并且调用mallo库向上增长(这块我也不理解,暂且了解吧),具体看下图:
有下面2个问题:
1、为什么每个C程序都需要一个叫做main的函数?
每个程序都需要一个main函数,因为C的启动代码对于每个C程序而言都是相同的,要跳转到一个叫做main的函数上。
2、为什么C的main函数可以通过调用exit或者return语句来结束,或者两者都不做,程序依然可以正确的终止?
如果main以 return语句中之,那么控制传递给启动程序,该程序通过调用_exit在讲控制传递给操作系统,如果用户省略了return也会发生这种情况,如果main是一调用exit终止的,那exit将最终通过调用_exit将控制返回操作系统,这三种情况最终效果是相同的。
处理目标文件的常用工具介绍
花半天多的时间,简单总结了关于目标文件的一些知识,有几个工具在我们在C开发中可能经常用到的,ldd ar readelf等等,
参考资料
<深入理解计算机操作系统〉第二版