GCC

GCC

fetch150zy

gcc&gdb常规操作 静态库、动态库的创建和链接

GCC

GCC是Linux下的编译工具集,是GNU Compiler Collection的缩写,包含gcc g++等编译器

gcc工作流程

gcc编译器对程序的编译分为四个阶段:预处理(预编译)、编译、汇编、链接

  • 预处理:展开头文件、宏替换、去注释

    gcc调用预处理器来完成,得到的还是文本格式的源文件

  • 编译:gcc调用编译器进行编译,得到汇编文件

  • 汇编:gcc调用汇编器进行汇编,得到二进制文件

  • 链接:gcc调用链接器对程序所需要的库进行链接,得到可执行文件

文件名后缀 说明 gcc参数
.c 源文件 -
.i 预处理后的源文件 -E
.s 汇编文件 -S
.o 二进制文件 -c
  • 演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 预处理
    g++ -E filename.c -o filename.i
    # 编译
    g++ -S filename.i -o filename.s
    # 汇编
    g++ -c filename.s -o filename.o
    # 链接
    g++ filename.o -o filename
    # 运行
    ./filename

gcc常用参数

gcc编译选项 选项的意义
-E 预处理
-S 编译
-c 汇编
-o 指定输出文件名
-I 指定include包含文件的搜索目录
-g 生成调试信息,该程序可被调试器调试
-D 在编译时指定一个宏
-w 不生成任何警告信息
-Wall 生成所有警告信息
On 0~3,四个优化选项-O3优化级别最高
-l 编译时指定使用的库
-L 指定编译时搜索库的路径
-fPic 生成与位置无关的代码
-shared 生成共享库
-std 指定C标准

多文件编译

gcc可以自动编译链接多个文件,不管是目标文件还是源文件,都可以使用一个命令编译到一个可执行文件中

假设有string.c string.h main.c三个文件

因为有文件是包含在源文件中的,因此在使用gcc编译程序的时候不需要指定头文件的名字

(在头文件无法被找到的时候需要使用参数 -I 指定其具体路径而不是名字)

  • 直接链接生成可执行程序

    1
    2
    gcc -o res string.c main.c
    ./res
  • 先将源文件编成目标文件,然后进行链接得到可执行程序

    1
    2
    3
    gcc -c string.c main.c
    gcc -o res string.o main.o
    ./res

gcc与g++

gcc g++的一些区别

在代码编译阶段

  • 后缀为.c的,gcc 把它当作是 C 程序,而 g++ 当作是 C++ 程序
  • 后缀为.cpp的,两者都会认为是 C++ 程序,C++ 的语法规则更加严谨一些
  • g++ 会调用 gcc,对于 C++ 代码,两者是等价的,也就是说 gcc 和 g++ 都可以编译 C/C++ 代码

在链接阶段

  • gcc 和 g++ 都可以自动链接到标准 C 库
  • g++ 可以自动链接到标准 C++ 库,gcc 如果要链接到标准 C++ 库需要加参数 -lstdc++

关于__cplusplus宏的定义

  • g++ 会自动定义__cplusplus宏,但是这个不影响它去编译 C 程序
  • gcc 需要根据文件后缀判断是否需要定义__cplusplus
  • 不管是gcc还是g++都可以编译C程序,编译程序的规则和参数都相同
  • g++可以直接编译C++程序,gcc编译C++程序需要添加额外参数-lstdc++
  • 不管是gcc还是g++都可以定义__cplusplus

静态库和动态库

不管是在Linux还是Windows中的库文件其本质和工作模式都是相同的

程序中调用的库包括静态库 动态库两种

在项目中使用库一般有两个目的,一个是为了使程序更加简洁不需要在项目中维护太多的源文件,另一方面是为了源代码保密。当我们拿到了库文件,还需要库中API的声明,也就是头文件。

静态库

在Linux下静态库由程序ar生成

  • Linux中静态库以lib作为前缀,以.a作为后缀,中间是库的名字,即libxxx.a
  • Windows中静态库一般以lib作为前缀,lib作为后缀,中间是库的名字,即libxxx.lib

生成静态链接库

生成静态库,需要先对源文件进行汇编操作(使用参数-c)得到二进制格式的目标文件(.o格式)

然后通过ar工具将目标文件打包就可以得到静态库文件

  • c:创建一个库,不管库是否存在都创建
  • s:创建目标文件索引,这在创建较大的库时能加快时间
  • r:在库中插入模块,默认新的成员添加在库的结尾处,如果模块名已经存在库中,则替换同名的模块

生成静态链接库的具体步骤如下

  • 对源文件进行汇编,得到二进制文件

    1
    gcc <src(*.c)> -c
  • 对二进制文件打包,得到静态库

    1
    ar rcs <libname.a> <bin(*.o)>
  • 发布静态库

    1
    # 提供头文件和libxxx.a

静态库的使用

拿到发布的静态库 head.h libcalc.a

将静态库、头文件、测试程序放在同一目录下进行测试

1
gcc main.c -o app -L <path> -l<lib>		# 编译测试程序

动态库

动态库是程序运行时加载的库,当动态库正确部署之后,运行的多个程序可以使用同一个加载到内存中的动态库

动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊方式形成的。库中函数和变量的

地址使用的是相对地址(静态库中使用的是绝对地址),其真实地址是在程序加载动态库时形成的

  • 在Linux中动态库以lib作为前缀,以so作为后缀,中间为库名,即libxxx.so
  • 在Windows中动态库一般以lib作为前缀,以dll作为后缀,中间为库名,即libxxx.dll

生成动态链接库

生成动态链接库是直接使用gcc命令并且需要添加-fPic-shared参数

  • -fPic参数是使得gcc生成的代码是与位置无关的,也就是相对位置
  • -shared是告诉编译器生成一个动态链接库

生成动态链接库的具体步骤如下

  • 对源文件进行汇编,需要-c-fPic参数

    1
    gcc <src(*.c)> -c -fpic
  • 将得到的.o打包成动态库,使用gcc-shared参数

    1
    gcc -shared <bin(*.o)> -o <libname.so>
  • 发布动态库和头文件

    1
    # 提供头文件和libxxx.so

动态链接库使用

拿到发布的动态库和头文件

1
gcc main.c -o app -L <path> -l<lib>

解决动态链接库无法加载问题

库的工作原理

  • 静态库如何被加载

    在程序编译的最后一个阶段也就是链接阶段,提供的静态库会被打包到可执行程序中。当可执行程序被执行

    静态库中的代码也会一并被加载到内存中,因此不会出现静态库找不到无法被加载的问题

  • 动态库如何被加载

    • 在程序链接阶段

      gcc命令虽然指定了库路径(-L),但是这个路径并没有记录到可执行程序,只是检查了这个路

      径下的库文件是否存在

      同样对应的动态库文件也没有被打包到可执行程序中,只是在可执行程序中记录了库的名字

    • 在程序执行后

      程序执行的时候会先检测需要的动态库是否可以被加载,加载不到就会提示上边的错误信息

      当动态库中的函数在程序中被调用了,这个时候动态库才被加载到内存,如果不被调用就不加载

      动态库的检测和内存加载操作都是由动态连接器来完成的

动态链接器

动态链接器是一个独立于应用程序的进程,属于操作系统,当用户的程序需要加载动态库的时候动态链接器就开始

工作,显然动态链接器根本就不知道用户通过gcc编译程序的时候通过-L参数指定的路径

那么动态链接器是如何搜索某一个动态库的呢,在它内部有一个默认的搜索顺序,按照优先级从高到低的顺序

  1. 可执行文件内部的DT_RPATH
  2. 系统的环境变量LD_LIBRARY_PATH
  3. 系统动态库的缓存文件/etc/ld.so.cache
  4. 存储动态库、静态库的系统目录/lib/ /usr/lib

如果按照上面的顺序一次搜索后仍没找到,动态链接器就会提示动态库找不到的错误信息

解决方案

  • 方案一、将库路径添加到环境变量LD_LIBRARY_PATH

    1. 找到相关配置文件

      用户级别:~/.bashrc对当前用户有效

      系统级别:/etc/profile对所有用户有效

    2. 修改配置文件

      1
      export LIBRARY_PATH=$LIBRARY_PATH:<path>
    3. 使配置文件生效

      1
      2
      source ~/.bashrc
      source /etc/profile
  • 方案二、更新/etc/ld.so.cache文件

    1. 找到动态库的绝对路径

    2. 修改/etc/ls.so.conf文件,将上面路径加到文件中

    3. 更新/etc/ld.so.conf中的数据到/etc/ls.so.cache

      1
      sudo ldconfig
  • 方案三、拷贝动态库到系统库目录/lib或者/usr/lib中(或将库的软链接文件放进去)

    1
    2
    3
    4
    5
    # 库拷贝
    sudo cp <path>/libxxx.so /usr/lib
    # 创建软链接
    sudo ln -s <path>/libxxx.so /usr/lib/libxxx.so
    # 注意必须使用绝对路经

验证

1
ldd <exec>

优缺点

静态库

  • 优点
    • 静态库被打包到应用程序中加载速度很快
    • 发布程序无需提供静态库,移植方便
  • 缺点
    • 相同的库文件数据可能在内存中被加载多份,消耗系统资源,浪费内存
    • 库文件更新需要重新编译项目,生成新的可执行程序,浪费时间

动态库

  • 优点
    • 可实现不同进程间的资源共享
    • 动态库升级简单,只需要替换库文件,无需重新编译应用程序
    • 可以控制何时加载动态库,不调用库函数动态库不会被加载
  • 缺点
    • 加载速度比静态库慢,现在计算机性能可以忽略
    • 发布程序需要提供依赖的动态库

GDB

gdb是由GNU软件系统社区提供的调试器,同gcc配套组成完整的开发环境

gdb是一套字符界面的程序集,可以使用命令gdb加载要调试的程序

调试准备

项目程序如果为了进行调试而编译时,必须打开调试选项(-g

此外,尽量不要使用编译器优化选项(-On

-Wall打开所有waring

启动和退出gdb

启动gdb

1
gdb <exec>

命令行传参

如果程序在启动时需要传递命令行参数,如果要调试这类程序,这些命令行参数必须要在应用程序启动之前通过调试

程序的gdb进程传递进去

1
2
3
gcc <src> -o -g <exec>
gdb app
(gdb) set args <...>

gdb中启动程序

gdb中启动要调试的应用程序有两种方式,一种是使用run,另一种是使用start

  • run:可以缩写为r,如果程序设置了断点会停在第一个断点的位置,如果没有设置断点,程序执行完
  • start:启动程序,最终会阻塞在main函数的第一行,等待输入后续其它gdb指令

如果想让程序start之后继续执行,或者在断点处继续执行,可以使用continue命令,简写为c

退出gdb

退出gdb调试,就是终止gdb进程,需要使用quit命令,简写为q

查看代码

gdb调试没有IDE那样完善的可视化窗口,给调试的程序打断点又是调试之前必须要做的一项工作

因此,gdb提供了查看代码的命令,可以轻松定位要调试代码行的位置

  • list,简写为l,通过这个命令我们可以查看项目中任意一个文件中的内容

当前文件

一般项目中有多个源文件,默认情况下通过list查看代码信息位于程序入口函数main对应的那个文件

如果不进行文件切换,main函数所在文件就是当前文件

1
2
3
(gdb) list
(gdb) list <lines> # 从指定行显示代码
(gdb) list <func> # 显示该函数上下文

切换文件

1
2
(gdb) list <file>:<lines>	# 切换到指定文件并显示该行上下文
(gdb) list <file>:<func> # 切换到指定文件并显示该函数上下行

设置显示的行数

默认list只能一次查看10行代码,如果想显示更多,可以通过set listsize来设置

同样你如果想查看当前显示的行数可以使用show listsize来查看,这里的listsize可以简写为list

1
2
(gdb) set listsize n
(gdb) show listsize

断点操作

想要通过gdb调试某一行或者得到某个变量在运行状态下的实际值,就需要在这一行设置断点

程序指定到断点的位置就会阻塞,我们可以通过gdb的调试命令得到我们想要的信息

  • break简写为b,设置断点

设置断点

两种断点:常规断点(程序运行到这个位置就会被阻塞),条件断点(指定条件被满足才会在断点处阻塞)

  • 设置普通断点到当前文件

    1
    2
    3
    # break == b
    (gdb) b <lines> # 断点打在该行
    (gdb) b <func> # 断点打在该函数首行
  • 设置普通断点到某个非当前文件上

    1
    2
    (gdb) b <file>:<lines>
    (gdb) b <file>:<func>
  • 设置条件断点

    1
    2
    (gdb) b <lines> if <var>==<val>
    # 通常情况下,在循环中条件断点用的比较多

查看断点

断点设置完毕之后,可以通过info break命令查看设置的断点信息,其中info可简写为i

1
2
# info == i
(gdb) i b

在显示的断点信息中有一些属性需要在其他操作中被使用

  • Num:断点的编号,删除断点或者设置断点状态的时候都需要使用
  • Enb:当前断点的状态,y表示断点可用,n表示断点不可用
  • What:描述断点被设置在了哪个文件的哪一行或者哪个函数上

删除断点

确定设置的某个断点不再使用了,可将其删除,删除命令是delete Numdelete可用简写为del d

删除断点的方式有两种:删除一个或多个指定断点,删除一个连续的断点区间

1
2
3
# delete == del == d
(gdb) d Num1 Num2 ... # 删除一个或多个指定断点
(gdb) d Num1-Num2 # 删除一个连续区间

设置断点状态

如果某个断点只是临时不需要了,可以将其设置为不可用状态,命令为disable Num

使用enable Num可在需要时重新将其设置回可用状态,简写为dis

1
2
3
4
5
6
7
# disable == dis
(gdb) dis Num1 Num2 ...
(gdb) dis Num1-Num2

# enable == ena
(gdb) ena Num1 Num2 ...
(gdb) ena Num1-Num2

调试命令

继续运行gdb

1
2
# continue == c
(gdb) continue

手动打印信息

打印变量值

1
2
3
# print == p
(gdb) p 变量名
(gdb) p /fmt 变量名
格式化字符(/fmt) 说明
/x 以十六进制形式打印整数
/d 以有符号,十进制形式打印整数
/u 以无符号,十进制形式打印整数
/o 以八进制形式打印整数
/t 以二进制形式打印整数
/f 以浮点数形式打印变量或表达式值
/c 以字符形式打印变量或表达式的值

打印变量类型

如果在调试1过程中需要查看某个变量的类型,可以使用ptype

1
(gdb) ptype 变量名

自动打印信息

设置变量名自动显示

当我们想频繁查看某个变量或表达式的值,使用display

1
2
(gdb) display 变量名
(gdb) display /fmt 变量名

查看自动显示列表

1
(gdb) info display
  • Num:变量或表达式的编号,gdb调试器为每个变量或表达式都分配有唯一的编号
  • Enb:表示当前变量(表达式)是处于激活还是禁用状态,y激活,n禁用
  • Expression:被自动打印值的变量或表达式的名字

取消自动显示

对于不需要再打印的变量或表达式,可以将其删除或禁用

  • 删除自动显示列表中的变量或表达式

    1
    2
    3
    4
    5
    (gdb) undisplay num1 num2 ...
    (gdb) undisplay num1-num2

    (gdb) delete display num1 num2 ...
    (gdb) delete display num1-num2
  • 禁用自动显示列表中处于未激活状态下的变量或表达式

    1
    2
    (gdb) enable display num1 num2 ...
    (gdb) display num1-num2

单步调试

step

  • step s命令执行一次,代码向下执行一次
1
2
# step == s
(gdb) step

finish

  • finish如果单步s跳入函数,通过finish跳出函数体
1
(gdb) finish

next

  • next n单步,但是不会进入函数体
1
2
# next == n
(gdb) next

until

  • until可直接跳出循环体,满足跳出循环内部不能有断点,必须在循环体的开始和结束行执行该命令
1
(gdb) until

设置变量值

直接更改某个特殊变量的值 set var 变量名=值

1
(gdb) set var 变量名=值