我们通常编写的文本程序是由ASCII字符组成,但是一个可执行程序是由二进制数字组成,从ASCII——>二进制文件,经历了

  1. 预处理:进行头文件和宏定义的替换
  2. 编译:由编译器把高级语言代码编译为汇编代码
  3. 汇编:由汇编器把汇编代码翻译成二进制代码,也即是.o文件
  4. 连接:由连接器把多个.o文件连接成可执行文件;可分为编译时链接,加载时链接(程序被加载到内存中执行时),运行时链接(由应用程序来执行时)。

静态链接

  静态链接器(gcc执行时)以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的,可以加载运行的可执行目标文件作为输出。

  静态库:把目标文件中的一个打包为单独一个文件,称为静态库。当构造一个输出可执行程序时,他只复制静态库中被应用程序引用的目标木块。在链接时,应用程序只复制被应用程序引用的目标模块(一个链接库中有多个函数,只需复制被引用的即可),减少了可执行文件在磁盘和内存中的大小。

静态库的缺点:

  1. 如果静态库有更新,必须了解到他的最新版本然后显示的将程序与更新的库连接。
  2. 他们的代码会被复制到进程的文本段中,大量的进程在运行的话会造成内存的浪费

  连接器的任务:目标文件是字节块的集合。它包含了代码与数据,还有其他连接器加载器的数据结构,连接器是把这些块连接起来,确定被连接块运行时的位置,修改代码和数据块中的位置。总的来说:

  1. 符号解析:变量或函数。为了让每个符号引用和一个符号定义关联。
  2. 重定位:通过把符号定义与一个内存地址关联起来,重定位这些节,修改这些符号的引用,使他们指向内存的位置。

符号和符号表

  每个可重定位模块都有一个符号表,包含

  1. 由该模块定义并能被其他模块一用的全局符号(非static函数和全局变量)
  2. 由其他模块定义并被该模块引用的全局符号(对应于其他模块的非static函数和全局变量)
  3. 由该模块定义并且只能被该模块引用的局部符号(该模块中static函数和全局变量)

  本地连接器符号和本地程序变量是不同的,.symtab中的符号不包含对应于本地非静态程序变量的任何符号,这些符号运行时在栈中被管理。

  编译器向汇编器输出每个全局符号,汇编器把他们放到符号表条目中,函数和已经初始化的全局变量为强符号,未初始化的全局变量为弱符号。

  1. 不允许有多个同名强符号
  2. 如果多个强符号和弱符号同名,选择强符号
  3. 如果多个弱符号同名,选择任意其中一个

符号解析

  连接器在符号解析阶段从左到右按照他们在编译器驱动程序命令行上的出现顺序来扫描可重定位目标文件(E,这个集合中的文件会被形成可执行文件)和存档文件( .c文件翻译为.o文件),未解析的符号(U),在前面输入中已定义的符号集合(D)

  1. 如果文件f是目标文件,则添加到E中,修改D和U反应f中符号的定义和引用,继续下一个文件
  2. 如果是个存档文件,则匹配u中未解析的符号和存档文件成员定义的符号,存档文件的成员m定义了一个u中未解析的符号,则把m加到e中,修改u和d反应m中的符号定义和引用。对存档文件中所有成员目标都依次进行这个过程,直到u和d不再变化。
  3. 如果扫描完命令行的输入后,u为非空,则连接器输出一个错误并终止程序,否则合并重定位e中的目标文件。构建输出的可执行文件。

重定位

  当遇到一个不在该模块定义的符号时,程序会假设他在其他模块定义,生成一个连接器符号表条目,把他交给连接器,如果连接器在其它模块中找不到该符号的定义,会输出一个错误并终止程序。

  完成符号解析后,代码中的每个符号引用和一个符号定义(输入一个输入目标文件模块的符号条目表)关联起来,此时连接器就知道目标模块代码和数据确切大小。然后可以开始重定位,为每个符号分配运行时地址。

  1. 重定位节和符号定义。将所有类型的节(比如所有目标模块中.data)合并为一个新聚合节。
  2. 重定义节中的符号引用。连接器修改代码中的每个符号引用,使他们指向正确的运行时地址。依赖于重定义目标模块中重定位条目的数据结构

重定位条目

  当汇编器生成一个目标模块时,他不知道数据和代码最终放在内存的什么位置,他也不知道模块引用的外部定义或函数的全局变量的位置。所以无论何时汇编器遇到对最终位置未知的目标引用时,他就会生成一个重定义条目。告诉连接器生成执行文件时如何修改这个条目。

目标文件

  有三种形式:

  1. 可重定位目标文件:包含二进制代码和数据,可以在编译时与其他可重定位文件合并共建一个可执行文件。(由编译器和汇编器生成)
  2. 可执行目标文件:包含二进制代码和数据,可直接加载到内存执行。(由连接器生成)
  3. 共享目标文件:特殊的可重定位文件,可以在加载时或运行时被动态加载到内存并连接。(由编译器和汇编器生成)

  目标模块:一个字节序列;目标文件:以文件形式存放在磁盘上的目标模块。

可重定位目标文件

  ELF头包含了16字节开始的序列,剩下的是EFL头大小,目标文件类型,机器类型,节头部表的文件偏移,节头目表中的条目大小和数量。

可执行目标文件

  成语一开始是ASCII文本文件,此时被转化为二进制文件,而且这个二进制文件包含加载程序到内存并运行它所需要的信息。

  ELF头标书文件的整体格式还包含程序的入口点(程序需要运行时执行的第一条指令的地址)。可执行文件的连续片(chunk)被映射到连续的内存段。

加载可执行目标文件

  当在shell中输入./programName时,shell解析到/判断不是内置命令(如果是内置命令时会搜索/usr /usr/lib ...)而是一个可执行文件,调用常驻内存的加载器(通过execve调用加载器)的操作系统代码来调用他。将可执行程序的代码和数据从磁盘复制到内存,在程序头部表的引导下加载器将可执行文件的片(chunk)复制到代码段和数据段,跳转到程序的第一条指令或入口点来运行。

  linux的每个程序都运行在一个进程的上下文中,有自己的虚拟地址空间。当一个shell运行时,父进程shell生成一个子进程,他是父进程的一个复制。子进程通过execve系统调用调用加载器,加载器删除现有的虚拟内存段,创建新的代码段数据段堆栈,新堆栈被初始化为0,通过将虚拟地址空间的页映射到可执行文件的页面大小chunk,新的代码段和数据段被初始化为可执行文件的内容,最后跳转到_start,最终调用程序的main函数,除了头部的一些信息,加载过程没有任何数据从磁盘复制到内存,知道CPU引用的第一个虚拟页时才被复制。利用页面调度算法将他从磁盘复制到内存。

共享目标文件

  共享库是个目标模块,在运行加载时,可以被加载到任意内存地址,并和一个在内存中的程序连接起来。这成为动态链接由动态链接器的程序来执行。

  -fpic指示编译器生成与位置无关代码,-shared指示连接器创建一个共享目标文件。

  当创建可执行文件时,静态执行一些连接,然后在程序加载时,完成动态链接过程,此时动态库中没有任何代码和数据被复制到可执行文件中,连接器复制了一些重定位和符号表信息,其中的.interp节包含动态链接器的路径名,然后执行下列完成重定位:

  1. 重定位libc.so的文本段和数据段到某个内存段
  2. 重定位libvector.so的文本段和数据段到某个内存段
  3. 重定位执行程序中对动态库的符号定义和引用

  最后将控制权给程序,此后共享库的位置不再变。

注意:

  调用动态库的时候有几个问题会经常碰到,有时,明明已经将库的头文件所在目录 通过 “-I” include进来了,库所在文件通过 “-L”参数引导,并指定了“-l”的库名,但通过ldd命令察看时,就是死活找不到你指定链接的so文件,这时你要作的就是通过修改 LD_LIBRARY_PATH或者/etc/ld.so.conf文件来指定动态库的目录。通常这样做就可以解决库无法链接的问题了。

  在linux下可以用export命令来设置这个值,在linux终端下输入:

  LD_LIBRARY_PATH=newdirs:$LD_LIBRARY_PATH.(newdirs是新的路径串)

  然后再输入:export ,即会显示是否设置正确,export方式在重启后失效,所以也可以用 vim /etc/bashrc ,修改其中的LD_LIBRARY_PATH变量。

从引用程序中加载和链接共享库

  1. #include<dlfcn.h>
  2. /*下面是打开一个动态连接库句柄指针*/
  3. void *dlopen(const char * filename,int flag);
  4.  
  5. /*下面是返回动态连接库中的函数地址*/
  6. void dlsym(void *handle,char *symbol);
  7.  
  8. /*下面是关闭打开的动态连接库*/
  9. int dlclose(void *handle);
  10.  
  11. /*下面是用来检测是否连接成功*/
  12. const char *dlerror(void);

与位置无关代码

  把代码加载到内存的任何位置而无需连接器修改。加载而无需重定位的代码称为与位置无关代码,这样无数进程可以共享代码段的单一副本。

PIC数据引用

  无论在内存何处加载一个目标模块(包含共享目标模块),数据段和代码段的距离总保持不变,因此代码段中的任何指令和数据段中的任何变量运行时都是一个常量。

  在数据段开始的地方创建一个GOT(全局偏移量表),在此表中,每个被这个模块引用的全局数据目标都有一个8字节条目,每个条目还有个重定位记录。连接器会重定位GOT中的每个条目,使它包含正确的目标地址。

PIC函数调用

  延迟绑定是避免一个库中成百上千个函数,在连接时需要大量重定位不需要的函数,把地址的绑定延迟到第一次调用该过程时。

库打桩机制

  给一个需要打桩的目标函数创建个包裹函数,它的原型与目标函数一样,使用某种打桩机制欺骗系统调用某种包裹函数而不是目标函数。

编译时打桩

  正是有-I.所以会进行打桩,告诉预处理器在搜索系统目录之前先在当前目录中malloc.h查找。

链接时打桩

  链接时打桩通过在链接时传递标志 -wl, --wrap f 给链接器,告诉链接器把符号 f 和 __real_f解析为 __wrap_f,实现替换。同样,实现替换的函数

  1. #ifdef LINKTIME
  2. #include<stdio.h>
  3. #include<malloc.h>
  4. //std malloc
  5. //试了直接调用malloc,编译链接ok,但是运行时core
  6. void *__real_malloc(size_t size);
  7. void __real_free(void *ptr);
  8. void *__wrap_malloc(size_t size)
  9. {
  10. void *ptr = __real_malloc(size);
  11. printf("[debug] malloc size %d\n", (int)size);
  12. return ptr;
  13. }
  14. void __wrap_free(void *ptr)
  15. {
  16. __real_free(ptr);
  17. printf("[debug] free %p\n", ptr);
  18. }
  19. #endif

  编译代码,-Wl,option标志把option传递给链接器,其中的每个都好替换为空格。

  1. gcc -DLINKTIME -c mymalloc.c
  2. gcc -Wl,--wrap,malloc -Wl,--wrap,free -o out main.c mymalloc.o

运行时打桩

  以上两种需要有源文件的情况下实现,而对于运行时打桩,只需要可以访问执行文件,利用动态链接器的LD_PRELOAD环境变量实现。

  当加载程序时,解析未定义的引用时,动态链接器会先搜索LD_PRELOAD指定的库,然后才搜索其他,因此,通过把自己实现的动态库设置到这个环境变量,动态链接器加载时搜索的该库内有对应实现的函数,就会直接使用该函数而不会再搜索其他系统库。

  实现自己的动态库,包含需要替代的函数

  1. #ifdef RUNTIME
  2. #define _GNU_SOURCE
  3. #include<stdio.h>
  4. #include<stdlib.h>
  5. #include<dlfcn.h>
  6.  
  7. void *malloc(size_t size)
  8. {
  9. void *(*mallocp)(size_t size);
  10. char *error;
  11. // 查找标准库的实现
  12. mallocp = dlsym(RTLD_NEXT, "malloc");
  13. if ((error = dlerror()) != NULL) {
  14. fputs(error, stderr);
  15. exit();
  16. }
  17. void *ptr = mallocp(size);
  18. printf("[debug] malloc size %d\n", (int)size);
  19. return ptr;
  20. }
  21.  
  22. void free(void *ptr)
  23. {
  24. void (*freep)(void *ptr);
  25. char *error;
  26. freep = dlsym(RTLD_NEXT, "free");
  27. if ((error = dlerror()) != NULL) {
  28. fputs(error, stderr);
  29. exit();
  30. }
  31. freep(ptr);
  32. printf("[debug] free %p\n", ptr);
  33. }
  34. #endif

  编译代码

  1. all:out
  2. out: main.c mymalloc.o
  3. gcc -o out main.c
  4.  
  5. ## 编译共享库
  6. mymalloc.o: mymalloc.c
  7. gcc -DRUNTIME --share -fpic -o mymalloc.so mymalloc.c -ldl
  8.  
  9. .PHONY : clean run
  10. run:
  11. # 指定运行时加载的库
  12. #setenv LD_PRELOAD "./mymalloc.SO"; ./out; unsetenv LD_PRELOAD
  13. ## 设定环境
  14. export LD_PRELOAD="./mymalloc.so"; ./out; unset LD_PRELOAD
  15. ## 其他任何的可执行程序都可以打桩
  16. export LD_PRELOAD="./mymalloc.so"; uptime; unset LD_PRELOAD
  17.  
  18. clean:
  19. @rm -rf out *.so

动态库中的soname的更多相关文章

  1. linux下动态库中的soname

    soname( Short for shared object name) 其是应用程序加载dll 时候,其寻找共享库用的文件名.其格式为 lib + math+.so + ( major versi ...

  2. Lib作为“静态库”与“动态库”中的区别

    Lib作为“静态库”与“动态库”中的区别 0. 前言: 什么是静态连接库: 静态库在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中.因此对应的链接方式称为静态链接. 为什么 ...

  3. 【C】——动态库中函数的作用范围

    如何生成动态库 net小伙 已经在此文中说明——[C]——如何生成静态库和动态库:接下来就要看看动态库中函数的作用范围: 首先我们使用命令   gcc -fPIC -shared -o libtest ...

  4. Golang调用windows下的dll动态库中的函数

    Golang调用windows下的dll动态库中的函数 使用syscall调用. package main import ( "fmt" "syscall" & ...

  5. Golang调用windows下的dll动态库中的函数 Golang 编译成 DLL 文件

    Golang调用windows下的dll动态库中的函数 package main import ( "fmt" "syscall" "time&quo ...

  6. c++动态库中使用命名空间的问题

    这是C++才会有的语言特性. 假如你使用一个程序库,他里面有桓霰淞拷衋bc,可是你自己也不小心定义了一个叫abc的变量,这样就会引起重定义错误.所以为了避免这样的现象,C++引入了名字空间(names ...

  7. C++调用动态库中的虚基类成员函数时总是进错函数

    原创文章,转载请注明作者与本文原始URL. 问题描述:最近遇到这样一个问题,在调用C++的一个成员函数时,总是进错函数.在调用 pMsg->GetMsgContent() 的时候,总是进入到 p ...

  8. 如何调用.so动态库中的函数,如何把自己的函数导出为.so的动态库函数供别人调用

    调用.so中的函数和平常的函数没有区别,只是在编译连接时加上-lxxxx就行了.要生成.so库,则编译时用下面的语句:gcc -shared -Wl,-soname,libmyfun.so -o li ...

  9. C++晋升之std中vector的实现原理(标准模板动态库中矢量的实现原理)

    我们实现的数据结构是为了解决在执行过程中动态的开辟空间使用(比如我们不停的输入,输入的多少我们不确定) 假设当你看到这篇文章的话,就当作是零食咀嚼,营养没有有BUG,能够直接看我博客中文章:CPU对内 ...

随机推荐

  1. JavaScript进阶内容笔记1:各种对象类型判断

    该文章主要用来介绍JavaScript中常用的一些对象检测判断方法,整理资源来自书本和网络,如有错误或说明不详之处,望评论提出,本菜定提名感谢……(本文章知识比较基础,大牛请提些意见再绕道,三克油^_ ...

  2. CV 两幅图像配准

    http://www.cnblogs.com/Lemon-Li/p/3504717.html 图像配准算法一般可分为: 一.基于图像灰度统计特性配准算法:二.基于图像特征配准算法:三.基于图像理解的配 ...

  3. Golang 中的指针 - Pointer

    http://www.cnblogs.com/jasonxuli/p/6802289.html   Go 的原生数据类型可以分为基本类型和高级类型,基本类型主要包含 string, bool, int ...

  4. 20145211MSF基础应用实验

    20145211MSF基础应用实验 一.实验博客 ms08_067攻击实验 http://www.cnblogs.com/entropy/p/6690301.html ms12_004漏洞攻击 htt ...

  5. 【Android】使用BaseAdapter实现复杂的ListView【转】

    本文转载自:http://blog.csdn.net/jueblog/article/details/11857281 步骤 使用BaseAdapter实现复杂的ListView的步骤: 1. 数据你 ...

  6. CentOS7.2 安装Redis3.2.8

    Redis3.2.8 下载 下载Redis3.2.8.tar.gz 将文件放置在usr/local/redis/中 解压文件 安装: make && make install [roo ...

  7. Flask 4 拓展

    NOTE 1.Flask被设计为可拓展模式,所以没有提供如数据库和用户认证等重要的功能,允许开发者按需开发. 2.使用Flask-Script支持命令行选项: 安装flask-script: pip ...

  8. No module named import_export.admin

    解决方法: pip install django-import-export

  9. HDU 3315 My Brute(二分图最佳匹配+尽量保持原先匹配)

    http://acm.hdu.edu.cn/showproblem.php?pid=3315 题意: 有S1到Sn这n个勇士要和X1到Xn这n个勇士决斗,初始时,Si的决斗对象是Xi. 如果Si赢了X ...

  10. 【MySQL】经典数据库SQL语句编写练习题——SQL语句扫盲

    [MySQL]数据库原理复习——SQL语言 对基本的SQL语句编写的练习题,其中的题目的答案可能会有多种书写方式. 1.题目1 1.1 关系模式 学生student:SNO:学号,SNAME:姓名,A ...