c/c++程序的编译和调试--gcc,make,cmake,gdb

337 阅读9分钟

gcc/g++手动编译

gcc通常用来编译c文件,g++通常用来编译c++文件.gcc/g++的常用选项的含义

选项功能
-o指定生成的可执行文件的文件名
-c仅仅编译,但是不进行链接,如果文件没有定义main函数就需要使用该选项
-llibrarylibrary表示要搜索的库文件名称.该选项用于指定手动链接环节中需要调用的库文件,如libcrypto
-Llibdicdic表示库文件的目录
-Ihdichdic表示要导入的头文件所在的目录
-share尽量使用动态库
-static禁止使用动态库
-w不生成任何警告信息
-Wall显示所有的警告信息
-g编译时添加调试信息

makefile文件进行编译

如果用gcc,需要手动的进行编译,如果有成百上千个c文件,那么手动输入命令十分麻烦。因此一个思路是,通过在一个文本文件中说明对哪些需要执行什么操作,需要使用到哪些以来,然后根据这个文件自动的生成命令区编译c文件。根据这种思路,我们把这种写明了编译信息的文件就称为makefie,通过make命令就根据目录下的makefile文件自动生成编译命令进行编译.

makefile文件的基本结构

规则的基本组成

makefile文件由规则组成,每条规则明确:

  1. 构建目标的前置条件
  2. 如何构建
 <target> : <prerequisites> 
 [tab]  <commands>
部件含义可选值Example
target指明Make命令要构建的对象文件名,如果目标是多个文件名,文件名之间用空格隔开
操作名,成为伪目标(phony target),如clean操作。 通常需要使用.PHONY:OP命令表示,OP是一个操作,而不是一个文件.PHONY: clean clean: rm *.o temp
prerequisites指明了目标是否需要重新构建。如果依赖有更新就重新构建。一组文件,有空格隔开
commands表示如何更新目标文件,由一行或多行shell命令组成shell命令前需要使用一个*Tab键。每行命令在一个单独的shell中执行,shell之间没有继承关系。

顺序执行命令的三种方式

command默认是并行执行的,没有继承关系,如果要让命令顺序执行,可以采用以下三种方式

方式示例
合并写,用分号隔开var-kept: export foo=bar; echo "foo=[$$foo]"
用分号,在换行符号前加转义符var-kept: export foo=bar; \ echo "foo=[$$foo]"
前置ONESHELL命令.ONESHELL: var-kept: export foo=bar; echo "foo=[$$foo]"

makefile的基本语法

写法含义
@command关闭回声测试(echo),即终端不会打印各条要执行的命令
%.o : %.c等同于f1.o : f1.c,%表示一个相同的部件
txt = Hello World test: @echo $(txt); \ @echo $$HOME使用$引用自定义的变量.使用$$引用shell变量
$@指代当前的target
$<指代第一个prerequisity.(前置条件)
$?指代所有前置条件
$*指代%表示的内容
$(@D) 和 $(@F)$(@D)$(@F) 分别指向 $@ 的目录名和文件名。比如,$@src/input.c,那么$(@D) 的值为 src$(@F) 的值为 input.c
D和F放在目标后,表示对象所在文件夹和文件名

Makefile中调用shell中的各种函数,其语法为

 $(function arguments)
 # 或者
 ${function arguments}
常用函数作用示例
shell执行shell命令srcfiles := $(shell echo src/{00..99}.txt)
wildcard替换Bash的通配符srcfiles := $(wildcard src/*.txt)
subst进行文本替换$(subst from,to,text),在text文本中,将from替换成to
patsubst模式匹配的替换$(patsubst pattern,replacement,text)

make的常用命令

 make VERBOSE=1 # 详细打印make过程中的各种命令,目录等信息,verbose:冗长的

使用cmake生成Makefile文件

CMakeLists.txt 的一般结构和编译的一般过程

不同平台,makefile文件不同,cmake就是使用同一文件,生成不同平台的makefile文件的工具。cmake需要CMakeLists.txt文件。(注意:名字只能是这个,不能叫其他)

 #要求的cmake的最小版本
 cmake_minimum_required(VERSION 3.10)
 ​
 #项目名称
 project(hello) # ${PROJECT_NAME}
 ​
 #include
 ​
 #link
 ​
 #执行规则:指定生成的可执行文件的名称和相关源文件
 add_executable(hello.o main.cpp printhello.cpp)

一般的编译过程:

  1. 因为使用**cmake**进行编译会生成很多中间文件,因此一般需要建立一个build目录来存放:
  2. 使用cmake ..命令,在父目录寻找CMakeLists.txt, 在当前目录生成Makefile文件
  3. 使用make命令进行编译和链接
 mkdir build && cd build
 cmake ..
 make

include

 #set(SOURCES
 #    src/Hello.cpp
 #    src/main.cpp
 #)
 file(GLOB SOURCES "src/*.cpp") # 替代上面的内容,glob命令:使用正则表达式查找文件
 # target_include_directories()函数将头文件所在目录转意成 -I/path的形式
 target_include_directories(target
     PRIVATE
         ${PROJECT_SOURCE_DIR}/include
 )

链接库

 # 添加一个静态链接库,名字叫做hello_library,使用src/hello.cpp来生成
 add_library(hello_library STATIC
     src/Hello.cpp
 )
 # 添加一个共享链接库
 add_library(hello_library SHARED
     src/Hello.cpp
 )
 #  给链接库起别名
 add_library(hello::library ALIAS hello_library)
 # 将链接库链接到可执行文件
 target_link_libraries(hello_binary
     PRIVATE
         hello::library
 )
规则含义类比
add_library(libmy.a mylib.cpp)使用mylib.cpp生成一个叫做libmy.a的库相当于gcc -c
add_executable(main.o main.cpp)使用main.cpp生成一个叫做main.o的可执行文件相当于gcc -o
add_subdirectory(subModuleDir)表示添加一个子模块,子模块中包含CMakeLists.txt和源代码,子模块的路径为./subModuleDir
target_link_libraries(target source)
target_include_directories
set(key value)定义一个变量

link和include中关PUBLICPRIVATE的含义

假设A→B→C,如果编译B的可执行文件写的是:target_link_libraries(B PUBLIC C),则表示C是B的public成员,提供对外访问服务.即A也能调用C中定义的函数.

PRIVATE:表示被链接的库是私有属性,因此不提供对外接口,只能B使用,A不能使用

静态库与共享库(动态链接库)

共享库其实就是动态链接库

cmake中的一些变量

VariableInfo
CMAKE_SOURCE_DIRThe root source directory
CMAKE_CURRENT_SOURCE_DIRThe current source directory if using sub-projects and directories.
PROJECT_SOURCE_DIRThe source directory of the current cmake project.
CMAKE_BINARY_DIRThe root binary / build directory. This is the directory where you ran the cmake command.
CMAKE_CURRENT_BINARY_DIRThe build directory you are currently in.
PROJECT_BINARY_DIRThe build directory for the current project.

make install设置

install()函数的一般使用方法:

 install(TARGETS targets... [EXPORT <export-name>]
         [RUNTIME_DEPENDENCIES args...|RUNTIME_DEPENDENCY_SET <set-name>]
         [[ARCHIVE|LIBRARY|RUNTIME|OBJECTS|FRAMEWORK|BUNDLE|           PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE|FILE_SET <set-name>|CXX_MODULES_BMI]
          [DESTINATION <dir>]
          [PERMISSIONS permissions...]
          [CONFIGURATIONS [Debug|Release|...]]
          [COMPONENT <component>]
          [NAMELINK_COMPONENT <component>]
          [OPTIONAL] [EXCLUDE_FROM_ALL]
          [NAMELINK_ONLY|NAMELINK_SKIP]
         ] [...]
         [INCLUDES DESTINATION [<dir> ...]]
         )

使用cmake进行安装的一般语法:

注意:cmake_install_prifix前缀需要加在顶层cmakelists.txt文件中

 # 设置安装路径,Linux下默认是/usr/local
 SET(CMAKE_INSTALL_PREFIX ${PROJECT_BINARY_DIR})
 ​
 ​
 # Install
 ############################################################
 ​
 # Binaries
 install (TARGETS cmake_examples_inst_bin DESTINATION bin)
 ​
 # Library
 # Note: may not work on windows
 install (TARGETS cmake_examples_inst
     LIBRARY DESTINATION lib)
 ​
 # Header files
 install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/ DESTINATION include)
 ​
 # Config
 install (FILES cmake-examples.conf DESTINATION etc)

卸载安装:

在当前目录(即使用make install命令的当前路径下),输入sudo xargs rm < install_manifest.txt命令即可从路径中删除安装的库

build type

通过命令手动设置构建类型

通过cmake命令的-DCMAKE_BUILD_TYPE=XXX参数手动设置编译的4种类型。

  • Release
  • Debug
  • MinSizeRel
  • RelWithDebInfo
 cmake .. -DCMAKE_BUILD_TYPE=Release

设置默认的build type

通过在CMakeLists.txt中添加下列语句设置默认的build type

 if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
   message("Setting build type to 'RelWithDebInfo' as none was specified.")
   set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE)
   # Set the possible values of build type for cmake-gui
   set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release"
     "MinSizeRel" "RelWithDebInfo")
 endif()

第三方库

使用find()函数在${CMAKE_MODULE_PATH}下查找第三方库。CMAKE_MODULE_PATH的默认值在Linux下为/usr/share/cmake/Modules

关于编译时的一些注意事项

关于c和c++混合编程

c和c++混合编程的时候,需要注意由此引发的文件链接问题。即cpp文件调用c文件时,出现未定义的问题。例如有:a.h,a.c,b.cpp三个文件,如果b.cpp使用了#include "a.h"调用a.h,在使用cmake和make进行编译的时候有可能报错为:

image-20230302153822082

解决方法,

  1. 如果是cpp文件调用c文件,则直接将c的后缀名改成cpp就行

  2. 使用extern "c"语句,使用c语言的规范导入头文件,如

     #ifdef _cplusplus
     // _cplusplus宏是c++标准中定义的一个宏,用于表示c++的版本
     extern "c"{
         #include "a.h";
     }
     #endif
    

关于文件main函数

1. 没有main函数的文件编译

  • gcc编译器编译时报错:/usr/lib/gcc/x86_64-linux-gnu/5/…/…/…/x86_64-linux-gnu/crt1.o:在函数‘_start’中:(.text+0x20):对‘main’未定义的引用
  • gcc默认编译并链接成可执行文件,如果c文件中没有写main方法的话,表示这是一个静态库而是一个可执行文件.此时可以添加-c参数表明要生成一个库

2. 多文件调用的编译

假如a.ha.cpp,b.cpp调用了a.h,则在编译的时候,b.cpp就需要添加a.cpp的依赖。此外,还需要注意,a.cppb.cpp中只能有一个main函数,否则会报错:链接失败。

使用gdb进行调试

过程

  1. 编译时加上-g选项
  2. 启动gdb时加上-q选项,屏蔽gdb的免责条款
  3. 使用命令进行调试

gdb的基本命令:

调试指令作 用
(gdb) break xxx (gdb) b xxx在源代码指定的某一行设置断点,其中 xxx 用于指定具体打断点的位置。
clear num清除某行的断点
(gdb) run (gdb) r执行被调试的程序,其会自动在第一个断点处暂停执行。
(gdb) continue (gdb) c当程序在某一断点处停止运行后,使用该指令可以继续执行,直至遇到下一个断点或者程序结束。
(gdb) next (gdb) n令程序一行代码一行代码的执行。
(gdb) print xxx (gdb) p xxx打印指定变量的值,其中 xxx 指的就是某一变量名。
display每次执行打印指定变量的值
(gdb) list (gdb) l显示源程序代码的内容,包括各行代码所在的行号。
(gdb) quit (gdb) q终止调试。
finish跳出函数

打断点

break--打普通断点

watch命令-打观察断点-监控变量值的变化

使用 GDB 调试程序的过程中,借助观察断点可以监控程序中某个变量或者表达式的值,只要发生改变,程序就会停止执行。

watch的语法为:watch variable 查看当前的观察点:info watchpoints

catch-打捕捉断点

而捕捉断点的作用是,监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标时间发生,则程序停止执行。

 (gdb) catch event

常见的事件有:

event 事件含 义
throw [exception]当程序中抛出 exception 指定类型异常时,程序停止执行。如果不指定异常类型(即省略 exception),则表示只要程序发生异常,程序就停止执行。
catch [exception]当程序中捕获到 exception 异常时,程序停止执行。exception 参数也可以省略,表示无论程序中捕获到哪种异常,程序都暂停执行。
load [regexp] unload [regexp]其中,regexp 表示目标动态库的名称,load 命令表示当 regexp 动态库加载时程序停止执行;unload 命令表示当 regexp 动态库被卸载时,程序暂停执行。regexp 参数也可以省略,此时只要程序中某一动态库被加载或卸载,程序就会暂停执行。

条件断点

使用if cond,做条件判断,仅当满足一定条件时才打断点.

 (gdb) break ... if cond

删除断点

通过 delete命令可以删除断点。使用delete NUM的方式删除序号为NUM的断点

image-20230302215526179

单步调试

命令特点
s步入被调用函数内部
n执行当前行代码,不进入函数内部
u执行到指定行
finish跳出当前函数

s命令

n命令

u命令

 (gdb) until
 (gdb) until location
  1. 可以使 GDB 调试器快速运行完当前的循环体,并运行至循环体外停止注意,until 命令并非任何情况下都会发挥这个作用,只有当执行至循环体尾部(最后一行代码)时,until 命令才会发生此作用;反之,until 命令和 next 命令的功能一样,只是单步执行程序。
  2. 通过执行 until 19 命令,GDB 调试器直接执行至指定的第 19 行。

变量信息查看

数据的查看格式

符号说明
s以字符串形式进行输出

查看内存

查看变量

p(print)命令

 (gdb) print num
 (gdb) p num

num:目标变量或者表达式

display命令

使用 display 命令查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 调试器都会自动帮我们打印出来,而 print 命令则不会。

 (gdb) display expr
 (gdb) display/fmt expr

其中,expr 表示要查看的目标变量或表达式;参数 fmt 用于指定输出变量或表达式的格式,表 1 罗列了常用的一些 fmt 参数。

/fmt功 能
/x以十六进制的形式打印出整数。
/d以有符号、十进制的形式打印出整数。
/u以无符号、十进制的形式打印出整数。
/o以八进制的形式打印出整数。
/t以二进制的形式打印出整数。
/f以浮点数的形式打印变量或表达式的值。
/c以字符形式打印变量或表达式的值。

注意,display 命令和 /fmt 之间不要留有空格。以 /x 为例,应写为 (gdb)display/x expr

事实上,对于使用 display 命令查看的目标变量或表达式,都会被记录在一张列表(称为自动显示列表)中。通过执行info dispaly命令,可以打印出这张表.

display变量的删除,禁用,和激活

功能命令
删除display变量undisplay num...
delete display num...
禁用displaydisable display num...
激活禁用的displayenable display num...

p和display命令的区别

使用 1 次 print 命令只能查看 1 次某个变量或表达式的值,而同样使用 1 次 display 命令,每次程序暂停执行时都会自动打印出目标变量或表达式的值。因此,当我们想频繁查看某个变量或表达式的值从而观察它的变化情况时,使用 display 命令可以一劳永逸

Q:如何使用<>导入自己写的头文件而不是使用"",mvfst项目中使用的就是<>

通过-I或者cmake中的target_include_directories()函数就可以使用<>替代,当然,使用""也是可以的