前言
学习资料:
B站视频:基于 VSCode 和 CMake 实现 C/C++ 开发
B站视频:Cherno C++ 教程
从本文开始,正式进入 C++ 部分。由于个人 C++ 零基础,仅了解一些 Python,所以学习时的痛点更偏向于 C++ 的基础,例如 VS Code、CMake、C++ 语法等,TensorRT 的部分和在 Python 中使用大同小异。
本文主要解决以下入门时的痛点:
(1)TensorRT 的示例或是 github 上的项目大多使用 Makefile 或 CMakeLists,并在命令行输入相关指令进行编译和运行,虽然通常修改一些路径依赖就能跑通,但仍是懵逼的。Makefile 看起来复杂一些,仅学习使用 CMake。
(2)在 IDE 如 VS Code 中运行和调试代码。
(3)建议学习 Cherno 教程中编译、链接的部分。
1. 安装 gcc、g++、gdb、cmake
sudo apt update
sudo apt install build-essential gdb
sudo apt install cmake
2. g++ 编译过程
将 C++ 源码编译成可执行文件,并且这个过程可以分为四个步骤
g++ main.cpp -o main
(1)预处理
g++ -E main.cpp -o main.i
(2)编译
将预处理后的文件翻译为汇编代码
g++ -S main.i -o main.s
(3)汇编
将汇编代码转换为机器可执行的目标文件(通常是二进制文件)
g++ -c main.s -o main.o
(4)链接
将目标文件与其他目标文件和库链接在一起,生成最终的可执行文件
g++ main.o -o main
3. g++ 编译参数
(1)调试-g
使编译器在生成的可执行文件中包含调试信息
g++ -g main.cpp -o main
(2)优化
启用编译器优化
g++ -O2 main.cpp -o main
(3)库文件-l
指定库文件,会在默认搜索路径中查找指定库文件,默认路径与操作系统有关
Linux 通常为 /lib
、/usr/lib
、/usr/local/lib
Windows 通常为 C:\Windows\System32
、系统环境变量 LIB
指定路径、应用程序所在目录
通常库名称会省略前缀 lib
和文件名后缀。
g++ main.cpp -lfile -o main
-L
指定库文件路径,库文件不在默认搜索路径中时用于指定搜索路径
g++ main.cpp -L/path/to/libs -lfile -o main
(4)头文件-I
指定头文件的搜索路径,编译器会先在指定路径中查找,再去系统默认路径中查找。Linux 默认头文件路径为 /usr/include
、/usr/local/include
。
g++ -I/path/to/include main.cpp -o main
(5)警告-Wall
启用编译器提供的警告信息-w
关闭所有警告信息
(6)设置编译标准
g++ -std=c++11 main.cpp -o main
(7)定义预处理器宏-D<name>[=<value>]
用于定义预处理器宏,通常用于控制条件编译
g++ -DDEBUG main.cpp -o main
例如下面代码只有在 DEBUG
定义时才会运行
#ifdef DEBUG
std::cout << "Debug" << std::endl;
#endif
4. g++ 实战案例
源码信息
项目文件结构
.
├── include
│ └── Log.h
├── main.cpp
└── src
└── Log.cpp
main.cpp
#include <iostream>
#include "Log.h"
int main()
{
Log("Hello World!");
}
Log.h
#pragma once
void Log(const char* message);
Log.cpp
#include <iostream>
#include "Log.h"
void Log(const char* message)
{
std::cout << message << std::endl;
std::cin.get();
}
直接编译
未指定输出文件名时会默认输出 a.out
g++ main.cpp src/Log.cpp -Iinclude
生成静态库
cd src
g++ -c Log.cpp -I../include
ar rs libLog.a Log.o
cd ..
g++ main.cpp -Iinclude -Lsrc -lLog -o static_mian.out
生成动态库
cd src
g++ Log.cpp -I../include -fPIC -shared -o libLog.so
cd ..
g++ main.cpp -Iinclude -Lsrc -lLog -o dyna_mian.out
生成动态库的语句可以按如下方式分步骤执行
g++ Log.cpp -I../include -c -fPIC
g++ -shared -o libLog.so Log.o
静态库和动态库的区别
静态库 | 动态库 | |
---|---|---|
后缀 | Linux .a Windows .lib |
Linux .so Windows .dll |
链接方式 | 嵌入到可执行文件中 | 编译时不会被嵌入到可执行文件中,仅在运行时加载和链接 |
文件大小 | 可执行文件较大,每个使用该库的可执行文件都包含库的副本 | 可执行文件较小,但会增加运行时的内存占用 |
部署运行 | 可执行文件与静态库完全独立,不需要额外的运行时支持 | 依赖于动态库,需要同时提供可执行文件和相应动态库的安装部署 |
更新维护 | 静态库代码发生变化时,每个依赖它的可执行文件都需要重新编译以包含最新的库代码 | 动态库代码发生变化时,只需重新编译动态库,不需要重新编译可执行文件,可执行文件在运行时会自动使用最新版本的动态库 |
加载和启动 | 加载时间较长,启动速度较快 | 加载时间较短,启动速度较慢 多个程序可共享同一个动态库,从而减少内存占用 |
动态库路径问题
由于 libLog.so
在 src
文件夹下,在学习的教程案例中,运行 ./dyna_mian.out
会报错找不到动态库文件。其原因是 src
并不在系统默认寻找动态库路径中,此时需要执行 LD_LIBRARY_PATH=src ./dyna_mian.out
将其添加到动态库路径中才能正常运行。
然而,在测试时可以直接成功运行 dyna_mian.out
,弹幕评论区也有不少遇到了相同的情况,于是寻找了一下原因,以下为探寻过程,所有方法都来自 ChatGPT3.5。
先总结一下,其根本原因是编译器优化导致 Log
函数没有使用,所以程序运行根本不需要生成的动态库文件。之后找到方法使编译器强制使用 Log
函数,才得到了和教程中一样的结果,在此基础上再解决找不到路径的问题。
查找问题所在
(1)Linux 系统动态库搜索路径配置/etc/ld.so.conf
文件包含了动态库搜索路径的配置,其内容如下:
include /etc/ld.so.conf.d/*.conf
通过 find /etc/ld.so.conf.d/ -type f -name "*.conf"
查找对应文件,结果如下:
/etc/ld.so.conf.d/cuda-11-8.conf
/etc/ld.so.conf.d/cuda-12-2.conf
/etc/ld.so.conf.d/libc.conf
/etc/ld.so.conf.d/fakeroot-x86_64-linux-gnu.conf
/etc/ld.so.conf.d/i386-linux-gnu.conf
/etc/ld.so.conf.d/x86_64-linux-gnu.conf
这些文件的内容就不一一列出了,总之未包含工程中的 src
路径。
(2)动态库缓存ldconfig -p
可以查看系统当前的动态库缓存,即系统已知的动态库路径ldconfig -p | grep libLog
筛选查询结果,得知 libLog
也不在缓存中。sudo ldconfig
可更新动态库缓存
(3)可执行文件依赖关系ldd ./dyna_mian.out
查看可执行文件所依赖的动态库及其路径信息
linux-vdso.so.1 (0x00007fff3c1b5000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f218a02e000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2189e3c000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f2189ced000)
/lib64/ld-linux-x86-64.so.2 (0x00007f218a23a000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f2189cd2000)
(4)追踪运行时的动态库加载情况
strace -e open,access ./dyna_mian.out
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (没有那个文件或目录)
Hello World!
+++ exited with 0 +++
根据输出结果,运行时并没有尝试打开或访问动态库文件,libLog.so
在运行时可能已经加载并缓存。在 Linux 系统中,动态库加载一般是由动态连接器 ld.so
或 ld-linux.so
负责,它会根据系统的动态库搜索路径以及运行时路径(RPATH)来查询加载动态库。
下方指令可以更全面的查看动态库加载的情况,会输出详细的搜索路径和动态库加载过程,而其中并没有发现 libLog.so
LD_DEBUG=libs ./dyna_mian.out
(5)查看程序的符号表
nm -D ./dyna_mian.out
U __cxa_atexit
w __cxa_finalize
w __gmon_start__
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
U __libc_start_main
U _ZNSi3getEv
U _ZNSolsEPFRSoS_E
U _ZNSt8ios_base4InitC1Ev
U _ZNSt8ios_base4InitD1Ev
0000000000004160 B _ZSt3cin
0000000000004040 B _ZSt4cout
U _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
其中 U
表示未解析的符号,表明程序引用了一些 C++ 库的函数和对象,而输出中并没 Log
函数的符号,也就是编译器在优化过程中把函数剔除了,根本没有使用。
解决优化函数方法
第一个尝试的方法是用 -rdynamic
选项重新编译,会让编译器将所有符号都加入到可执行文件的动态符号表中,然而编译器仍然优化掉了 Log
函数。
g++ main.cpp -Iinclude -Lsrc -lLog -o dyna_mian.out -rdynamic
第二个尝试的方法可行,将 Log.h
和 Log.cpp
分别按如下修改,强制编译器包含 Log
函数
#pragma once
__attribute__((__visibility__("default"))) void Log(const char* message);
#include <iostream>
#include "Log.h"
__attribute__((used)) void Log(const char* message)
{
std::cout << message << std::endl;
}
查看程序的符号表:
nm -D ./dyna_mian.out
U __cxa_atexit
w __cxa_finalize
w __gmon_start__
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
U __libc_start_main
U _Z3LogPKc
U _ZNSi3getEv
U _ZNSt8ios_base4InitC1Ev
U _ZNSt8ios_base4InitD1Ev
0000000000004020 B _ZSt3cin
可以发现 U _Z3LogPKc
代表了 Log
函数,此时运行 ./dyna_mian.out
就会出现报错:
error while loading shared libraries: libLog.so: cannot open shared object file: No such file or directory
解决动态库路径方法
(1)运行时指定动态库搜索路径
LD_LIBRARY_PATH=src ./dyna_mian.out
(2)编译时设置运行时路径
g++ main.cpp -Iinclude -Lsrc -lLog -Wl,-rpath=./src -o dyna_mian.out
此时可以使用 ./dyna_mian.out
直接运行,通过 readelf -d dyna_mian.out
显示可执行文件中的动态段信息,通过 RPATH
或 RUNPATH
字段可以查找到搜索路径。
0x000000000000001d (RUNPATH) Library runpath: [./src]
5. VS Code 与 CMake
插件安装
点击左侧扩展 → 搜索 C/C++ → 安装 C/C++ Extension Pack
其内部包含了 C/C++、C/C++ Themes、CMake、CMake Tools
示例一
从最简单的示例开始,此时可以使用经常看到的指令,可以使用 ctrl+`
打开内部终端:
mkdir build
cd build
cmake ..
make
./helloworld
但这是使用终端操作,删除之前的 build 文件夹后,在 VS Code 中可以点击上方运行 → 添加配置,此时左侧会多出一个 CMake 的按钮,如下图。我这里选择配置 GCC 9.4.0 x86_64-linux-gnu
。
点击底部状态栏的生成按钮便会自动在 build 目录下生成可执行文件。旁边有调试和运行按钮,如果有多个可执行文件,可以在上图中 CMake 的调试和启动选项中修改目标文件。
CMake 常用指令
# 指定 CMake 最小版本要求
cmake_minimum_required(VERSION 3.10)
# 定义工程名称
project(HELLOWORLD)
添加生成可执行文件
# 类似 g++ main.cpp -o helloworld
add_executable(helloworld main.cpp)
添加头文件搜索路径
# 相当于 g++ 中 -I, ${PROJECT_SOURCE_DIR} 代表当前工程的文件
include_directories(${PROJECT_SOURCE_DIR})
添加库文件搜索路径
# 相当于 g++ 中 -L
link_directories(/usr/local/cuda/lib64)
添加需要链接的共享库
# 相当于 g++ 中 -l
target_link_libraries(helloworld Log)
生成库文件
# 生成 libLog.so 动态库
add_library(Log SHARED src/Log.cpp)
# 生成 libLog.a 静态库
add_library(Log STATIC src/Log.cpp)
定义变量
# 定义 SRC 变量, 其值为 file1.cpp file2.cpp
set(SRC file1.cpp file2.cpp)
添加编译参数
add_compile_options(-Wall -std=c++11 -O2)
常用变量
# 编译选项 gcc: CMAKE_C_FLAGS
# 编译选项 g++: CMAKE_CXX_FLAGS
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
# 编译类型: CMAKE_BUILD_TYPE
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_BUILD_TYPE Release)
# 可执行文件输出存放路径: EXECUTABLE_OUTPUT_PATH
# 库文件输出存放路径: LIBRARY_OUTPUT_PATH
示例二
这里沿用 g++ 实战案例的代码,如下图,现在需要编写 CMakeLists,其思路感觉和使用 g++ 编译非常相似。
直接编译
参考之前直接编译的方式,只需要把头文件路径加进来,并且把两个 cpp 文件加入到可执行文件中即可。文章来源:https://www.toymoban.com/news/detail-836428.html
cmake_minimum_required(VERSION 3.10)
project(HELLOWORLD)
include_directories(${PROJECT_SOURCE_DIR}/include)
add_executable(helloworld main.cpp src/Log.cpp)
生成库文件
以生成动态库文件为例,库文件会默认生成在 build 文件夹下,这里我们用 lib 文件夹存放库文件,用 bin 文件夹存放可执行文件。
这里将代码按照 解决优化函数方法 修改代码后进行测试,发现可以直接正常运行,使用 readelf -d ./build/helloworld
查看后发现,自带了 RUNPATH
,这应该意味着内部生成的库文件路径会自动添加(也有可能和各种东西的版本有关),不需要手动使用 link_directories
添加库文件搜索路径。文章来源地址https://www.toymoban.com/news/detail-836428.html
cmake_minimum_required(VERSION 3.10)
project(HELLOWORLD)
include_directories(${PROJECT_SOURCE_DIR}/include)
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
add_library(Log SHARED src/Log.cpp)
add_executable(helloworld main.cpp)
target_link_libraries(helloworld Log)
到了这里,关于从零开始 TensorRT(5)C++ 篇:g++、CMake、VS Code 环境入门的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!