首页 > 系统相关 >Linux下C/C++配置与调试

Linux下C/C++配置与调试

时间:2023-11-14 14:23:02浏览次数:44  
标签:文件 gcc C++ 编译 so Linux test 动态 调试

环境:Ubuntu 18.04.6

一. GCC

简介:

GCCLinux下的编译工具集,是GNU Compiler Collection的缩写,包含gccg++等编译器,该工具及不仅包含编译器,还包含其他工具集,例如arnum等。

GCC工具集不仅能编译C/C++语言,其它例如Objective-C、Pascal、FOrtan、Java、Ada等语言均能进行编译。GCC可以根据不同的硬件平台进行编译,可以进行交叉编译,在A平台上编译B平台上的程序,支持常见的X86、ARM、PowerPC、mips等,以及Linux、Windows等软件平台。

1. 安装

sudo apt update # 更新apt下载源,尽可能保证下载到的软件是最新的
sudo apt install gcc g++	# 同时下载gcc和g++

安装完毕后,查看版本:

gcc -v
g++ -v

2. 工作流程

GCC编译器的工作流程分为四步:

  1. 预处理:GCC调用预处理器主要完成三件事:
    • 展开头文件
    • 宏替换
    • 去掉注释行
  2. 编译和优化:GCC调用编译器对文件进行编译,得到汇编文件。
  3. 汇编:GCC调用汇编器对汇编文件进行汇编,得到二进制文件。
  4. 链接:GCC调用链接器对程序需要的库进行链接,最终得到一个可执行的二进制文件。

具体如下:

处理前文件名后缀 处理 gcc参数 处理后文件名后缀
.c/.cpp 预处理 -E .i
.i 编译 -S .s
.s 汇编 -c .o
.o 链接 无固定后缀

EG:

  1. 新建程序:test.c

    // 假设程序对应的源文件名为 test.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    
    int main()
    {
        #ifdef HELLO
        printf("HELLO宏被定义");
        #endif
        int arr[5] = {1,2,3,4,5};
        for(int i=0; i<5; ++i)
        {
            printf("arr[%d] = %d\n", i, array[i]);
        }
        return 0;
    }
    
  2. 预编译:

    gcc -E test.c -o test.i
    

    得到文件形式如下:

    // 前面是展开的头文件
    
    int main()
    {
        
        
        
        int arr[5] = {1,2,3,4,5};
        for(int i=0; i<5; ++i)
        {
            printf("arr[%d] = %d\n", i, array[i]);
        }
        return 0;
    }
    

    可以看出:

    • 头文件被展开
    • 注释被消去
    • 宏常量被替换。
  3. 编译:

    gcc -S test.i -o test.s
    

    得到的test.s文件内是汇编语言。

  4. 汇编:

    gcc -c test.s -o test.o
    

    得到二进制文件。

  5. 链接:

    gcc test.o -o test
    

    得到可执行文件test

  6. 执行test文件,输出如下:

    ./test
    arr[0] = 1
    arr[1] = 2
    arr[2] = 3
    arr[3] = 4
    arr[4] = 5
    

3. gcc常用参数

以下表格列出gcc常用参数,这些参数在实际使用中并无顺序之分,只要指定即可。

参数 作用
-E 对源文件预编译,不进行编译
-S 对源文件进行编译,不进行汇编。
-c 对源文件进行汇编,不进行链接。
-o file 将原文件编译为file
-I 指定include头文件的搜索路径
-D 在程序编译时,定义一个宏
-w 不生成任何警告信息。(不推荐,因为有时警告信息就是报错)
-Wall 生成所有警告信息。
-O[n] 编译时优化代码。n代表优化的级别,0不优化,1为缺省值,3优化级别最高。
-l(小写的l) 在程序编译时,指定使用的库的名字。要去掉前缀和后缀
-L 指定编译的时候,搜索的库的路径。相对或绝对路径均可
-fpic/fPIC 生成与位置无关的代码(通常动态链接库使用)
-shared 生成共享目标文件。通常在建立共享库时。
-std 指定C/C++方言,如-std=c99,gcc默认的方亚是GNU C

3.1 搜索头文件(-I)

注意是大写的i不是小写的l

编写文件如下:

  1. 头文件./include/head.h:

    #ifndef _HEAD_H
    #define _HEAD_H
    // 加法
    int add(int a, int b);
    // 减法
    int sub(int a, int b);
    #endif
    
  2. 加法文件:./add.c

    #include <stdio.h>
    #include "head.h"
    int add(int a, int b){
        return a+b;
    }	
    
  3. 减法文件:./sub.c

    #include <stdio.h>
    #include "head.h"
    
    int sub(int a, int b)
    {
        return a-b;
    }
    
  4. 测试文件:./main.c:

    #include <stdio.h>
    #include "head.h"
    
    int main()
    {
        int a = 10;
        int b = 5;
        printf("a+b=%d\n", add(a, b));
        printf("a-b=%d\n", sub(a,b));
        return 0;
    }
    

最终文件结构如下:

.
├── add.c
├── cal
├── include
│   └── head.h
├── main.c
└── sub.c

此时使用gcc进行编译,发现无法找到头文件head.h。头文件寻找策略如下:

  • 在c/c++中,头文件如果使用<>,那就是从默认库中寻找(linux默认在/usr/lib
  • 如果是""包裹,那就从当前目录下寻找。

而头文件在include下,自然找不到,因此要在编译时指定头文件目录:

gcc *.c -o cal -I include/

编译成功。

3.2 定义宏(-D)

在编译程序时我们可以额外指定一个宏,这个宏会被写入文件中,从而对文件进行一定的控制。

EG:

  1. 定义程序如下:

    #include <stdio.h>
    #define NUMBER 3
    
    int main()
    {
        #ifdef DEBUG
    	printf("这是一条调试信息\n");
        #endif
        for (int i = 0; i < NUMBER; i++)
        {
            printf("%d\n", i);
        }
        return 0;
    }
    
  2. 不指定宏进行编译:此时不会输出调试语句

    gcc test.c -o test.o
    ./test.o
    0
    1
    2
    
  3. 指定宏进行编译:

    gcc test.c -o test.o -D DEBUG
    ./test.o
    这是一条调试信息
    0
    1
    2
    

使用场景:

用于控制调试语句,将所有调试语句根据宏的存在与否判断是否编译,这样就可以在灵活控制程序中调试信息输出与否。

4. gcc与g++

二者之间的区别如下:

  • 在编译阶段(即预编译之后的阶段):
    • 后命名为.c的文件,gcc将其视作C程序,而g++将其视作C++程序。
    • 后缀名为.cpp的文件,gcc和g++都会将其视作C++程序。
    • 在该阶段g++会调用gcc对文件进行编译。也就是说编译阶段只会由gcc完成。
  • 在链接阶段(最后一个阶段):
    • gcc和g++都能连接到标准C库。
    • g++可以自动链接到C++标注库,而gcc想要连接到C++标准库需要参数lstdc++
  • 关于_cplusplus宏的定义
    • g++会自动定义该宏,但这并不影响其对于c文件的编译。
    • gcc需要根据后缀名来确认是否需要定义该宏。

综上:

  • gcc和g++都能编译C文件
  • g++能直接编译C++文件,而gcc需要添加参数-lstdc++
  • gcc和g++都能定义宏_cplusplus

EG:

# 编译c程序
gcc test.c -o test
g++ test.c -o test
# 编译c++程序
g++ test.cpp -o test
gcc test.cpp -o test -lstdc++

二.静态库和动态库

简介:

所谓库文件,其实就是经过编译的二进制源文件,可以分为静态库动态库。在使用时需要搭配头文件。

在项目中使用库有两个目的:

  1. 使程序更加简洁,减少程序中的源文件数量。
  2. 避免源代码泄露。

1. 静态库

linux中静态库由ar(gcc内自带的程序)命令生成,现在已经使用的很少,大多数情况都是使用动态库。

命名规则如下:

  • Linux中,以lib为前缀,.a为后缀,中间随意,也就是libxxx.a的命名格式。
  • Windows中,以lib为前缀,以.lib为后缀,中间随意,也就是libxxx.lib的命名格式。

1.1 生成静态链接库

将源文件经过预编译、编译、汇编得到的二进制文件,通过ar工具打包即可得到静态库文件。

ar工具参数如下:

  • -c:创建一个库,不论库是否存在都将进行创建。
  • -s:创建目标文件索引,这样如果库较大时,能加快搜索速度。
  • -r:在库中插入模块。默认新成员是添加在库文件的结尾,但如果该模块名已经存在,那么就进行替换。

最后发布需要两个文件:

  • 制作的libxxx.a库文件,里面包含了具体实现的源代码。
  • 相应的头文件,相当于提供了源代码的接口。

1.2 实例

测试程序:

这里依旧使用一-->3-->3.1中的简单的计算机程序,结构如下:

.
├── add.c
├── include
│   └── head.h
├── main.c
├── sub.c

其中:

  • add.c和sub.c分别为加法和减法程序
  • include/head.h为头文件
  • main.c为测试文件

生成静态库:

  1. 将源文件进行汇编操作(前三步),得到二进制文件(注意指定头文件):

    gcc add.c sub.c -c -I include/
    

    得到二进制文件:

    add.o
    sub.o
    
  2. 将生成的目标文件通过ar工具打包为静态库(注意命名):

    ar -csr libcal.a add.o sub.o
    

    得到静态库文件:

    libcal.a
    
  3. 将头文件和静态库文件一起发给用户即可使用:

    include/head.h
    libcal.a
    

1.3 静态库的使用

首先要得到静态库和头文件,随后开始使用,当前文件结构如下:

head.h
libcal.h
main.c

错误示范:

gcc main.c -o cal
/tmp/ccT4oiqj.o:在函数‘main’中:
main.c:(.text+0x21):对‘add’未定义的引用
main.c:(.text+0x43):对‘sub’未定义的引用
collect2: error: ld returned 1 exit status

发现编译报错了,这是因为main.c中引入了头文件head.h,但编译器未能找到head.h中函数的具体实现,也就是找不到库文件,这与库文件的检索有关(后面会讲),简而言之就是找不到库文件,因此我们只需要在编译时指定库文件的路径和名字即可:

  • -L:指定库文件所在的目录,相对或绝对都可以。
  • -l(小写的l):指定库文件的名字(去掉前缀和后缀)。

正确示范:

gcc main.c -o test -L ./ -l cal

生成成功。得到可执行程序test。

该执行程序不依赖库文件和头文件即可运行,因为编译过程实际上是将库中的代码复制到了可执行程序中。

2. 动态库

简介:

与静态库不同,动态库是程序运行时才会加载的库,当动态链接成功部署后,多个程序可以使用同一个加载到内存中的动态库,因此在Linux中动态库也可以被称之为共享库。

动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊形式形成的。库中函数和变量使用的地址是相对地址(静态库中使用的是绝对地址),其真实地址是在应用程序加载动态库时形成的。

命名规则如下:

  • Linux中,以lib为前缀,以.so为后缀,中间是库的名字。也就是libxxx.so
  • Windows中,以lib为前缀,以.dll为后缀,中间是库的名字。也就是libxxx.dll

2.1 生成动态链接库

具体步骤如下:

  1. 通过-fpic参数在汇编时生成与位置无关的代码。
  2. 通过-shared参数告知编译器生成一个动态链接库。
  3. 发布头文件和动态链接库。

2.2 实例

实例代码:

依旧以一-->3-->3.1中的代码为例,其结构如下:

beasts777@ubuntu:~/coding/c++/cal$ tree
.
├── add.c
├── include
│   └── head.h
├── main.c
├── sub.c

生成动态链接库

  1. 使用gcc对源文件进行汇编(参数-c)生成与位置无关的目标文件,需要指定参数-fpic(注意指定头文件所在目录)

    gcc add.c sub.c -c -fpic -I include
    

    得到目标文件:

    add.o
    sub.o
    
  2. 使用gcc将二进制源文件打包成动态库,需要使用参数-shared

    gcc add.o sub.o -shared -o libcalc.so
    

    生成动态库文件:

    libcalc.so
    
  3. 发布动态库文件和头文件

    libcalc.so
    head.h
    

2.3 使用动态链接库

  1. 首先获取动态链接库和头文件:

    .
    ├── head.h
    ├── libcalc.so
    └── main.c		# 这是测试文件
    
  2. 编译测试文件(注意指定库文件的地址和名字):

    gcc main.c -L ./ -l calc -o app
    

    得到可执行文件app

  3. 执行文件:

    ./app
    ./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
    

    发现文件报错:找不到共享库libcal.so。这是为什么?明明在编译测试文件时制定了库文件的路径和名字,但实际运行时却记得名字却找不到目录。还有为什么静态库不会出现这一问题?答案见下一节?

2.4 解决动态库无法加载的问题

2.4.1 库的工作原理

静态库:

在程序编译的最后一个阶段,也就是链接阶段,提供的静态库会被打包进可执行程序中。也就是说:此时可执行程序内已经包含了静态库中的代码,当可执行程序执行时,其拷贝的静态库的代码也会加载到进程的代码区,因此也就不需要再去寻找静态库了。

动态库:

  • 在链接阶段,虽然使用gcc命令的-L-l指定了动态库的目录和名字,但此时:
    • 这一步只检查了动态库是否存在,并未将动态库中的代码拷贝到可执行程序中,因此运行时仍需要依赖动态库。
    • 虽然链接时指定了动态库的目录和名字,但可执行程序中只保留了库名,而未保留库的路径,它寻找库实际上是通过程序链接器按照指定顺序在固定目录寻找的。
  • 在可执行程序执行阶段:
    • 程序执行时会先检测需要的动态库是否存在,加载不到就会报错,显示无法加载到动态库。
    • 当动态库中的函数在程序中被调用了,这时动态库才会加载到内存中,不调用就不加载。
    • 动态库的检测和内存加载操作都是通过动态链接器完成的。

2.4.2 动态链接器

简介:动态链接器是一个独立于应用程序的进程,其本身属于操作系统,搜索动态库的依照一定策略,优先级从高到低依次是:

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

按照以上顺序,依次搜索动态库是否存在,如果都搜索不到,那么动态链接器就会报错,提示无法找到动态库。

由此,便可以得到找不到动态库的解决方案。

2.4.3 解决方案

一共有三种方法:

  • 方案一:将库的路径添加到环境变量LD_LIBRARY_PATH中。具体步骤如下:

    1. 找到配置文件:

      • 用户级别:~/.bashrc。该设置仅对当前用户有效。
      • 全局级别:/etc/profile。该设置对所有用户都有效。
    2. 打开配置文件,添加一句话:

      export LD_LIBRARY_PATH =$LD_LIBRARY_PATH :动态库的绝对路径
      # eg: export LD_LIBRARY_PATH =$LD_LIBRARY_PATH :/home/beasts777/coding/c++/activeLib/libcalc.so
      
    3. 令配置的文件生效:

      • 用户级别的修改:重启终端即可。(因为用户配置文件是在打开终端时加载的)

      • 系统级别的修改:重启系统即可。(全局配置文件在开机时加载)

      • 也可以用命令让操作系统重新加载配置文件,无需重启终端或系统:

        # 用户级
        source ~/.bashrc
        # 系统级
        source /etc/profile
        
  • 方案二:更新系统动态库的缓存文件:/etc/ld.so.cache(需要注意的是,我们无法直接更改缓存文件,应当更改:/etc/ld.so.conf配置文件,随后再同步到缓存文件):

    1. 打开/etc/ld.so.conf,将动态库的目录(注意这里时目录,不要添加库的名字)添加到最后一行,保存退出

      /home/beasts777/coding/c++/activeLib/
      
    2. ld.so.conf同步到ld.so.cache中:

      sudo ldconfig
      

      不需要进行其它操作即可生效。

  • 方案三:将动态库文件拷贝到系统库目录/lib//usr/lib/,或者在里面创建库的软连接(更推荐,因为这样如果后续库被修改了,就不用再拷贝一次了)

    # 拷贝库
    sudo cp /home/beasts777/coding/c++/activeLib/libcalc.so /usr/lib/libcal.so
    # 创建软链接(推荐)
    sudo ln -s /home/beasts777/coding/c++/activeLib/libcalc.so /usr/lib/libcal.so
    

2.4.4 验证是否能够链接到动态库文件

语法:ldd 可执行程序名

EG:

ldd app
linux-vdso.so.1 (0x00007ffe5ffbb000)
libcalc.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3a8b488000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3a8ba7b000)

如果可以链接,那么会显示地址,否则会显示not found。

2.4.5 实操

通过在系统动态库内添加软连接实现。

当前文件结构如下:

.
├── app			# 可执行文件
├── head.h		# 头文件
├── libcalc.so	# 动态库文件
  1. /usr/lib下创建动态库文件的软链接:

    sudo ln -s ~/coding/c++/cal/activeLib/libcalc.so /usr/lib/libcalc.so
    
  2. 查看可执行程序是否可以读取到动态库文件:

    ldd app
    linux-vdso.so.1 (0x00007ffe3e3ee000)
    libcalc.so => /usr/lib/libcalc.so (0x00007f29c7c68000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f29c7877000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f29c806c000)
    

    读取成功。

  3. 直接运行程序即可。

3. 优缺点

3.1 静态库

优点:

  • 静态库被直接打包到应用程序中,因此加载速度更快。
  • 发布程序时无需发布静态库。

缺点:

  • 相同的库文件可能在内存中被加载多份,浪费内存。
  • 如果库文件更新,就需要对项目进行重新编译,将新的库文件代码打包到可执行程序中。

3.2 动态库

优点:

  • 不同进程可使用同一动态库,实现不同进程间的资源共享,无需多次复制。
  • 修改动态库时,只需替换库文件,无需重新编译应用程序。
  • 因为动态库只有在使用库函数时才会被调用,因此程序员可以控制何时加载动态库。

缺点:

  • 加载速度比静态库慢,但当今计算机基本可以忽略。
  • 发布应用程序时需要发布以来的动态库。

标签:文件,gcc,C++,编译,so,Linux,test,动态,调试
From: https://www.cnblogs.com/beasts777/p/17831481.html

相关文章

  • AlmaLinux 9.3 正式版发布下载 - RHEL 兼容免费发行版
    AlmaLinux9.3正式版发布下载-RHEL兼容免费发行版由社区提供的免费Linux操作系统,RHEL兼容发行版。请访问原文链接:https://sysin.org/blog/almalinux-9/,查看最新版。原创作品,转载请保留出处。作者主页:sysin.org由社区提供的免费Linux操作系统一个开源、社区拥有和......
  • 学无止境--Linux开发实用命令(不定时更新)
    备注:学习记录所用,若有高手不吝赐教,万分感谢!1、将文件夹p1、p2、p3压缩到pkg.tar.gz:  tar-czfpkg.tar.gzp1p2p32、解压缩:  tar-zxvfpkg.tar.gz3、查找  find.-name*.c|xargsgrep-n"字符串"  查找并删除xx:find.-name*xx-typed-print-execrm......
  • Linux安装Kafka
    前言本文Kafka演示版本依赖Zookeeper,需要先安装Zookeeper:Linux安装Zookeeper1.解压文件tar-zxvfkafka_2.12-2.7.02.修改配置文件,日志输出地址vimconfig/server.propertieslog.dirs=/opt/kafka_2.12-2.7.0/logs3.启动bin/kafka-server-start.sh-daemonconfig/s......
  • Linux下安装Zookeeper
    前言Zookeeper是java编写,所以需要先安装JDK环境:Linux安装JDK1.下载解压文件tarzxvfapache-zookeeper-3.6.3-bin.tar.gz2.在主目录下创建data和logs两个目录用于存储数据和日志:cd/opt/apache-zookeeper-3.6.3-binmkdirdatamkdirlogs3.在conf目录下新建zoo.cfg文件......
  • Red Hat Enterprise Linux (RHEL) 9.3 (x86_64, aarch64) - 红帽企业 Linux 9.3 发布
    RedHatEnterpriseLinux(RHEL)9.3(x86_64,aarch64)-红帽企业Linux9.3发布下载红帽企业Linux9请访问原文链接:https://sysin.org/blog/rhel-9/,查看最新版。原创作品,转载请保留出处。作者主页:sysin.org红帽企业Linux9红帽企业Linux9.3新增功能红帽企业Li......
  • 学无止境--linux串口编程(RS485)
    备注:学习记录所用,若有高手不吝赐教,万分感谢!一、概括  linux将串口都映射成了TTY终端,所以在串口编程时,找到并使能平台的TTY,然后操作TTY终端即可。  例如对于Nuclei平台的轩辕91030M芯片设备树: uart0:serial@10013000{ compatible="sifive,uart0"; reg=<0x00x100......
  • kali linux系统下格式化硬盘/U盘
    首先点击虚拟机左上角,搜素GParted工具:打开该工具后,将磁盘切换至要格式化的设备:切换完成后,右键磁盘的未分配区域,选择New在createnewpartition对话框中,可以拖拽要格式化的磁盘边界:还可以选择filesystem格式:修改卷标后,点击Add:最后点击√选择Applied:待进......
  • C++U4-04-递推2
    上节课作业部分(点击跳转) 排列组合排列 组合:  练习题目 题2 编程题1,用递推求组合数编程题3:[【递推】直线分割平面问题]【算法分析】用a[i]表示i条直线最多能将这个圆分割成的部分数:当i=1时,a[1]=2;当i=2时,a[2]=4;......
  • 1823_ChibiOS的调试功能
    GreyZhang/g_ChibiOS:IfoundanewRTOScalledChibiOSanditseemsinteresting!(github.com)1.这里提到的调试,debug,跟测试工作本身是没有直接关系的。主要是为了保证开发以及实现上的保障来考虑的。2.所有的调试选项对于内核配置来说都是可访问的,这种设计应该是因为这样的......
  • C++U5-05-广度优先搜索2
    广搜逻辑  广搜代码核心思路 广搜伪代码前面讲解的广度优先搜索案例都类似迷宫类的问题,但有些非迷宫类问题也可以使用广搜的思路解决[【广搜2】填涂颜色]【算法分析】可以在外面增加一圈0,然后从(0,0)位置开始广搜所有为0的位置,没有被搜索到且为0的......