跳转至

调试工具

在日常开发需要使用gdb调试程序,如打印变量,查看内存以及汇编,生成core文件等,本文分享平时调试中常见的方法。

gdb的使用

  • gdb-server
  • x命令 如:gdb x/150x,gdb x/i
  • 查看源码
  • ctrl-x-a 或者source layout,查看源码
  • list 显示代码
  • ulimit -c unlimit,cat/proc/sys/kernel/core_pattern 查看core文件保存位置
  • echo “/home/coresave/core.%e.%p.%t” > /proc/sys/kernel/core_pattern 设置core文件保存地址
  • info inferiors是查看当前的子进程
  • pstree -p

gdb调试程序方法论

  • 首先编译程序,需要在可执行程序文件以及动态库中编入调试信息,设置编译选项,可以通过cmakelist文件设置,也可以直接设置gcc等参数,比如-g,-Wall等,设置程序的优化等级为-O0

Text Only
1
2
3
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g")
add_definitions(-std=c++11 -Wall -g -O0)
也可以编译的时候,cmake指定DEBUG,cmake .. -DCMAKE_BUILD_TYPE=Debug。

  • 注释一些Cmake可见性选项
Text Only
# set(CMAKE_CXX_VISIBILITY_PRESET default)
# set(CMAKE_VISIBILITY_INLINES_HIDDEN 0)
  • 调试程序,远程调试使用gdbserver(如果是板子上cpu架构不同,使用开发机上交叉编译环境的gdb和gdbserver),开发机上运行的程序直接使用gdb
Text Only
1
2
3
4
5
6
# 不带参数
gdb main
# 带参数
gdb --args main arg1 arg2
# attach 程序
gdb -p ${port}
  • 断点,断点可以使用文件名加行号,也可以函数,用b断点(breakpoints的缩写),用disable禁用断点,delete删除断点
Text Only
(gdb) b hello.cpp:12 # hello.cpp文件的12行
(gdb) b func1 # 在func1开始位置断点
  • 执行程序,包括开始运行、继续到下一个断点、单步运行、进入函数
Text Only
1
2
3
4
5
(gdb) r # 开始运行
(gdb) c # 继续到下一个断点
(gdb) n # 单步运行
(gdb) n 5 # 单步运行5步
(gdb) step # 进入函数
  • 打印变量,查看内容,info、list、p命令、x命令
Text Only
1
2
3
4
5
(gdb) p str1.size()
(gdb) p str1
(gdb) x/32bc str1.c_str() # 打印32个byte大小的char字符,从str1.c_sr()指针位置开始
(gdb) x/32bt str1.c_str() # 打印32个byte大小的二进制,从str1.c_sr()指针位置开始
# x命令还可以通过i打印寄存器内容、address打印地址、f打印float等。
  • 查看栈帧
Text Only
(gdb) bt # 查看栈帧
(gdb) frame 1 # 查看1的函数栈
  • 反汇编 disassemb
  • 设置ulimit -c unlimit 生成core文件,gdb调试core文件
Text Only
gdb main core_file

其它调试

还有其他一些常见的调试方法,在日常开发中可以帮助更加快速的发现问题。比如

  • c++程序使用了动态链接库.so做功能扩展,如何调试动态链接库,断点进入so源文件?
  • Python调用c语言动态链接库如何调试,这个在做ffi跨语言调用时很常见,比如ctypes load so文件?
  • python调试除了使用gdb,还可以使用pdb
  • python -m pdb main.py
  • 在clion中调试环境很强大,很方便调试,但是在vscode中如何配置调试环境呢?

bin 文件查看方法

  • 比较bin文件

python3 compare_bin.py 1.bin 2.bin

Text Only
import numpy as np
import argparse
parser = argparse.ArgumentParser(description="your script description")
#parser.add_argument('--verbose', '-v', action='store_true', help='verbose mode')
parser.add_argument("file1")
parser.add_argument("file2")
args = parser.parse_args()
print(args)
print(args.file1)
print(args.file2)

arr_file1 = np.fromfile(args.file1, dtype="float16")
arr_file2 = np.fromfile(args.file1, dtype="float16")
print(np.allclose(arr_file1, arr_file2))
  • 查看bin文件的size bytes
Text Only
ls -ll xxx.bin
  • od工具
Text Only
1
2
3
4
od -t fF -N 32 xxx.bin
-t fF : 按照float32打印
-N 32 : 读取32个字节
-t fD : 按照float64打印
  • 保存ndarray to img
Text Only
from PIL import Image
import numpy as np

height = 500
weight = 500
channel = 3
img_numpy = np.zeros((height, weight, channel), dtype=np.uint8)
img = Image.fromarray(img_numpy, "RGB")
# Display the Numpy array as Image
img.show()

# Save the Numpy array as Image
image_filename = "opengenus_image.jpeg"
img.save(image_filename)
  • 保存ndarray to bin file
Text Only
arr.tofile("out.bin")

设置gdb源码目录

GDB技巧 当再开发机上编译好调试程序后,scp到目标机上,使用gdb调试,但是因为可执行文件更改了目录。导致gdb找不到源代码路径。 可以通过:

Text Only
1
2
3
4
5
(gdb)pwd # 查看当前路径
(gdb)show directories # 查看源码搜索路径 
(gdb)dir /new_path/src # /new_path/src 是目标机上的源码路径,gdb就会去这个目录搜索源码
(gdb)info source # 可以查看当前源码信息,其中也包含编译信息
(gdb)set substitute-path from_path to_path # 可以让你修改源码搜索路径,把源码绝对路径里面的一个path映射到另一个path上去

交叉编译gdb和gdbserver

为了远程调试,开发机(x86)和目标机(arm64),需要编译开发机上运行的gdb,和目标机上运行的gdbserver,并且它们的版本需要一致。 首先为了,编译gdbserver,因为是在目标机上运行的,所以需要有交叉编译环境,比如:/proc/arm-linux/…/bin目录下,存在交叉编译使用的gcc等工具。 https://developer.aliyun.com/article/243857

  • 下载gdb8.1.1(版本不是越新越好)

下载地址:http://ftp.gnu.org/gnu/gdb/

Text Only
wget http://ftp.gnu.org/gnu/gdb/gdb-8.1.1.tar.gz
  • 编译gdb(使用x86的gcc编译)
Text Only
#!/bin/bash

cd gdb-8.1.1/

# aarch64-mix210-linux 参考交叉编译工具命名
./configure --program-prefix=`aarch64-mix210-linux-` \
    --target=aarch64-mix210-linux \
    --prefix=`pwd`/out

make -j2
make install
  • 编译gdbserver (使用交叉编译工具中的aarch64-mix210-linux-gcc编译)
Text Only
#!/bin/bash

cd gdb-8.1.1/gdb/gdbserver
export CC=/opt/linux/x86-arm/aarch64-mix210-linux/bin/aarch64-mix210-linux-gcc

./configure \
    --host=aarch64-mix210-linux \
    --target=aarch64-mix210-linux \
    --prefix=`pwd`/out

make -j2
make install
  • 如果gdb和gdbserver文件size太大,可以使用strip工具,但注意strip gdbserver时要使用交叉编译工具中的aarch64-mix210-linux-strip
  • scp或者nfs将gdbserver传送到目标机上

addr2line

http://lazybing.github.io/blog/2016/12/22/addr2line-use/ 拒绝超大coredump - 用backtrace和addr2line搞定异常函数栈 https://linux.die.net/man/3/backtrace_symbols

Text Only
#include <stdlib.h>
#include <stdio.h>
#include <stddef.h>
#include <signal.h>
#include <execinfo.h>

// 0: GENERATE COREDUMP FILE 
// 1: PRINT STACK BY SELF
int g_iTestFlag = 1;
#define ADDR_MAX_NUM 100

void CallbackSignal (int iSignalNo) {
    printf ("CALLBACK: SIGNAL:\n", iSignalNo);
    void *pBuf[ADDR_MAX_NUM] = {0};
    int iAddrNum = backtrace(pBuf, ADDR_MAX_NUM);
    printf("BACKTRACE: NUMBER OF ADDRESSES IS:%d\n\n", iAddrNum);
    char ** strSymbols = backtrace_symbols(pBuf, iAddrNum);
    if (strSymbols == NULL) {
        printf("BACKTRACE: CANNOT GET BACKTRACE SYMBOLS\n");
        return;
    }
    int ii = 0;
    for (ii = 0; ii < iAddrNum; ii++) {
        printf("%03d %s\n", iAddrNum-ii, strSymbols[ii]);
    }
    printf("\n");
    free(strSymbols);
    strSymbols = NULL;
    exit(1); // QUIT PROCESS. IF NOT, MAYBE ENDLESS LOOP.
}

void FuncBadBoy() {
    void* pBadThing = malloc(1024*1024*256);
    free (pBadThing);
    free (pBadThing);
}

void FuncBadFather() {
    FuncBadBoy();
}

int main(int argc, char **argv){
    if (g_iTestFlag) {
        signal(SIGSEGV, CallbackSignal);
    }
    FuncBadFather();   
    return 0;
}
Text Only
#include <stdio.h>

int div(int numerator, int denominator)
{
    return numerator / denominator;
}

int main(int argc, char **argv)
{
    int numerator   = 10;
    int denominator = 0;

    return div(numerator, denominator);
}

Text Only
gcc -o div -g div.c
使用addr2line 查看一个dmsg打印的地址看不到函数和行号,显示??,那么因为地址被重定位了,比如 dmesg打印:

Text Only
[201297.280078] traps: div[41537] trap divide error ip:55e1e0c947f8 sp:7ffc8f750470 error:0 in div[55e1e0c94000+1000]
http://tangxinfa.github.io/article/linux-4e0b8c038bd5526553bb8c038bd54fe1606f76847a0b5e8f5d296e83.html

Text Only
1
2
3
addr2line -e ./div -f 55e1e0c947f8
??
??:0
使用7f8就可以查看了:

Text Only
1
2
3
addr2line -e ./div -f 7f8
_Z3divii
/home/xxx/div.c:5
这样我们通过在生产环境使用gdb提取崩溃调用栈以及地址映射空间,就可以在调试环境进行源代码级的问题定位,而不需要从生产环境取得 core 文件。

objdump

Text Only
1
2
3
4
commond to output all the functions in a lib
objdump -Tt ./pavaro/baidu/vp-lab/pavaro/dependency/linux-arm/pavaro/lib/libpavaro.so | ag boost |  awk '{print $7}' | c++filt

objdump -CTt ./pavaro/baidu/vp-lab/pavaro/dependency/linux-arm/pavaro/lib/libpavaro.so | ag boost |  awk '{print $7}'

gprof

性能分析工具,通过gcc编译程序时,加入-pg选项 然后使用gprof工具分析 超级方便的Linux自带性能分析工具!gprof介绍、安装、使用及实践

uftrace

跟踪函数调用图 https://github.com/namhyung/uftrace/blob/master/doc/uftrace-live-demo.gif

Symbols

http://arthurchiao.art/blog/linux-tracing-basis-zh/

Perf火焰图的使用

https://www.cnblogs.com/happyliu/p/6142929.html

Text Only
1
2
3
4
5
sudo perf record -F 99 -p PID -g -- sleep 60
-F 99: 每秒采样99次
-p PID: 指定进程id
-g: 记录调用栈
sleep 60: 持续60秒, 如果不加则一直采样到手动中断(CRTL+C)为止

嵌入式开发工具

嵌入式开发杂谈_Stoneshen1211的博客-CSDN博客 results.size() 是unsigned类型,这段代码,compare的时候会发生int到unsigned的转换,导致进入循环。

Text Only
1
2
3
4
std::vector<int> results;
for (int index = 0; index < results.size() - 1; index++) {
    std::cout <<"enter index is : " << index << "\n";
}

gdb交叉编译环境调试core文件

image.png bt找不到函数和文件名,设置solib-search-path core-file # 清空core文件 core-file ./corexxxx # 重新设置core文件