Table of Contents

一、基础知识
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 编译四步
- 预处理Pre-processing:把头文件写入cpp,生成.i的文档; (预处理器cpp)
- 编译Compiling:检查语法错误,把代码翻译成汇编语言,生成文档.s; (编译器egcs)
- 汇编Assembling:把编译生成的.s文件转为目标文件,生成.o的文档,即二进制的机器代码;(汇编器as)
- 链接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命令可以定位到程序崩溃的位置。