Linux下的静态链接

1.链接器驱动程序

大多数编译系统提供编译器驱动程序(compiler driver),它代表用户在需要时调用语言预处理器、编译器、汇编器和链接器。比如,要用GNU 编译系统构造示例程序,我们就要通过在shell中输人下列命令来调用GCC驱动程序:

linux> gcc -og -o prog main.c sum. c

假设示例由两个源文件组成,main.c和 test.c。main.c有对sum.c的调用

//main.c
int sum (int *a, int n) ;
int array [2] = {1,2};
int main()
{
    int val = sum(array,2);
    return val;
}
//sum.c
int sum(int *a,int n)
{
    int i, s = 0;
    for (i = o; i < n; i++)
    {
        s+=a[i];
    }
    return s;
}

下图概括了驱动程序在将示例程序从ASCII码源文件翻译成可执行目标文件时的行为。)驱动程序首先运行C预处理器( cpp)R,它将C的源程序main.c翻译成一个ASCII 码的中间文件main.i 。

接下来,驱动程序运行C编译器(cc1),它将main.i翻译成一个ASCII汇编语言文件main.s:然后,驱动程序运行汇编器( as),它将main.s翻译成一个可重定位目标文件(relo-catable object file)main.o:

驱动程序经过相同的过程生成sum.o。最后,它运行链接器程序ld,将main.o和sum.o 以及一些必要的系统目标文件组合起来,创建一个可执行目标文件(executable ob-ject file):

graph TD 源文件-->A2[cpp ,cc1, as ]-->A3[可重定位目标文件]-->A4[链接器ld]-->A5[完全链接的可执行目标文件]

2.静态链接

像Linux LD程序这样的静态链接器( static linker)以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的、可以加载和运行的可执行目标文件作为输出。输入的可重定位目标文件由各种不同的代码和数据段( section)组成。

为了构造可执行文件,链接器必须完成两个主要任务:

  1. 符号解析(symbol resolution)。目标文件定义和引用符号,每个符号对应于一个函数、一个全局变量或一个静态变量(即C语言中任何以static属性声明的变量)。符号解析的目的是将每个符号引用正好和一个符号定义关联起来。

  2. 重定位(relocation)。编译器和汇编器生成从地址О开始的代码和数据段。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些段,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目(relocation entry)的详细指令,不加甄别地执行这样的重定位。

3.目标文件

目标文件有三种形式:

  1. 可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。

  2. 可执行目标文件。包含二进制代码和数据,其形式可以被直接复制到内存并执行。

  3. 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。

编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。

目标文件是按照特定的目标文件格式来组织的,各个系统的目标文件格式都不相同。

从贝尔实验室诞生的第一个Unix系统使用的是a.out格式(直到今天,可执行文件仍然称为a.out文件)。Windows使用可移植可执行(Portable Executable,PE)格式。MacOS-X使用 Mach-O格式。现代x86-64 Linux和Unix系统使用可执行可链接格式(Execut-able and Linkable Format,ELF)。尽管我们的讨论集中在ELF上,但是不管是哪种格式,基本的概念是相似的。

4.可重定位目标文件

ELF Header 定义ELF魔数、机器字段长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度以及段的数量
.text 已编译程序的机器代码
.rodata 只读数据,比如printf 语句中的格式串和开关语句的跳转表
.data 已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data段中,也不出现在.bss段中
.bss 未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个段不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0
.symtab 一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目
.rel.text 重定位.text段中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。注意,可执行目标文件中并不需要重定位信息,因此通常省略,除非用户显式地指示链接器包含这些信息。
.rel.data 被模块引用或定义的所有全局变量的重定位表。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。
.debug 一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件。只有以-g选项调用编译器驱动程序时,才会得到这张表
.line 原始C源程序中的行号和.text段中机器指令之间的映射。只有以-g选项调用编译器驱动程序时,才会得到这张表。
.strtab 一个字符串表,其内容包括.symtab和.debug 段中的符号表,以及段表中的段名字。字符串表就是以null 结尾的字符串的序列。
段表(Section Header Table) 描述ELF中各个段的信息。如每个段的段名、段的长度、在文件中的偏移、读写数据权限及段的其他属性

上图展示了一个典型的ELF可重定位目标文件的格式

ELF Header 以一个16字段的序列开始,这个序列描述了生成该文件的系统的字的大小和字段顺序。ELF Header剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、段表(section header table)的文件偏移,以及段段表中条目的大小和数量。不同段的位置和大小是由段表描述的,其中目标文件中每个段都有一个固定大小的条目(entry)。

5.符号表

每个可重定位目标模块m都有一个符号表,它包含m定义和引用的符号的信息。在链接器的上下文中,有三种不同的符号:

  1. 由模块m定义并能被其他模块引用的全局符号。全局链接器符号对应于非静态的C函数和全局峦量

  2. 由其他模块定义并被模块m引用的全局符号。这些符号称为外部符号,对应于在其他模块中定义的非静态C函数和全局变量。

  3. 只被模块m定义和引用的局部符号。它们对应于带static属性的C函数和全局变量。这些符号在模块m中任何位置都可见,但是不能被其他模块引用。

认识到本地链接器符号和本地程序变量不同是很重要的。.symtab中的符号表不包含对应于本地非静态程序变量的任何符号。这些符号在运行时在栈中被管理,链接器对此类符号不感兴趣。

有趣的是,定义为带有 static属性的本地变量是不在栈中管理的。相反,编译器在.data或.bss中为每个定义分配空间,并在符号表中创建一个有唯一名字的本地链接器符号。比如,假设在同一模块中的两个函数各自定义了一个静态局部变量x:

int f()
{
    static int x = 0;
    return x;
}
int g()
{
    static int x = 1;
    return x;
}

在这种情况中,编译器向汇编器输出两个不同名字的局部链接器符号。比如,它可以用x.1表示函数f中的定义,而用x.2表示函数g中的定义。

利用static属性隐藏变量和函数名字

C程序员使用static属性隐藏模块内部的变量和函数声明,就像你在Java和C++中使用public和private声明一样。在C中,源文件扮演模块的角色。任何带有static属性声明的全局变量或者函数都是模块私有的。类似地,任何不带static属性声明的全局变量和函数都是公共的,可以被其他模块访问。尽可能用static属性来保护你的变量和函数是很好的编程习惯。

符号表是由汇编器构造的,使用编译器输出到汇编语言.s文件中的符号。.symtab段中包含ELF符号表。这张符号表包含一个条目的数组。下图展示了每个条目的格式。

//ELF符号表条目。type和 binding字段每个都是4 位
typedef struct {
    int name;			/*String table offset*/
    char type : 4;		/*Function or data (4 bits)*/
    char binding: 4;	/* Local or global (4 bits)*/
    char reserved;		/* Unused */
    short section;		/*Section header index*/
    long value;			/* Section offset or absolute address */
    long size;			/*Object size in bytes */
}Elf64_Symbol;

name是字符串表中的字节偏移,指向符号的以null结尾的字符串名字。value是符号的地址。对于可重定位的模块来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行时地址。size是目标的大小(以字节为单位)。type通常要么是数据,要么是函数。符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目。所以这些目标的类型也有所不同。binding字段表示符号是本地的还是全局的。

每个符号都被分配到目标文件的某个节,由section字段表示,该字段也是一个到节头部表的索引。有三个特殊的伪节(pseudosection),它们在节头部表中是没有条目的:ABS代表不该被重定位的符号;UNDEF代表未定义的符号,也就是在本目标模块中引用,但是却在其他地方定义的符号;COMMON表示还未被分配位置的未初始化的数据目标。对于COMMON符号,value字段给出对齐要求,而 size给出最小的大小。注意,只有可重定位目标文件中才有这些伪节,可执行目标文件中是没有的。

COMMON和.bss的区别很细微。现代的GCC版本根据以下规则来将可重定位目标文件中的符号分配到COMMON和.bss 中:

  1. COMMON 未初始化的全局变量
  2. .bss 未初始化的静态变量,以及初始化为0的全局或静态变量

GNU READELF程序是一个查看目标文件内容的很方便的工具。比如,下面是上图7中示例程序的可重定位目标文件main.o的符号表中的最后三个条目。开始的8个条目没有显示出来,它们是链接器内部使用的局部符号。

NUM: Value Size Type Bind Vis Ndx Name
8: 0000000000000000 24 FUNC GLOBAL DEFAULT 1 main
9: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 array
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sum

在这个例子中,我们看到全局符号main定义的条目,它是一个位于.text节中偏移量为0(即value值)处的24字节函数。其后跟随着的是全局符号array的定义,它是一个位于.data节中偏移量为0处的8字节目标。最后一个条目来自对外部符号sum 的引用。READELF用一个整数索引来标识每个节。Ndx=1表示.text节,而Ndx=3表示.data节。

6.符号解析

通常的观念,之所以要链接,是因为当前目标文件所引用的符号被定义在其他目标文件。

链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表中的一个确定的符号定义关联起来。对那些和引用定义在相同模块中的局部符号的引用,符号解析是非常简单明了的。编译器只允许每个模块中每个局部符号有一个定义。静态局部变量也会有本地链接器符号,编译器还要确保它们拥有唯一的名字。

对全局符号的引用解析就棘手得多。当编译器遇到一个不是在当前模块中定义的符号(变量或函数名)时,会假设该符号是在其他某个模块中定义的,生成一个链接器符号表条目,并把它交给链接器处理。如果链接器在它的任何输人模块中都找不到这个被引用符号的定义,就输出一条(通常很难阅读的)错误信息并终止。比如,如果我们试着在一台Linux机器上编译和链接下面的源文件:

void foo(void);
int main() {
	foo();
    return 0;
}

那么编译器会没有障碍地运行,但是当链接器无法解析对foo的引用时,就会终止:

linux> gcc -wall -Og -o linkerror linkerror.c

/tmp/ccSz5uti.o: In function 'main ' :

/tmp/ccSz5uti.o(.text+0x7): undefined reference to 'foo'

对全局符号的符号解析很棘手,还因为多个目标文件可能会定义相同名字的全局符号。在这种情况中,链接器必须要么标志一个错误,要么以某种方法选出一个定义并抛弃其他定义。Linux系统采纳的方法涉及编译器、汇编器和链接器之间的协作,这样也可能给不警觉的程序员带来一些麻烦。

6.1 链接器解析多重定义的全局符号

链接器的输入是一组可重定位目标模块。每个模块定义一组符号,有些是局部的(只对定义该符号的模块可见),有些是全局的(对其他模块也可见)。如果多个模块定义同名的全局符号,会发生什么呢?下面是Linux编译系统采用的方法。

在编译时,编译器向汇编器输出每个全局符号,而汇编器把这个信息隐含地编码在可重定位目标文件的符号表里。函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。

根据强弱符号的定义,Linux链接器使用下面的规则来处理多重定义的符号名:

  1. 不允许强符号被多次定义
  2. 如果有一个强符号和多个弱符号同名,那么选择强符号。
  3. 如果有多个弱符号同名,那么选择其中占空间最大者。

编译器按照一个规则来把符号分配为COM-MON和.bss。实际上,采用这个惯例是由于在某些情况中链接器允许多个模块定义同名的全局符号。当编译器在翻译某个模块时,遇到一个弱全局符号,比如说x,它并不知道其他模块是否也定义了x,如果是,它无法预测链接器该使用x的多重定义中的哪一个。所以编译器把×分配成COMMON,把决定权留给链接器。另一方面,如果x初始化为0,那么它是一个强符号(因此根据规则⒉必须是唯一的),所以编译器可以很确定的将它分配成.bss。类似地,静态符号的构造就必须是唯一的,所以编译器可以确定的把它们分配成.data或.bss。

6.2 重定位

对于可重定位的EIF文件来说,它必须包含重定位表,用来描述如何修改相应的段里的内容。比如, .text段如有需要被重定位的内容,就会有一个相对应叫.rel.text的段保存了代码段的重定位表。 .data段也是一样。

一旦链接器完成了符号解析这一步,就把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码段和数据段的确切大小。现在就可以开始重定位步骤了,在这个步骤中,将合并输人模块,并为每个符号分配运行时地址。重定位由两步组成:

  1. 重定位段和符号定义。在这一步中,链接器将所有相同类型的段合并为同一类型的新的聚合段。例如,来自所有输入模块的.data段被全部合并成一个段,这个段成为输出的可执行目标文件的.data段。然后,链接器将运行时内存地址赋给新的聚合段,赋给输人模块定义的每个段,以及赋给输人模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。

  2. 重定位段中的符号引用。在这一步中,链接器修改代码段和数据段中对每个符号的引用,使得它们指向正确的运行时地址。要执行这一步,链接器依赖于可重定位目标模块中称为重定位条目(relocation entry)的数据结构,我们接下来将会描述这种数据结构。

6.2.1重定位条目

当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。代码的重定位条目放在.rel.text中。已初始化数据的重定位条目放在.rel.data 中。

//ELF重定位条目。每个条目表示一个必须被重定位的引用,并指明如何计算被修改的引用
typedef struct {
    long offset;		/*Offset of the reference to relocate */
    long type:32;     	/* Relocation type*/		
    long symbol:32;		/* Symbol table index */
    long addend;		/* Constant part of relocation expression */
}Elf64_Rela;;

上示展示了ELF重定位条目的格式。offset是需要被修改的引用的节偏移。symbo标识被修改引用应该指向的符号。type告知链接器如何修改新的引用。addend是一个符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。

7.1与静态库链接

迄今为止,我们都是假设链接器读取一组可重定位目标文件,并把它们链接起来,形成一个输出的可执行文件。实际上,所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库(static library),它可以用做链接器的输入。当链接器构造一个输出的可执行文件时,它只复制静态库里被应用程序引用的目标模块。

在Linux系统中,静态库以一种称为存档( archive)的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a标识。

为了使我们对库的讨论更加形象具体,考虑下图中的两个例程。每个例程有一个副作用,会记录它自己被调用的次数,每次被调用会把一个全局变量加1。

//addvec.c
int addcnt = 0;
void addvec(int *x, int *y ,int*z,int n)
{
    int i ;
    addcnt++;
    for (i =0; i < n; i++)
    {
        z[i] = x[i] +y[i];
    }
}
//multvec.c
int multcnt = 0;
void multvec(int*x,int *y ,int *z, int n)
{
    int i ;
    multcnt++;
    for (i = 0; i < n; i++)
    {
        z[i] = x[i] * y[i];
    }
}

要创建这些函数的一个静态库,我们将使用AR工具,如下:

linux> gcc -c addvec.c multvec.c

linux> ar rcs libvector.a addvec.o multvec.o

为了使用这个库,我们可以编写一个应用,它调用addvec库例程。包含(或头)文件vector.h定义了libvector.a中例程的函数原型。

#include <stdio.h>
#include "vector.h"

int x[2] = {1,2};
int y[2] = {3,4};
int z[2];
int main()
{
    addvec(x,y,z,2);
    printf("z =[%d %d]\n",z[0],z[1]);
    return 0;
}

为了创建这个可执行文件,我们要编译和链接输入文件main.o和 libvector.a:

linux> gcc -c main2.c

linux> gcc -static -o prog2c main2.o ./libvector.a

或者等价地使用:

linux> gcc -c main2.c

linux> gcc -static -o prog2c main2.o -L. -lvector

-static参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到内存并运行,在加载时无须更进一步的链接。-lvector参数是 libvector.a的缩写,-L.参数告诉链接器在当前目录下查找libvector.a。

当链接器运行时,它判定main2.o引用了addvec.o定义的addvec符号,所以复制addvec.o到可执行文件。因为程序不引用任何由multvec.o定义的符号,所以链接器就不会复制这个模块到可执行文件。链接器还会复制libc.a中的printf.o模块,以及许多C运行时系统中的其他模块。

7.2链接器如何使用静态库来解析引用

虽然静态库很有用,但是它们同时也是一个程序员迷惑的源头,原因在于Linux链接器使用它们解析外部引用的方式。在符号解析阶段,链接器从左到右按照它们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件。(驱动程序自动将命令行中所有的.c文件翻译为.o文件。)在这次扫描中,链接器维护一个可重定位目标文件的集合E(这个集合中的文件会被合并起来形成可执行文件),-一个未解析的符号(即引用了但是尚未定义的符号)集合U,以及一个在前面输人文件中已定义的符号集合D。初始时E、U和D均为空。

  1. 对于命令行上的每个输入文件f,链接器会判断f是一个目标文件还是一个存档文件。如果f是一个目标文件,那么链接器把f添加到E,修改U和D来反映f中的符号定义和引用,并继续下一个输人文件。
  2. 如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m,定义了一-个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改U和D来反映m中的符号定义和引用。对存档文件中所有的成员目标文件都依次进行这个过程,直到U和D都不再发生变化。此时,任何不包含在E中的成员目标文件都简单地被丢弃,而链接器将继续处理下一个输人文件。
  3. 如果当链接器完成对命令行上输人文件的扫描后,U是非空的,那么链接器就会输出一个错误并终止。否则,它会合并和重定位E中的目标文件,构建输出的可执行文件。

不幸的是,这种算法会导致一些令人困扰的链接时错误,因为命令行上的库和目标文件的顺序非常重要。在命令行中,如果定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能被解析,链接会失败。比如,考虑下面的命令行发生了什么?

linux> gcc -static ./libvector.a main2.c

/tmp/cc9XH6Rp.o: In function 'main' :

/tmp/cc9XH6Rp.o(.text+0x18): undefined reference to 'addvec'

在处理libvector.a时,U是空的,所以没有libvector.a中的成员目标文件会添加到E中。因此,对addvec 的引用是绝不会被解析的,所以链接器会产生一条错误信息并终止

关于库的一般准则是将它们放在命令行的结尾。如果各个库的成员是相互独立的(也就是说没有成员引用另一个成员定义的符号),那么这些库就可以以任何顺序放置在命令行的结尾处。另一方面,如果库不是相互独立的,那么必须对它们排序,使得对于每个被存档文件的成员外部引用的符号s,在命令行中至少有-一个s的定义是在对s的引用之后的。比如,假设foo.c调用libx.a和 libz.a中的函数,而这两个库又调用liby.a中的函数。那么,在命令行中 libx.a和 libz.a必须处在liby.a之前:

linux> gcc foo.c libx.a libz.a liby.a

如果需要满足依赖需求,可以在命令行上重复库。比如,假设foo.c调用libx.a中的函数,该库又调用liby.a中的函数,而liby.a又调用libx.a中的函数。那么libx.a 必须在命令行上重复出现:

linux> gcc foo.c libx.a liby.a libx.a

另一种方法是,我们可以将libx.a和 liby.a合并成一个单独的存档文件。

8.1可执行目标文件

可执行目标文件的格式类似于可重定位目标文件的格式。ELF头描述文件的总体格式。它还包括程序的入口点(entry point),也就是当程序运行时要执行的第一条指令的地址。.text、.rodata和.data节与可重定位目标文件中的节是相似的,除了这些节已经被重定位到它们最终的运行时内存地址以外。.init节定义了一个函数,叫做_init,程序的初始化代码会调用它。因为可执行文件是完全链接的(已被重定位),所以它不再需要rel节。

ELF可执行文件被设计得很容易加载到内存,可执行文件的连续的片(chunk)被映射到连续的内存段。程序头部表( program header table)描述了这种映射关系。

8.2 加载可执行目标文件

要运行可执行目标文件 prog,我们可以在Linux shell的命令行中输入它的名字:

linux> ./prog

因为prog不是一个内置的shell命令,所以shell 会认为prog是一个可执行目标文件,通过调用某个驻留在存储器中称为加载器(loader)的操作系统代码来运行它。任何Linux程序都可以通过调用execve函数来调用加载器。加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行该程序。这个将程序复制到内存并运行的过程叫做加载。

ELF可执行文件引入了一个概念叫做Segment,一个Segment包含一个或多个属性类似的Section。如果将.text段和.init 段合并在一起看作一个Segment,那么装载的时候就可以将它们看作一个整体一起映射。在将目标文件连接成可执行文件的时候,链接器会尽量把相同权限属性的段划分在同一空间。比如可读可执行的段都放在一起。典型的是代码段;可读可写的段都放在一起,典型是数据段。

我们很难将SegmentSection这两词在中文上区分。很明显,从链接的角度,ELF文件是按照Section存储的,事实也的确如此;从装载的角度看,ELF文件又可以按照Segment划分。

每个Linux程序都有一个运行时内存映像。在Linux x86-64系统中,代码段总是从地址0x400000处开始,后面是数据段。运行时堆在数据段之后,通过调用malloc库往上增长, 堆后面的区域是为共享模块保留的。用户栈总是从最大的合法用户地址(248 一1)开始,向较小内存地址增长。栈上的区域,从地址248开始,是为内核(kernel)中的代码和数据保留的,所谓内核就是操作系统驻留在内存的部分。

当加载器运行时,它创建一个内存映像。在程序头部表的引导下,加载器将可执行文件的片(chunk)复制到代码段和数据段。接下来,加载器跳转到程序的入口点,也就是_start函数的地址。这个函数是在系统目标文件ctrl.o中定义的,对所有的C程序都是一样的。_start函数调用系统启动函数__libc_start_main,该函数定义在libc.so中。它初始化执行环境,调用用户层的main 函数,处理main函数的返回值,并且在需要的时候把控制返回给内核。

参阅自:程序员的自我修养-链接、装载、与库
CS:APP

posted @ 2020-12-22 22:50  Redwarx008  阅读(261)  评论(0编辑  收藏  举报