C++ Makefile / Cmake 构建工程 & GDB调试

Table of Contents

一、基础知识

1.1 编译器

1.2 编译四步

1.3 链接

二、MAKE

2.1 指定头文件路径

2.2 指定链接库

2.3 编译源文件

三、CMAKE

3.1 编译源文件

3.2 加入头文件 & 链接库文件

a) 加入头文件

b) 引入可执行文件

c) 链接库文件

3.3 生成库文件

3.4 其他常用命令

四、GDB调试

4.1 配置gdb调试模式

4.2 gdb断点调试命令

4.3 gdb查看Coredump

 


一、基础知识

1.1 编译器

  • gcc: gnu的C编译器
  • g++:gnu的C++编译器。

对于简单工程,使用编译器直接在terminal中进行编译:

g++ main.cpp -o main  //main.cpp为需要编译的源文件, [-o main]为输出文件名。
g++ main.cpp -o main `pkg-config --cflags --libs opencv`  //使用pkg-config命令输出opencv的INCLUDE(--cflags)和LIBSPATH(--libs)

对于更复杂的工程,可以直接编写makefile后进行make, 或者使用cmake工具生成makefile,然后再make编译。

1.2 编译四步

  1. 预处理Pre-processing:把头文件写入cpp,生成.i的文档;   (预处理器cpp)
  2. 编译Compiling:检查语法错误,把代码翻译成汇编语言,生成文档.s; (编译器egcs) 
  3. 汇编Assembling:把编译生成的.s文件转为目标文件,生成.o的文档,即二进制的机器代码;(汇编器as) 
  4. 链接Linkling:将每个cpp文件生成的.o文件以及函数库链接在一起,生成可执行文件 。C++支持分离式编译,对源文件分别编译成目标文件,再链接生成可执行文件。(链接器ld,即linker eDitor)

参考链接:俊华的博客:GCC 编译详解liuchao1986105的博客:gcc编译选项

1.3 链接

这里着重讲一下链接。

函数库一般静态库(.a文件)和动态库(.so文件)

  • 静态库在编译链接时,把库文件的代码全部加入可执行文件中,运行时无需库文件,但生成的文件比较大;
  • 动态库在编译链接时不把库文件的代码加入到可执行文件中,而在程序执行时由运行时的链接文件加载库。这样的优点是:a. 不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题;  b. 如果静态库更新了,所以使用它的应用程序都需要重新编译、发布给用户;而动态库在程序运行是才被载入,用户只需要更新动态库即可,增量更新。

gcc在编译时默认使用动态库。

链接动态库有四种方法:

a) 动态库添加到/lib或者/usr/lib文件夹中,系统会默认搜索这些路径;

b) 每次运行程序前,临时将动态库所在的路径添加到环境变量LD_LIBRARY_PATH中,例如:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:TensorRT-5.1.5.0/lib

    此时,如果打开一个新的终端,则在之前的终端中添加的LD_LIBRARY_PATH无效。

c) 在配置文件中bashrc, /etc/profile或者/etc/ld.so.conf中添加动态链接库路径,例如:

sudo gedit ~/.bashrc

  在文件末尾加入

export LD_LIBRARY_PATH=/home/yly/Software/TensorRT-5.1.5.0/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}

 可以通过如下命令查看LD_LIBRARY_PATH:

echo $LD_LIBRARY_PATH

d) 在Cmake或makefile中添加libspath。

参考链接: 阿进的写字台的博客:运行时动态库

接下来分别介绍cmake和makefile的使用方法。

二、MAKE

对于简单的、文件比较少的工程,直接编写makefile,逻辑清晰可控。

GNU make的官方使用说明:http://www.gnu.org/software/make/manual/make.html

2.1 指定头文件路径

INCLUDE = -I $(OPENCV_ROOT)/include 

其中, -I表示将$(OPENCV_ROOT)/include作为第一个寻找头文件的目录,如果找不到,会搜索系统默认路径。

2.2 指定链接库

LIBSPATH= -L/usr/local/lib -lopencv_imgcodecs 

其中,-L表示将/usr/local/lib设为第一个寻找库文件的目录;-lopencv_imgcodecs表示在该路径中寻找libopencv_imgcodecs.so动态库文件。

2.3 编译源文件

2.3.1 语法规则

target … : prerequisites …
        recipe
        …

Makefile中清晰地指明了生成的目标文件名(target),目标文件的所有依赖文件(prerequisites),和生成规则(recipe)。

注意:recipe前要为<TAB>。

2.3.2 使用cpp生成 .o 文件,例如:

target.o:source.cpp
    g++ $(INCLUDE) -c source.cpp  -o target.o

使用g++编译器;

recipe中的$(INCLUDE)指定的是搜索cpp中#include包含的头文件的路径;

-o后指定生成的文件

-c 对指定的cpp文件进行编译和汇编(但不链接)

注意:

prerequistites中可不添加头文件,这样的问题是如果修改了头文件,Makefile不会识别到修改,因此不会重新编译。

如果想在prerequisites中加入头文件,例如:target.o:source.cpp header.h,  Makefile并不支持自动在$(INCLUDE)路径下搜索prerequisites添加的头文件,因此,需要在prerequisites中给出header.h的路径(如果只写header.h,则只查找Makefile当前路径;如果header.h不在Makefile的同一路径下,则显示 No rule to make target ‘header.h’, needed by 'target.o'. Stop.

2.3.3 链接生成可执行文件

target: target.o target2.o
    $(CXX) -o $@ $^ $(LIBSPATH)

PS: recipe中常用通配符:

$@  表示目标文件
$^  表示所有的依赖文件
$<  表示第一个依赖文件
$?  表示比目标还要新的依赖文件列表

三、CMAKE

Cmake首先需要编写Cmakelist.txt,随后运行如下命令生成makefile:

mkdir build
cd build
cmake ..

接下来利用生成的makefile进行编译,即可生成可执行文件:

make -j12 VERBOSE=1  #VERBOSE=1表示打印出编译的详情

该方法为外部构建,即中间文件和可执行文件都放在build目录中,与source_dir不同,从而保持代码目录的整洁。

下面讲解cmakelist的编写:

3.1 编译源文件

对于单独的cpp文件,无依赖lib,且无.h或.hpp头文件的情况,可以直接编译源文件,生成可执行文件

ADD_EXECUTABLE(main  hello.cpp)

3.2 加入头文件 & 链接库文件

对于稍复杂工程,需要添加头文件和链接lib。

a) 加入头文件

INCLUDE_DIRECTORIES(${CMAKE_CURRENT_LIST_DIR}/include)

编译器到指定的INCLUDE_DIRECTORIES路径下寻找cpp文件中include的.h和.hpp头文件。注意:不会自动遍历该路径的子目录,如果头文件在其子目录中,需要在.cpp代码中指定目录,例如: #include "opencv2/opencv.hpp"

b) 引入可执行文件

之所以说引入可执行文件,而不说但是编译源文件、生成可执行文件,是因为Cmakelist语法是先引入可执行文件名称以及构建该可执行文件的源文件,随后链接库文件才能真正生成可执行文件。

aux_source_directory(${CMAKE_CURRENT_LIST_DIR}/src SRCLIST) #寻找指定路径${CMAKE_CURRENT_LIST_DIR}/src下的源文件(包括cpp,cc;但不包括.h文件),并赋给变量SRCLIST
ADD_EXECUTABLE(main ${SRCLIST})

(cmake的指令是大小写无关的,add_executable和ADD_EXECUTABLE意义相同)

Note: 引入所有源文件时可以使用模糊匹配,收缩指定目录及其子目录下的所有文件

file(GLOB_RECURSE PROJECT_HEADERS "include/*.h" "include/*.hpp")
file(GLOB_RECURSE PROJECT_SOURCES "src/*.cpp" "src/*.cc")

c) 链接库文件

下面举例给出了链接库文件的几种使用方式。

LINK_DIRECTORIES(
    /home/Software/TensorRT-5.1.5.0/lib
    )
find_package(OpenCV 3.4 REQUIRED)
target_link_libraries(main
    -lnvinfer
     ${OpenCV_LIBS}  
    /usr/lib/x86_64-linux-gnu/libalglib.so  
    )
  • 通过LINK_DIRECTORIES(/home/Software/TensorRT-5.1.5.0/lib)指定目标库的路径,然后,在target_link_libraries(main -lnvinfer)中直接指定库的名称,gcc在编译时默认使用动态库;
  • 直接在target_link_libraries(main    /usr/lib/x86_64-linux-gnu/libalglib.so)中给出库的绝对路径;
  • 通过find_package(OpenCV 3.4 REQUIRED)直接找到对应模块的绝对路径,其中,find_package(<packagename> [version] [REQUIRED]), [version]指定寻找的版本(可选),[REQUIRED]表示若未找到模块则停止。

PS: find_package()

find_package()有Module和Config两种模式。

Module模式:cmake --help-module-list 查看cmake可以添加的模块列表,或查看/usr/share/cmake-3.5/Modules下的.cmake文件。使用find_package()后,cmake自动给一些变量赋值,可通过cmake --help-module FindBoost查看变量。例如OpenCV_LIBS,cmake脚本中可以直接使用这些变量。

Config模式:如果在cmake的module下未查找到模块,则进入Config模式。Config模式查找顺序如下:

1、如果cmakelist中定义了<PackageName>_DIR(仅在该路径下查找,不查找其子目录)或<PackageName>_ROOT(在目录及其子目录查找),则在相应路径下查找<PackageName>Config.cmake或<lower-case-package-name>-config.cmake文件。例如:

OPENCV_ROOT=/home/yly/Software/opencv-3.4.6

2、在cmake特定的缓存变量或环境变量中查找。例如CMAKE_PREFIX_PATH.

添加方法一:在cmakelist中

set(CMAKE_PREFIX_PATH /home/yly/Software/opencv-3.4.6)

添加方法二:在编译时直接添加flag,

cmake -DCMAKE_PREFIX_PATH=/your/path ../

3、HINT字段制定路径。

4、系统环境变量PATH中(如果path以/bin/sbin结尾,则自动转为其父目录)

     例如,echo ${PATH}查看系统环境变量发现存在/usr/local/bin,则可以在/usr/local中查找,在其子目录找到OpenCV对应的.cmake文件,/usr/local/share/OpenCV/OpenCVConfig.cmake,该文件定义了find_package(OpenCV 3.4 REQUIRED)后系统所定义的变量OpenCV_LIBS、OpenCV_INCLUDE_DIRS等。

3.3 生成库文件

对于更复杂的工程,可能需要通过cpp文件生成库文件。

set(IMPORTER_SOURCES
    a.cpp
    b.cpp
    c.cpp)
add_library(abc SHARED ${IMPORTER_SOURCES})  # 动态链接库
target_include_directories(abc PUBLIC <your-include-directory>)
target_link_libraries(abc PUBLIC <your-library-directory>)

set(IMPORTER_SOURCES
    a.cpp
    b.cpp
    c.cpp)
add_library(abc_static STATIC ${IMPORTER_SOURCES})  # 静态链接库
target_include_directories(abc_static PUBLIC <your-include-directory>)
target_link_libraries(abc_static PUBLIC <your-library-directory>)

set(EXECUTABLE_SOURCES
    main.cpp)
add_executable(main ${EXECUTABLE_SOURCES})
target_include_directories(main PUBLIC <your-library-directory>)
target_link_libraries(main PUBLIC abc_static)

3.4 其他常用命令

A. project (yourprojectname)

指定项目名后cmake隐式地定义了两个变量,<yourprojectname>_SOURCE_DIR和<yourprojectname>_BINARY_DIR。

  • <yourprojectname>_SOURCE_DIR是存放cmakelist.txt路径,即cmake后面跟的路径参数,例如cmake .. 则<yourprojectname>_SOURCE_DIR为当前路径的上一级目录。
  • <yourprojectname>_BINARY_DIR是cmake后生成的makefile文件存储的路径,注意:可能与生成的可执行文件的路径不同(如果通过set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)指定了可执行文件的存储路径的话)。

同时,隐式定义了PROJECT_SOURCE_DIR和PROJECT_BINARY_DIR,路径分别与<yourprojectname>_SOURCE_DIR和<yourprojectname>_BINARY_DIR一致,建议使用PROJECT_SOURCE_DIR和PROJECT_BINARY_DIR,不受工程名修改影响。

CMAKE_SOURCE_DIR 同样是存放cmake直接调用的cmakelist.txt路径。

CMAKE_CURRENT_SOURCE_DIR应用于含有子工程的add_subdirectory的场景,表示的是所运行的工程的cmakelist.txt的路径。

B. 传递FLAGS

a. 设置C++编译器的flags

set(CMAKE_CXX_STANDARD 11)

CMAKE_CXX_FLAGS定义的flag是编译每个.o文件时都会使用的。

b. CMAKE_EXE_LINKER_FLAGS

Linker flags to be used to create executables.仅在链接生成可执行文件时,CMAKE_EXE_LINKER_FLAGS指定的库会跟随在CMAKE_CXX_FLAGS指定的参数后。

set(CMAKE_EXE_LINKER_FLAGS "-lpthread")

在程序中使用多线程,需要使用pthread library,cpp文件中#include <pthread.h>,在链接时需要动态链接pthread库。

c. 将生成的可执行文件安装到指定目录:

set(CMAKE_INSTALL_PREFIX /usr/local)

显式定义变量CMAKE_INSTALL_PREFIX,当使用make install时,将生成的可执行文件拷贝到指定的目录/usr/local/目录下。在cmakelist中显式定义CMAKE_INSTALL_PREFIX等价于在运行cmake时指定,即:

cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..
install(TARGETS main
                abc
                abc_static
        RUNTIME DESTINATION bin    #可执行文件
        LIBRARY DESTINATION lib    # 动态链接库  (.so文件)
        ARCHIVE DESTINATION lib)   # 静态链接库  (.a文件)

在DESTINATION定义的路径,如果使用"/"开头则表示绝对路径,CMAKE_INSTALL_PREFIX失效;如果不以"/"开头则表示相对路径,安装路径在${CMAKE_INSTALL_PREFIX}/DESTINATION定义的路径。

d. cmakelist中手动指定g++/gcc编译器版本

SET(CMAKE_C_COMPILER "/usr/bin/gcc-4.8")
SET(CMAKE_CXX_COMPILER "/usr/bin/g++-4.8")

具体可参见:https://blog.csdn.net/Cxiazaiyu/article/details/106731687 

C. CHECK_CXX_COMPILER_FLAG(<flag> <var>)

Check whether the CXX compiler supports a given flag. 例如:

CHECK_CXX_COMPILER_FLAG("-std=c++11" COMPILER_SUPPORTS_CXX11)

如果编译器支持c++11则COMPILER_SUPPORTS_CXX11=1,否则为0。

D. 打印消息(常用于调试)

MESSAGE(STATUS "Project root directory:" ${PROJECT_SOURCE_DIR})

E. 增加宏定义 (非常有用,根据宏定义是否使用选择不同的代码)

cmakelist中添加:

add_definitions(-DUSE_MACRO)   #相当于在cpp代码中#define USE_MACRO,且应用于所有文件

也可以在cmakelist中设置一个开关,

OPTION(USE_MACRO "Build the project using macro" OFF)
IF(USE_MACRO)
add_definitions("-DUSE_MACRO")
endif(USE_MACRO)

然后在命令行中直接选择开启或关闭开关:

cmake .. -DUSE_MACRO=on 

PS: cmake缺点

跟已有体系接口不完善,例如有些程序下载后对应的是pkg-config,则通过find_package()无法找到对应的包。

四、GDB调试

gdb是linux系统下的调试器,是很多IDE的内核。直接使用gdb调试速度快,功能强大。可以查看程序崩溃位置和原因、设置断点调试等。

4.1 配置gdb调试模式

makelist中加入

set(CMAKE_BUILD_TYPE Debug)
SET(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g2 -ggdb") #debug模式下开启-g选项
SET(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall") #如果set(CMAKE_BUILD_TYPE RELEASE)则使用该配置

 如未进行如上配置,则gdb模式下程序崩溃无法找到对应的源码中的行号等与源码关联的信息,只有二进制代码的位置等信息。

4.2 gdb断点调试命令

进入gdb调试 gdb (空格) 可执行文件名 ,例如gdb main
设定程序的输入参数 set args(空格) 函数输入参数
显示程序行 l
显示程序中的某函数 l + 函数名
设置断点 b + 行号
开始运行(run) r
打印变量 p+变量名
继续运行,直到下一个断点 c
单步调试,如果遇到函数,则进入函数 s
单步调试,如果遇到函数,直接执行完函数 n
查看程序崩溃位置和原因 bt (即backtrace) 或 where
退出gdb调试 q

说明:表格中红色字体是常用的gdb调试命令,黑色字体表示设置断点调试相关命令。

参考:https://www.cnblogs.com/lsgxeva/p/8024867.html

4.3 gdb查看Coredump

在很多程序崩溃时(非gdb调试状态下运行),没有给出具体崩溃的位置和原因,但可以产生一个core dump文件。Coredump是进程在崩溃的那一刻生成的内存快照,通过gdb打开core文件可以分析定位程序崩溃的原因。

4.3.1 设置产生core文件

首先查看core file size,查看方法: 

ulimit -a

如果显示core file size为0,则程序不会生成core文件。为了产生core文件,修改方法:

ulimit  -c unlimited

PS: 以上设置仅在当前terminal内有效。

随后正常运行程序(不要gdb调试,gdb调试模式下崩溃不会产生core文件),如果程序崩溃,则会产生core文件,core文件默认存储在可执行文件的同一目录下。core文件一般命名为core-当时程序运行时的进程PID号-崩溃时的unix timestamp。

4.3.2 产生core文件的信号

SIGABRT 

Abort signal from abort(3)

SIGBUS

Bus error (bad memory access)

SIGFPE

Floating-point exception

SIGILL

Illegal Instruction

SIGIOT 

IOT trap. A synonym for SIGABRT

SIGQUIT

 Quit from keyboard

 SIGSEGV

Invalid memory reference

SIGSYS

Bad system call (SVr4);

SIGTRAP

Trace/breakpoint trap

 SIGUNUSED

Synonymous with SIGSYS

SIGXCPU

CPU time limit exceeded (4.2BSD)

SIGXFSZ

 File size limit exceeded (4.2BSD)

参考:http://www.tin.org/bin/man.cgi?section=7&topic=signal

4.3.3 使用coredump文件定位崩溃的位置

打开core文件debug:

gdb    可执行文件名    core文件名

如果出现以下提示,说明gbd未载入程序需要的库:

Could not load shared library symbols for libraries.  

Use the "info sharedlibrary" command to see the complete listing.

Do you need "set solib-search-path" or "set sysroot"?

如果是崩溃在这些库外面则对应库内,则通过where或bt可以定位到崩溃位置;如果崩溃在这些库内部,则必须为gbd设置动态链接库搜索的路径,设置方法:set solib-search-path [Directories] , 其作用相当于程序正常运行前需要export LD_LIBRARY_PATH。

gdb设置动态库路径方法参见:

https://blog.csdn.net/pcj_888/article/details/106882370

https://visualgdb.com/gdbreference/commands/set_solib-search-path

随后,使用bt或where命令可以定位到程序崩溃的位置。

https://blog.csdn.net/qq_39759656/article/details/82858101

猜你喜欢

转载自blog.csdn.net/Cxiazaiyu/article/details/90769079