在Linux系统下用命令行编译调试C++

可能会下雨 / 2024-04-29 / 原文

在Linux系统下用命令行编译调试C++

目录
  • 在Linux系统下用命令行编译调试C++
    • 一、编译
      • 1. 单文件编译
      • 2. 多文件编译
      • 3. 链接第三方动态库
    • 二、调试
      • 1. 启动和退出
      • 2. 查看源代码:list/l
      • 3. 断点:breakpoint/br、watchpoint
      • 4. 单步、步入、跳出
      • 5. 计算表达式命令:expression/expr、p、po
      • 6. 操作内存:memory read/x、memory write
      • 7. 获取当前线程调用栈:thread backtrace/bt
      • 8. 一些进阶操作

总结 Ubuntu 下使用 clang 和 lldb 对 C++ 进行编译调试的常用命令。

贴一下系统及工具版本:

mayrain@PC-HOME-Y:~$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.4 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
mayrain@PC-HOME-Y:~$ cat /proc/version
Linux version 5.15.146.1-microsoft-standard-WSL2 (root@65c757a075e2) (gcc (GCC) 11.2.0, GNU ld (GNU Binutils) 2.37) #1 SMP Thu Jan 11 04:09:03 UTC 2024
mayrain@PC-HOME-Y:~$ clang++ --version
Ubuntu clang version 14.0.0-1ubuntu1.1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
mayrain@PC-HOME-Y:~$ lldb --version
lldb version 14.0.0

一、编译

这里仅介绍常用的单文件及多文件编译,包括第三方库的链接。复杂项目还是直接上IDE哈哈哈。

注意,ubuntu默认是不安装libc++的,我在网上没搜到有什么方便的安装方式,干脆自己编译一下,过程记录如下(参考llvm官网编译指引):

git clone https://github.com/llvm/llvm-project.git ./llvm

cd llvm

mkdir build

cmake -G Ninja -S runtimes -B build -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi;libunwind"

ninja -C build cxx cxxabi unwind

ninja -C build check-cxx check-cxxabi check-unwind 

ninja -C build install-cxx install-cxxabi install-unwind

1. 单文件编译

将源文件 main.cpp 编译为可执行文件 foo:

clang++ -Wall -g -std=c++11 -stdlib=libc++ main.cpp -o foo

2. 多文件编译

首先准备三个文件:

add.h

int add(int a, int b);

add.cpp

#include <iostream>

using namespace std;

int add(int a, int b)
{
	int ret = a + b;
	cout << "sum of " << a << " and " << b << " is " << ret << endl;
	return ret;
}

main.cpp

#include <iostream>
#include <string>
#include <vector>
#include <set>
#include "add.h"

using namespace std;

template <typename T>
void print(const T& v)
{
    for (auto iter = v.begin(); iter != v.end(); iter++)
    {
        cout << *iter << " ";
    }
    cout << endl;
}

int main(int argc, char** argv) 
{
    cout << "hello lldb!" << endl;
    
    int i = 42;
    int *pi = &i;
    
    int i2 = 15;
    int i3 = add(i, i2);
    
    string s("A lazy dog jumps over the fox");
    string *ps = &s;

    vector<int> iv = {0,1,2,3,4,5,6,7,8,9};
    
    set<int> is;
    is.insert(0);
    is.insert(42);

    print(iv);
    print(is);

    return 0;
}

将以上代码编译为可执行文件 foo:

clang++ -Wall -g -std=c++11 -stdlib=libc++ main.cpp add.cpp -o foo

编译器默认会在当前目录中寻找头文件,所以这里无需单独指定包含目录。

3. 链接第三方动态库

以 opencv 为例:

#include <opencv2/opencv.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>
#include <string>

using namespace std;
using namespace cv;

int main()
{
    string path = "./cv_test.png";
    Mat img = imread(path);
    namedWindow("cv_test", 0);
    resizeWindow("cv_test", 1024, 768);
    imshow("cv_test", img);
    waitKey(0);
    return 0;
}

编译以上示例程序的命令为:

clang++ -Wall -g -std=c++11 -stdlib=libstdc++ -I /usr/local/include/opencv4 -L /usr/local/lib -lopencv_core -lopencv_highgui -lopencv_imgproc -lopencv_imgcodecs main.cpp -o foo

为什么改成 -stdlib==libstdc++ ?

因为 opencv 库是使用 libstdc++ 进行编译的,如果两者不一致,会报cv::imread函数找不到定义。细节可以看这篇文章


二、调试

本文仅列举常用调试命令,遇到不清楚的用法可以使用以下命令看说明:

lldb --help

也可以在lldb交互命令行中使用help [命令]获取某个具体命令的帮助信息:

image-20240429104935502

以下内容用本文三.2节的三个源文件作为调试demo。

1. 启动和退出

两种启动方式:

image-20240429104121541

image-20240429104148178

退出也有两种方式:

exitquit

2. 查看源代码:list/l

查看从给定行号开始的10行代码:

list [行号]l [行号]

按回车可以显示下边10行

image-20240429104214795

查看函数代码:

l [函数名]

image-20240429105055162

3. 断点:breakpoint/br、watchpoint

在函数起始处设置断点:

br set --name [函数名]

设置完断点,run运行foo即可

image-20240429113017798

在指定行设置断点:

br set --line [行号]

因为此时程序还断在main函数的开头,所以输入continuec让程序继续

image-20240429113044684

在指定文件的指定行设置断点:

br set --file [文件名] --line [行号]

image-20240429113119146

查看断点列表:

br list

image-20240429113800403

删除某个/多个断点:

br delete [断点序号1] [断点序号2] [断点序号3]

br delete [n-m]

image-20240429113735035

删除所有断点:

br delete

启用/禁用断点:

br enable [序号]

br disable [序号]

image-20240429113700209

内存断点:watchpoint/watch(当内存发生读/写操作时触发)

常用的条件断点:

watch set var [变量名]

watch modify -c '([变量名]==[值])'

image-20240429134648045

image-20240429134758843

删除、启用/禁用操作和breakpoint类似。

4. 单步、步入、跳出

单步 Step-over:

nextn

步入 Step-in:

steps

跳出 Step-out:

finish

操作截图:

add函数调用行设置并触发断点

image-20240429114330482

步入add

image-20240429114400611

单步

image-20240429114423759

跳出add,可以看到lldb会把返回值列出来

image-20240429114519795

修改函数返回值:

thread return [表达式]

image-20240429130419709

5. 计算表达式命令:expression/expr、p、po

pexpr --的简写,poexpr -o --的简写,它们会把参数编译并打印输出,两者仅是格式不同。

查看变量的值:

p [变量名]po [变量名]

image-20240429121525388

指定输出格式/做进制转换:

p/[格式] [变量名]

image-20240429123936865

修改变量值:

expr [变量名]=[值]

frame variable查看变量前后变化

image-20240429121821052

6. 操作内存:memory read/x、memory write

读内存:

x/[读取个数][字节数][格式] [内存地址]

[读取个数]:整数

[字节数]:b-1Byte、h-2Byte、w-4Byte、g-8Byte

[格式]:x-16进制、f-浮点数、d-10进制

image-20240429125742696

写内存:

memory write [内存地址] [值]

image-20240429125927396

7. 获取当前线程调用栈:thread backtrace/bt

image-20240429130239747

8. 一些进阶操作

显示汇编代码:disassemble/d

d -b

d --frame

d --name [函数名]

d -a [内存地址]

d -s [内存地址]

寄存器操作:register read、register write

image-20240429131328156