We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
使用gcc -Og -o prog main.c sum.c编译程序
gcc -Og -o prog main.c sum.c
main.c sum.c 源代码 | | 翻译器,cpp,cll,as 翻译器,cpp,cll,as | | main.o sum.o 可重定位目标文件 | | |_______链接器LD_______| | prog 完全可执行的目标
cpp [other arg] main.c /tmp/main.i
ccl /tmp/main.i -Og [other arg] -o /tmp/main.s
as [other arg] -o /tmp/main.o /tmp/main.s
ld -o prog [other arg]
libc.so
像Linux LD程序这样的静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的,可以加载和运行的可执行文件作为输出.为了构造可执行文件,链接器必须完成两个任务
目标文件有三种形式
ELF(Executable and Linkable Format).
ELF
___________________________0__ | ELF头 | | |--------------------------| | | .text | | |--------------------------| | | .rodata | | |--------------------------| | | .data | | |--------------------------| | | .bss | | |--------------------------| | | .symtab | 节 |--------------------------| | | .rel .text | | |--------------------------| | | .rel .data | | |--------------------------| | | .debug | | |--------------------------| | | .line | | |--------------------------| | | .strtab | | |--------------------------| 描述 | 节头部表 | 目标 |__________________________|文件的节
每个可重定位目标模块m都有一个符号表,它包含M定义和引用的符号的信息.在链接器的上下文中,有三种不同的符号:
全局符号分为强和弱,比如
/* foo1.c */ int main() { return 0; } /* bar1.c */ int main() { return 0; } linux > gcc foo1.c bar1.c /tmp/cc5kGGIR.o:在函数‘main’中: bar1.c:(.text+0x0): multiple definition of `main' /tmp/ccNctMlP.o:foo1.c:(.text+0x0):第一次在此定义 collect2: 错误:ld 返回 1
如果不使用静态库,编译器开发人员会使用什么方法来向用户提供这些预定义豪的函数.一种方法是让编译器辨认出对标准函数的调用.并直接生成相应的代码.但是这种方法对C是不合适的.因为C标准定义了大量的标准函数. gcc main.c /user/lib/libc.o 将所有的C函数都放在一个单独的可重定位目标模块中.这个方法的有点事将编译器的实现和标准函数实现分离开来.但是有一个不好的地方就是浪费空间.另外一个大的缺点是,对任何标准函数的任何改变,无论多么小的改变,都要求库的开发人员重新编译整个源文件.这是一个非常耗时的操作.
gcc main.c /user/lib/libc.o
gcc main.c /usr/lib/printf.o /usr/lib/scanf.o ... 我们可以为每个标准函数创建一个独立的可重定位文件,把它们存放在一个大家都知道的目录来解决这个问题.然而,这种方法要求应用程序员显示的链接合适的目标模块到它们的可执行文件中.这是一个容易出错而且耗时的过程: 所以静态库的概念被提出来了.以解决这些不同方法的缺点.相关的函数可以被编译为独立的目标模块,然后封装成一个单独的静态库文件.然后应用程序可以通过在命令行制定单独的文件名字来使用这些在库中定义的函数,比如,使用C标准库和数学库中函数的程序可以用形式一下的命令行来编译和链接: gcc main.c /usr/lib.libm.a /usr/lib/libc.a 在链接的时候,链接器支付至被程序引用的目标模块.这就减少了可执行文件在磁盘和内存中的大小.另一方面,应用程序员只要包含较少的库文件的名字.实际上C编译器驱动程序总是传送libc.a给链接器. 在linux系统中,静态库以一种称为存档的特殊文件格式存放在磁盘中, 存档文件是一组连接起来的可重定位目标文件的集合.有一个头部来描述每个成员目标文件的大小和位置.存档文件由后缀.a标识. 来,我们自己搞一个静态库.
gcc main.c /usr/lib/printf.o /usr/lib/scanf.o ...
gcc main.c /usr/lib.libm.a /usr/lib/libc.a
gcc -c addvec.c multvec.c ar rcs libvector.a addvec.o multvec. gcc -c main2.c gcc -static -o prog2c main2.o ./libvector.a # 或者 -L.参数告诉链接器在当前目录查找libvector.a gcc -static -o prog2c main2.o -L. -lvector
main2.c vector.h | | 翻译器(cpp,cc1, as) libvector.a libc.a 静态库 | | | main2.o addvec.o printf.o和其他调用printf.o调用的模块 |____________________________|___________________| |链接器(ld)| ------------ | prog2c 完全连接的可执行目标文件
当一个汇编器生成一个目标模块时,它并不知道数据和代码最终会在内存中的什么位置.他也不知道这个模块引用的任何外部定义的函数或者全局变量的位置,所以,无论何时汇编器遇到对最终为止未知的目标引用.他就会生成一个重定位条目,告诉链接器在将目标文件合并成为可执行文件时如何修改者饮用.代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.dat中.
最后的最后变成一个可执行的二进制文件,一个典型的ELF可执行文件的信息如下图
___________________________0__ | ELF头 | | |--------------------------| | | 段头部表 | |-> 将连续的文件节映射到运行时内存段 |--------------------------| | | .init | | |--------------------------| 只读内存段(代码段) | .text | | |--------------------------| | | .rodata | | |------------------------------| | .data | | |--------------------------| 读写内存段(数据段) | .bss | | |------------------------------| | .symtab | | |--------------------------| 不加载到内存 | .debug | 符号表和调试信息 |--------------------------| | | .line | | |--------------------------| | | .strtab | | |------------------------------- | 节头部表 | 描述目标 |__________________________| 文件的节
可执行目标文件的格式类似于可重定位目标文件的格式.ELF头描述文件的总体格式,它还包括程序的入口点.也就是当程序运行时要执行的第一条指令的地址..init节定义了一个小函数,叫做_init,程序的初始化代码会调用它,因为可执行文件是完全链接的.所以他不在需要.rel节.
ELF可执行文件被设计得很容易加载到内存,可执行文件的连续的片被映射到连续的内存段.程序头部表描述了这种映射关系
要运行可执行目标文件prog,我们可以在Linux shell的命令行输入它的名字: linux> ./prog 因为prog不是一个内置的shell命令,所以shell会认为prog是一个可执行目标文件.通过调用某个驻留在存储器中成为加载器的操作系统代码来运行它.任何Linux程序都可以通过调用execve函数来调用加载器,加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行改程序.这个将程序复制到内存运行的过程叫做加载.
linux> ./prog
execve
---------------------------------------- | 内核内存 | ---------------------------------------- 2^48-1 对用户代码不可见的内存 | 用户栈 | | (运行时创建) | ---------------------------------------- <- %rsp(栈指针) | ^ | | | | ---------------------------------------- | | | | 共享库的内存映射区域 | ---------------------------------------- | ^ | | | | ---------------------------------------- <- brk | 运行时堆 | | (由malloc创建) | -------------------------------------------- | 读/写段 | | | (.data, .bss) | | ---------------------------------------- 从可执行文件中加载 | 只读代码段 | | | (.init,.text,.rodata) | | --------------------------------------- 0x400000 | | | | ----------------------------------------0
每个Linux程序都有一个运行时内存印象,在linux x86-64系统中,代码段总是从地址0x400000处开始,后面是数据段.运行时堆在数据段之后,通过调用malloc库往上增长.堆后面的区域为共享模块保留的.用户栈总是从最大的合法地址(2^48-1)开始的,向较小内存地址增长.栈上的区域,从地址2^48开始,是为内核中的代码和数据表刘德. 为了简洁,堆/数据/代码段放在一起,实际由于.data有对齐要求,代码段和数据段是有间隙的.虽然每次程序运行时区域的地址都会改变,但是相对位置是不变的.当加载器运行时,它创建类似于内存印象,在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段.接下来,加载器跳转到程序的入口点.也就是_start函数的地址.这个函数是在系统目标文件ctrl.o中定义的,对所有的C程序都是一样的._start函数调用系统启动函数__libc_start_main,该函数定义在libc.so中,它初始化执行环境,调用用户层的main函数.处理main函数的返回值,并且在需要的时候需要控制返回内核.
静态链接库解决了许多关于如何让大量相关函数对应用程序可用的问题,然而静态库仍然有一些明显的缺点,静态库和所有的软件一样需要定期维护和更新,如果应用程序员想要使用一个库的最新版本,它们必须以某种方式了解到该库的更新情况,然后显示的将他们程序更新了的库重新链接,另外一个问题是几乎每个程序都使用标准的IO函数,比如printer,scan f在运行时,这些函数代码会被复制到每个运行进程的文本段中,在一个月上百个进程的典型系统上,这将是对稀缺内存系统资源的极大浪费 共享库是致力于解决静态库缺陷的一个现代创新产物,共享库是一个目标模块,在运行或加载是可以加载到任意的内存地址,任何一个内存中的程序链接起来,这个过程称之为动态连接,有一个动态链接器来执行共享库也称为共享目标,通常用来表示微软的操作系统,大量的使用了共享库,他们称之为DLL动态链接库,共享库是以两种不同方式来共享,
main2.c vector.h | | --------------- | 翻译器 | --------------- | libc.so libvector.so 可重定位目标文件main2.o ---------------------重定位和符号表信息 | | ---------------------------------------- | 链接器LD | ---------------------------------------- | prog21 (部分可执行的目标文件) | ----------------- | 加载器(execve)| ----------------- libc.so, libvector.so | ---------------------代码和数据 | | ----------------------------------------- | 动态链接器(ld-linux.so) | -----------------------------------------
使用gcc -shared -fpic -o libvector.so addvec.c multvec.c -fpicc选项指示编译器生成与位置无关的代码,shell的选项只是链接器创建一个共享的目标文件,一旦创建这个库,随后就要将它链接程序中 使用gcc -o prog21 main2.c ./libvector.so 这样就创建了一个可执行目标文件,此文件的形式使它在运行时可以和基本的思路是创建可执行文件是静态执行一些链接,然后在程序加载时动态完成这一点是非常重要的,只是没有任何vector.so的代码和数据集真的被复制到可执行文件中,反正链接去复制一些重定位和符号表信息,它是运行时可以解析对内部点so中代码和数据的应用
gcc -shared -fpic -o libvector.so addvec.c multvec.c
gcc -o prog21 main2.c ./libvector.so
到目前为止,我们已经讨论了在应用程序被加载后执行前10动态链接器加载和链接共享库的情景,然后应用程序还可能在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中,动态链接是一项强大有用的技术下面有一些现实世界中的励志
#include <dlfcn.h> void *dlopen(const char *filename, int flag); // 若成功返回指向句柄的指针,否者为null
dlopen函数加载和链接共享库filename,用已带RTLD_GLOBAL选项打开库解析filename的外部符号.如果可执行文件使用-rdynamic选项编译的,那么对于符号解析来说,他的 全局符号也是可用的.
-rdynamic
#include <dlfcn.h> void *dlsym(void *handle, char *symbol); // 若成功返回指向符号的指针,否者为null
dlsym函数的输入是一个指向前面已经带开了共享库和句柄和一个symbol的名字,如果该符号存在,就返回符号的地址,否则NUll
#include <dlfcn.h> void *dlclose(void *handle); // 若成功返回0,否者为-1
如果没有其他共享库还在使用该共享库,dlclose函数会卸载该共享库
#include <dlfcn.h> const char *dlerror(void); // 如果上面三个函数失败,则为错误消息,如果成功则为null
共享库的一个主要的目的就是允许多个运行的进程,共享内存中的相同的库代码,因而节约宝贵的内存资源那么多个进程是如何共享程序的一个副本呢,一种方法是给每个共享库分配一个事先预备的专
The text was updated successfully, but these errors were encountered:
No branches or pull requests
为什么要理解链接?
编译驱动程序
使用
gcc -Og -o prog main.c sum.c
编译程序cpp [other arg] main.c /tmp/main.i
.这一步将源码变成ASCII中间文件ccl /tmp/main.i -Og [other arg] -o /tmp/main.s
.这一步编译出汇编源文件as [other arg] -o /tmp/main.o /tmp/main.s
这一步搞出可重定位目标文件ld -o prog [other arg]
/tmp/main.o /tmp/sum.o 这一步高出可执行目标文件这里会出现一些错误,必须要手动链接
libc.so
. 可是我不会啊...静态链接
像Linux LD程序这样的静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的,可以加载和运行的可执行文件作为输出.为了构造可执行文件,链接器必须完成两个任务
目标文件
目标文件有三种形式
可重定位目标文件
ELF
(Executable and Linkable Format).符号和符号表
每个可重定位目标模块m都有一个符号表,它包含M定义和引用的符号的信息.在链接器的上下文中,有三种不同的符号:
认识到本地链接器符号和本地程序变量不同是非常重要的. .symtab中的符号表不包含对应于本地非静态程序变量的任何符号.这些符号在运行时在栈中被管理.符号根本不敢兴趣.
符号解析
链接器如何解析多重定义的全局符号
全局符号分为强和弱,比如
与静态库链接
如果不使用静态库,编译器开发人员会使用什么方法来向用户提供这些预定义豪的函数.一种方法是让编译器辨认出对标准函数的调用.并直接生成相应的代码.但是这种方法对C是不合适的.因为C标准定义了大量的标准函数.
gcc main.c /user/lib/libc.o
将所有的C函数都放在一个单独的可重定位目标模块中.这个方法的有点事将编译器的实现和标准函数实现分离开来.但是有一个不好的地方就是浪费空间.另外一个大的缺点是,对任何标准函数的任何改变,无论多么小的改变,都要求库的开发人员重新编译整个源文件.这是一个非常耗时的操作.
gcc main.c /usr/lib/printf.o /usr/lib/scanf.o ...
我们可以为每个标准函数创建一个独立的可重定位文件,把它们存放在一个大家都知道的目录来解决这个问题.然而,这种方法要求应用程序员显示的链接合适的目标模块到它们的可执行文件中.这是一个容易出错而且耗时的过程:
所以静态库的概念被提出来了.以解决这些不同方法的缺点.相关的函数可以被编译为独立的目标模块,然后封装成一个单独的静态库文件.然后应用程序可以通过在命令行制定单独的文件名字来使用这些在库中定义的函数,比如,使用C标准库和数学库中函数的程序可以用形式一下的命令行来编译和链接:
gcc main.c /usr/lib.libm.a /usr/lib/libc.a
在链接的时候,链接器支付至被程序引用的目标模块.这就减少了可执行文件在磁盘和内存中的大小.另一方面,应用程序员只要包含较少的库文件的名字.实际上C编译器驱动程序总是传送libc.a给链接器.
在linux系统中,静态库以一种称为存档的特殊文件格式存放在磁盘中, 存档文件是一组连接起来的可重定位目标文件的集合.有一个头部来描述每个成员目标文件的大小和位置.存档文件由后缀.a标识.
来,我们自己搞一个静态库.
链接器如何使用静态库来解析引用
重定位条目
当一个汇编器生成一个目标模块时,它并不知道数据和代码最终会在内存中的什么位置.他也不知道这个模块引用的任何外部定义的函数或者全局变量的位置,所以,无论何时汇编器遇到对最终为止未知的目标引用.他就会生成一个重定位条目,告诉链接器在将目标文件合并成为可执行文件时如何修改者饮用.代码的重定位条目放在.rel.text中,已初始化数据的重定位条目放在.rel.dat中.
可执行目标文件
最后的最后变成一个可执行的二进制文件,一个典型的ELF可执行文件的信息如下图
可执行目标文件的格式类似于可重定位目标文件的格式.ELF头描述文件的总体格式,它还包括程序的入口点.也就是当程序运行时要执行的第一条指令的地址..init节定义了一个小函数,叫做_init,程序的初始化代码会调用它,因为可执行文件是完全链接的.所以他不在需要.rel节.
ELF可执行文件被设计得很容易加载到内存,可执行文件的连续的片被映射到连续的内存段.程序头部表描述了这种映射关系
加载可执行文件
要运行可执行目标文件prog,我们可以在Linux shell的命令行输入它的名字:
linux> ./prog
因为prog不是一个内置的shell命令,所以shell会认为prog是一个可执行目标文件.通过调用某个驻留在存储器中成为加载器的操作系统代码来运行它.任何Linux程序都可以通过调用
execve
函数来调用加载器,加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口点来运行改程序.这个将程序复制到内存运行的过程叫做加载.每个Linux程序都有一个运行时内存印象,在linux x86-64系统中,代码段总是从地址0x400000处开始,后面是数据段.运行时堆在数据段之后,通过调用malloc库往上增长.堆后面的区域为共享模块保留的.用户栈总是从最大的合法地址(2^48-1)开始的,向较小内存地址增长.栈上的区域,从地址2^48开始,是为内核中的代码和数据表刘德.
为了简洁,堆/数据/代码段放在一起,实际由于.data有对齐要求,代码段和数据段是有间隙的.虽然每次程序运行时区域的地址都会改变,但是相对位置是不变的.当加载器运行时,它创建类似于内存印象,在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段.接下来,加载器跳转到程序的入口点.也就是_start函数的地址.这个函数是在系统目标文件ctrl.o中定义的,对所有的C程序都是一样的._start函数调用系统启动函数__libc_start_main,该函数定义在libc.so中,它初始化执行环境,调用用户层的main函数.处理main函数的返回值,并且在需要的时候需要控制返回内核.
动态链接共享库
静态链接库解决了许多关于如何让大量相关函数对应用程序可用的问题,然而静态库仍然有一些明显的缺点,静态库和所有的软件一样需要定期维护和更新,如果应用程序员想要使用一个库的最新版本,它们必须以某种方式了解到该库的更新情况,然后显示的将他们程序更新了的库重新链接,另外一个问题是几乎每个程序都使用标准的IO函数,比如printer,scan f在运行时,这些函数代码会被复制到每个运行进程的文本段中,在一个月上百个进程的典型系统上,这将是对稀缺内存系统资源的极大浪费
共享库是致力于解决静态库缺陷的一个现代创新产物,共享库是一个目标模块,在运行或加载是可以加载到任意的内存地址,任何一个内存中的程序链接起来,这个过程称之为动态连接,有一个动态链接器来执行共享库也称为共享目标,通常用来表示微软的操作系统,大量的使用了共享库,他们称之为DLL动态链接库,共享库是以两种不同方式来共享,
使用
gcc -shared -fpic -o libvector.so addvec.c multvec.c
-fpicc选项指示编译器生成与位置无关的代码,shell的选项只是链接器创建一个共享的目标文件,一旦创建这个库,随后就要将它链接程序中
使用
gcc -o prog21 main2.c ./libvector.so
这样就创建了一个可执行目标文件,此文件的形式使它在运行时可以和基本的思路是创建可执行文件是静态执行一些链接,然后在程序加载时动态完成这一点是非常重要的,只是没有任何vector.so的代码和数据集真的被复制到可执行文件中,反正链接去复制一些重定位和符号表信息,它是运行时可以解析对内部点so中代码和数据的应用
从应用程序中加载和链接共享库
到目前为止,我们已经讨论了在应用程序被加载后执行前10动态链接器加载和链接共享库的情景,然后应用程序还可能在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中,动态链接是一项强大有用的技术下面有一些现实世界中的励志
linux系统为动态链接提供了一个简单的借口允许应用程序在运行时加载和链接共享库
dlopen函数加载和链接共享库filename,用已带RTLD_GLOBAL选项打开库解析filename的外部符号.如果可执行文件使用
-rdynamic
选项编译的,那么对于符号解析来说,他的 全局符号也是可用的.dlsym函数的输入是一个指向前面已经带开了共享库和句柄和一个symbol的名字,如果该符号存在,就返回符号的地址,否则NUll
如果没有其他共享库还在使用该共享库,dlclose函数会卸载该共享库
位置无关代码
共享库的一个主要的目的就是允许多个运行的进程,共享内存中的相同的库代码,因而节约宝贵的内存资源那么多个进程是如何共享程序的一个副本呢,一种方法是给每个共享库分配一个事先预备的专
The text was updated successfully, but these errors were encountered: