本文内容大部分出自对传智播客linux课程内容的总结和课堂笔记。

1. gcc编译器

1.1 gcc工作流程

image.png

  • 其中gcc通过调用预处理器cpp、汇编器as和链接器ls完成整个编译链接过程。

bbb.gif

1.2 gcc的一些参数使用

  • 直接从源文件(.c)生成可执行文件gcc main.c -o main
    • gcc编译源文件不加任何参数,直接生成一个可执行文件main
    • 若不加-o,则默认生成可执行文件a.out
    • 其实在生成可执行文件main的过程中,默认调用了预处理器、编译器、汇编器和连接器,只是把中间文件省略了
  • 查看版本号 -v 或 —version
  • 产生目标文件 -o
  • 指定头文件目录
    • -I + 目录
    • 在.c源文件中指定完整头文件路径
    • 比如在源文件中采用 #include “head.h”的方式引用头文件,而head.h(位于./include目录下)和main.c不在同一个目录下
      • gcc main.c -I ./include -o main
      • 或在源文件中写成 #include “./include/head.h”
  • 编译时定义宏 -D
    • 可以定义宏用于指定是否输出调试信息
    • e.g. image.png
      • gcc main.c -o main -D DEBUG
  • 编译优化 -On n=0∼3
    • -O0 — 没有优化
    • -O1 — 缺省值
    • -O3 — 优化级别最高
  • 提示更多警告信息 -Wall
  • 生成预处理文件 -E
  • 生成汇编文件 -S
  • 只编译子程序,不执行链接 -c
    • 生成二进制文件.o
  • 链接前后二进制文件的区别:
    • 只编译得到的二进制文件,汇编代码如下:

image.png

  • 链接后得到的可执行文件,反汇编生成汇编代码如下:

image.png

  • 两者主要区别:
    • 左边列出的地址不同,链接器将这段代码的地址移到了一段不同的地址范围中
    • 链接器填上了callq指令调用函数mult2需要使用的地址,链接器的作用之一就是为函数调用找到匹配的函数的可执行代码的位置
      • 包含调试信息 -g
  • gdb调试的时候必须加此参数

    2. 静态库的制作

    2.1 命名格式

  • lib + **静态库名 + **.a
  • e.g. libsort.a
    • 其中sort为静态库的名字

      2.2 制作步骤

  1. 生成对应的.o文件
  • 由.c文件编译生成.o文件
  • gcc -c a.c b.c c.cgcc -c *.c
  1. 将生成的.o文件打包得到静态库
  • ar rcs + 静态库的名字 + 生成的所有的.o
    • e.g. ar rcs libmytest.a a.o b.o c.oar rcs libmytest.a *.o
    • ar 工具不包含在gcc中
    • r —> 将文件插入静态库中
    • c —> 创建静态库,不管库是否存在
    • s —> 写入一个目标文件索引到库中,或者更新一个存在的目标文件索引。
  • 查看库中的符号(函数、全局变量等)nm + 静态库名

    • nm libmytest.a

      2.3 发布和使用静态库

  • 生成的静态库需要跟对应的头文件同时发布

    • 头文件中存放的是函数接口(函数声明)
  • 编译时使用静态库
    • gcc + 源文件 + -L 静态库路径 + -l(小写L)静态库名 + -I(大写i)头文件目录 + -o 可执行文件名
      • -L —> 指定库所在的路径
      • -l —> 指定库的名字
        • 去掉前缀 lib、去掉后缀 .a、只留下中间部分即为名字,比如libmytest.a -> mytest
      • -I —> 头文件目录位置
      • e.g. gcc main.c -L ./ -l mytest -I./ -o app
    • gcc + 源文件 + -I 头文件 + libxxx.a + -o 可执行文件名
      • 其中库如果不在同一目录下,应给出具体路径
      • e.g. gcc main.c -I ./include lib/libmytest.a
  • 对静态库使用的进一步理解,如下图:
  • 首先假设静态库libmytest.a中打包了add.o、sub.o、mul.o等二进制文件,其中add.o文件中包括a()和b()两个函数,sub.o中包括c()和d()两个函数,mul.o中包括了e()和f()两个函数;在main函数中调用了静态库中的a()和d()函数,此时在编译main.c源文件时,会把包含a()函数的add.o文件和包含d()函数的sub.o文件打包到可执行程序中。
    • 使用静态库编译生成可执行文件时,打包的最小单元是.o

image.png

2.4 优缺点及使用场合

  • 优点
    • 寻址方便,加载库的速度快
    • 因为库被打包到可执行程序中,直接发布可执行程序即可使用,不需要提供对应的库
  • 缺点
    • 静态库的代码在编译过程中已经被载入可执行程序,因此体积较大。
    • 如果静态函数库改变了,那么程序必须重新编译。
  • 使用场合

    • 在核心程序上使用,保证速度,可忽视空间
    • 主流应用于80、90年代,现在很少用

      3. 动态库(共享库)的制作:

      3.1 命名格式

  • lib + 动态**库名 + **.so

  • e.g. libsort.so
    • 其中sort为动态库的名字

      3.2 制作步骤

  1. 生成“与位置无关”的目标文件 (.o)
  • gcc -fPIC ``-c ``a.c b.c c.cgcc -fPIC ``-c *.c
    • 参数:-fPIC 表示生成与位置无关目标文件
    • 执行完毕后生成一系列的 .o 文件
  1. 制作动态库(将.o打包成共享库)
  • gcc -shared -o libmytest.so a.o b.o c.ogcc -shared -o libmytest.so *.o
    • 参数:
      • -shared : 制作动态库
      • -o : 重命名生成的新文件
  1. 简单说明:什么是与位置无关的目标文件
  • 每个程序在运行起来后,都会被分配一个虚拟地址空间(32位—4G),如下图所示。其中静态库是与位置有关的,因为在生成可执行文件时,会把静态库中用到的.o文件一起打包到最后的可执行文件中,程序每次执行的时候,程序中用到的静态库中打包的.o文件都会被放到虚拟地址空间中代码段的固定位置,因为虚拟地址空间中的地址是从0开始的,而.o文件加载到内存中使用的是绝对地址,比如每次都加载到123的位置;而动态库则不同,在生成可执行文件打包时,.o不会被打包到最终的可执行文件中,只是做一个记录而已,当程序运行起来以后才会加载到虚拟地址空间的共享库区域,可能每次加载的动态库不同,所以存放的位置是不定的,就不能采用绝对地址的方式,加载动态库使用的是一种相对地址,加载库时是可以知道动态库被加载到虚拟地址空间的起始地址的,比如加载到456的位置,即可每次通过(456+1..)的方式找到正确的代码段的位置。image.png

    3.3 发布和使用共享库

  • 我的环境(如下图):

    • 动态库所在目录为:/home/jiang/test/lib
    • 头文件所在目录为:/home/jiang/test/include
    • main.c中使用了动态库libmytest.so中的函数

image.png

  • 第一种方法:
    • gcc main.c lib/libmytest.so -o main 或者 gcc main.c /home/jiang/test/lib/libmytest.so -o main
    • 这样直接指定动态库所在路径的方法,就会到指定目录中找指定动态库。
      • 如果指定的是相对路径,比如第一种写法,那么执行main时,会默认在当前目录中找动态库,一旦main被移到别的目录下,就会出现找不到动态库的错误,可以通过ldd ./main查看main运行时依赖库的情况;
        • image.png
        • image.png
      • 如果指定的是绝对路径,比如第二种写法,则不会出现第一种写法的问题,main可以在其他目录下运行。
        • image.png
        • image.png
    • 这种方法的主要问题:每次编译生成可执行文件时都要指定lib所在目录的完整路径,不然main程序就无法在其他目录下运行。
  • 第二种方法:gcc main.c -L lib/ -l mytest -I include/ -o main

    - **-L** --> 指定库所在的路径
    - **-l **--> 指定库的名字
       - 去掉前缀 lib、去掉后缀 .so、只留下中间部分即为名字,比如libmytest.so -> mytest
    - **-I** --> 头文件目录位置
    
    • 不管-L是否指定完整路径,都无法加载动态库
    • image.png

      3.3 解决程序执行时动态库无法被加载的问题:

  • 当执行可执行程序时,通过动态链接器自动调用需要使用的动态库,而动态链接器本质也是一个动态库。

image.pngimage.png

  • 原因
    • 查看依赖的共享库:ldd main 发现 libmytest 找不到
    • 没有给动态链接器(ld-linux.so.2)指定好动态库 libmytest.so 的路径
  • 解决方案

    • 临时设置:export LD_LIBRARY_PATH=库路径,将当前目录加入环境变量LD_LIBRARY_PATH,但是终端退出了就无效了。
      • LD_LIBRARY_PATH
        • 作用: 指定查找共享库(动态链接库)时除了默认路径之外的其他路径,该路径在默认路径之前查找
        • 设置方法:用export命令来设置值
      • 可用于开发动态库的过程中使用
    • 永久设置:将export LD_LIBRARY_PATH=库路径写入家目录下.bashrc文件中,然后重启终端不常用。
    • 粗暴设置:直接将libmytest.so文件拷贝到/usr/lib/【或/lib】目录下(受lib中C库的启发),但是不推荐使用。
    • 重点掌握)将libmytest.so所在绝对路径追加入到/etc/ld.so.conf文件,使用sudo ldconfig -v更新。

      3.4 描述

  • 机制

    • 共享库的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此代码体积较小。
  • 优点
    • 节省内存(共享)
    • 易于更新(动态链接)
      • 停止运行程序
      • 使用新库覆盖旧库(保证新旧库名称一致,接口一致) “接口”
      • 重新启动程序。
  • 缺点
    • 延时绑定,速度略慢
    • 发布程序时,需要把动态库提供给用户
  • 使用场合
    • 对速度要求不是很强烈的地方都应使用动态库
  • 注意事项
    • 动态库是否加载到内存,取决于程序是否运行。