Linux c c++ 开发调试技巧

时间:2021-12-24 06:50:19

看到一篇介绍 linux c/c++ 开发调试技巧的文章,感觉挺使用,哪来和大家分享。

通向 UNIX 天堂的 10 个阶梯
Author: Arpan Sen, 高级技术人员, Systems Documentation, Inc. (SDI)

讨论几种可以帮助 C++ 开发人员节省时间的技巧和免费工具。

C++ 开发人员在日常工作中通常要完成多个任务:开发新软件、调试其他人的代码、制订测试计划、为每个计划开发测试、管理衰退软件(regression suite)等等。在多种角色之间频繁转换会消耗大量宝贵的时间。为了帮助缓解这个问题,本文提供 10 种能够有效提高生产力的方法。本文中的示例使用 tcsh version 6,但是提供的思想适用于所有 UNIX? shell 变体。本文还介绍可以在 UNIX 平台上使用的几种开放源码工具。

保证数据安全

在 shell 提示上使用 rm -rf * 或其变体可能是导致 UNIX 开发人员丢失数小时工作成果的最常见的原因。有几种方法可以解决这个问题:通过在 $HOME/.aliases 中使用 alias rm、alias cp 或alias mv,把这些命令连接到它们的交互式版本,然后在系统启动时引用这个文件。根据登录 shell的不同,这意味着把 source $HOME/.aliases 放在 .cshrc(对于 C/tcsh shell)或 .profile 或.bash_profile(对于 Bourne shell)中,见清单 1。

清单 1. 为 rm、cp 和 mv 设置别名

alias rm 'rm –i'
alias cp 'cp –i'
alias mv 'mv –i'

对于 tcsh shell 的用户还有另一种方法,在启动脚本中添加以下行:

set rmstar on

在设置了 rmstar 变量的情况下,如果发出 rm * 命令,系统会提示您确认,见清单 2。

清单 2. 在 tcsh 中使用 rmstar shell 变量

arpan@tintin# pwd
/home/arpan/IBM/documents
arpan@tintin# set rmstar on
arpan@tintin# rm *
Do you really want to delete all files? [n/y] n

但是,如果使用带 -f 选项的 rm、cp 或 mv 命令,那么会抑制交互模式。更有效的一种方法是,为这些 UNIX 命令创建自己的版本,并使用 $HOME/.recycle_bin 这样的预定义文件夹保存删除的数据。清单 3 给出一个称为 saferm 的示例脚本,它只接受文件和文件夹名。

清单 3. saferm 脚本

#!/usr/bin/tcsh

if (! -d ~/.recycle_bin) then
mkdir ~/.recycle_bin
endif

mv $1 ~/.recycle_bin/$1.`date +%F`

自动备份数据

恢复数据需要全面的策略和措施。根据需求的不同,数据备份可以在每天夜间执行,也可以每隔几小时执行一次。在默认情况下,应该使用 cron 工具备份 $HOME 及其所有子目录,并把备份保存在预先指定的文件系统区域中。注意,应该只有系统管理员对备份数据有 读(Write) 或 执行(Execute)的权限。下面的 cron 脚本简要演示备份的设置:

0 20 * * * /home/tintin/bin/databackup.csh

这个脚本在每天 20:00 备份数据。数据备份脚本见清单 4。

清单 4. 数据备份脚本

cd /home/tintin/database/src
tar cvzf database-src.tgz.`date +%F` database/ main/ sql-processor/
mv database-src.tgz.`date +%F` ~/.backup/
chmod 000 ~/.backup/database-src.tgz.`date +%F`

另一种策略是在网络中维护一些名称简单明了的文件系统区域,比如/backup_area1、/backup_area2 等等。希望备份数据的开发人员应该在这些区域中创建目录或文件。另外,一定要注意一点:与 /tmp 相似,这种区域必须打开 sticky 位。

浏览源代码

使用可免费下载的 cscope 实用程序(见 参考资料 中的链接)是发现和浏览现有源代码的好方法。cscope 需要一个文件列表(C 或 C++ 头文件、源代码文件、flex 和 bison 文件、内联源代码[.inl] 文件等等),以便创建它自己的数据库。创建数据库之后,它会在一个简洁的界面中列出源代码。清单 5 演示如何构建并调用 cscope 数据库。

清单 5. 使用 cscope 构建并调用源代码数据库

arpan@tintin# find . –name “*.[chyli]*” > cscope.files
arpan@tintin# cscope –b –q –k
arpan@tintin# cscope –d

cscope 的 -b 选项让它创建内部数据库;-q 选项让它创建索引文件以加快搜索;-k 选项让 cscope在搜索时不考虑系统头文件(否则,即使是最简单的搜索,也会产生大量结果)。

使用 -d 选项调用 cscope 界面,见清单 6。

清单 6. cscope 界面

Cscope version 15.5                                               Press the ? key for help

Find this C symbol:
Find this global definition:
Find functions called by this function:
Find functions calling this function:
Find this text string:
Change this text string:
Find this egrep pattern:
Find this file:
Find files #including this file:

按 Ctrl-D 退出 cscope。使用 Tab 键在数据 cscope 列表和 cscope 选项(例如,Find C Symbol和 Find file)之间切换。清单 7 给出在搜索名称包含 database 的文件时的屏幕快照。按 0、1等分别查看各个文件。

清单 7. 在搜索名为 database 的文件时 cscope 的输出

Cscope version 15.5                                               Press the ? key for help

File
0 database.cpp
1.database.h
2.databasecomponents.cpp
3.databasecomponents.h

Find this C symbol:
Find this global definition:
Find functions called by this function:
Find functions calling this function:
Find this text string:
Change this text string:
Find this egrep pattern:
Find this file:
Find files #including this file:

用 doxygen 调试遗留代码

要想有效地调试别人开发的代码,就要花时间了解现有软件的总体结构 — 类及其层次结构,全局变量和静态变量,公共接口例程。在从现有的源代码中提取类层次结构方面,GNU 实用程序doxygen(见 参考资料 中的链接)可能是最好的工具。

要想在项目上运行 doxygen,首先需要在 shell 提示上运行 doxygen -g。这个命令在当前工作目录中生成一个名为 Doxyfile 的文件,必须手工编辑此文件。编辑之后,对 Doxyfile 再次运行doxygen。清单 8 给出一个运行示例。

清单 8. 运行 doxygen

arpan@tintin# doxygen -g
arpan@tintin# ls
Doxyfile
… [after editing Doxyfile]
arpan@tintin# doxygen Doxyfile

您需要理解 Doxyfile 中的几个字段。比较重要的字段是:

* OUTPUT_DIRECTORY。保存生成的文档文件的目录。
* INPUT。这是一个以空格分隔的列表,其中包含必须为其生成文档的所有源代码文件和文件夹。
* RECURSIVE。如果源代码列表是层次化的,就把这个字段设置为 YES。这样就不必在 INPUT 中指定所有文件夹,只需在 INPUT 中指定顶层文件夹并把这个字段设置为 YES。
* EXTRACT_ALL。这个字段必须设置为 YES,这告诉 doxygen 应该从没有文档的所有类和函数中提取文档。
* EXTRACT_PRIVATE。这个字段必须设置为 YES,这告诉 doxygen 应该在文档中包含类的私有数据成员。
* FILE_PATTERNS。除非项目没有采用一般的 C 或 C++ 源代码文件扩展名,比如.c、.cpp、.cc、.cxx、.h 或 .hpp,否则不需要在这个字段中添加设置。

注意:Doxyfile 中必须研究的其他字段取决于项目需求和所需的文档细节。清单 9 给出一个示例Doxyfile。

清单 9. 示例 Doxyfile

OUTPUT_DIRECTORY = /home/tintin/database/docs
INPUT = /home/tintin/project/database
FILE_PATTERNS =
RECURSIVE = yes
EXTRACT_ALL = yes
EXTRACT_PRIVATE = yes
EXTRACT_STATIC = yes

使用 STL 和 gdb

在使用 C++ 开发的软件中,最复杂的部分常常使用 C Standard Template Library (STL) 中的类。但糟糕的是,调试包含许多 STL 类的代码并不容易,GNU Debugger (gdb) 常常指出缺少信息,无法显示相关数据,甚至可能崩溃。为了解决这个问题,可以使用 gdb 的一个高级特性 — 添加用户定义的命令。例如,请考虑一下清单 10 中的代码片段,这段代码使用一个向量并显示信息。

清单 10. 在 C++ 代码中使用 STL 向量

#include <vector>
#include <iostream>
using namespace std;

int main ()
{
vector<int> V;
V.push_back(9);
V.push_back(8);
for (int i=0; i < V.size(); i++)
cout << V[i] << "\n";
return 0;
}

现在,在调试这个程序时,如果希望查明向量的长度,可以在 gdb 提示上运行 V._M_finish – V._M_start,其中的 _M_finish 和 _M_start 分别是向量开头和末尾的指针。但是,这要求您了解STL 的内部原理,而这不总是可行的。我推荐的替代方法是使用可免费下载的 gdb_stl_utils,它在gdb 中定义几个用户定义命令,比如 p_stl_vector_size(显示向量的大小)或 p_stl_vector(显示向量的内容)。清单 11 说明 p_stl_vector 如何循环遍历由 _M_start 和 _M_finish 指针指定的数据。

清单 11. 使用 p_stl_vector 显示向量的内容

define p_stl_vector
set $vec = ($arg0)
set $vec_size = $vec->_M_finish - $vec->_M_start
if ($vec_size != 0)
set $i = 0
while ($i < $vec_size)
printf "Vector Element %d:  ", $i
p *($vec->_M_start+$i)
set $i++
end
end
end

在 gdb 提示上运行 help user-defined,就可以看到使用 gdb_stl_utils 定义的命令列表。

加快编译

对于任何比较复杂的软件,编译源代码都会占用不少时间。在加快编译过程方面,最好的工具之一是ccache(见 参考资料 中的链接)。ccache 是一种编译器缓存,这意味着如果在编译期间文件没有修改过,就从工具的缓存获取它。如果用户只修改了一个头文件并调用 make clean; make,ccache会显著加快编译。因为 ccache 不仅仅使用时间戳决定文件是否需要重新编译,可以更好地节省宝贵的编译时间。下面是使用 ccache 的一个示例:

arpan@tintin# ccache g__ foo.cxx

ccache 在内部生成一个散列(hash),使用这个散列和其他东西考虑源代码文件的预处理版本(使用 g++ –E 获得)、调用编译器所用的选项等。编译的对象文件根据这个散列存储在缓存中。

ccache 定义了几个可以定制的环境变量:

* CCACHE_DIR。ccache 在这个目录中存储缓存的文件。在默认情况下,文件存储在 $HOME/.ccache中。
* CCACHE_TEMPDIR。ccache 在这个目录中存储临时文件。这个文件夹应该位于与 $CCACHE_DIR 相同的文件系统中。
* CCACHE_READONLY。如果不断增大的缓存文件夹可能造成问题,那么设置这个环境变量会有帮助。如果启用这个变量,ccache 在编译期间不会在缓存中添加任何文件;但是,它使用现有的缓存搜索对象文件。

通过结合使用 gdb 与 Valgrind 和 Electric-Fence 解决内存错误

C++ 编程有几个缺陷 — 最显著的问题是内存错误。有两个 UNIX 开放源码工具 — Valgrind 和Electric-Fence — 它们可以与 gdb 结合使用以解决内存错误。下面简要讨论如何使用这些工具。

Valgrind

对程序使用 Valgrind 最容易的方法是在 shell 上运行它,然后使用一般的程序选项。注意,为了获得最佳效果,应该运行程序的调试版本。

arpan@tintin# valgrind <valgrind options>
<program name> <program option1> <program option2> ..

Valgrind 报告一些常见的内存错误,比如不正确地释放内存(应该使用 malloc 分配,使用 delete释放)、使用尚未初始化的变量以及两次删除同一个指针。清单 12 中的示例代码中有一个明显的数组覆盖问题。

清单 12. C++ 内存错误示例

int main ()
{
int* p_arr = new int[10];
p_arr[10] = 5;
return 0;
}

Valgrind 和 gdb 可以结合使用。通过在 Valgrind 中使用 -db-attach=yes 选项,可以在运行Valgrind 时直接调用 gdb。例如,如果带 –db-attach 选项对清单 12 中的代码调用 Valgrind,在首次遇到内存问题时,它会调用 gdb,见清单 13。

清单 13. 在执行 Valgrind 期间连接 gdb

==5488== Conditional jump or move depends on uninitialised value(s)
==5488==    at 0x401206C: strlen (in /lib/ld-2.3.2.so)
==5488==    by 0x4004E35: _dl_init_paths (in /lib/ld-2.3.2.so)
==5488==    by 0x400305A: dl_main (in /lib/ld-2.3.2.so)
==5488==    by 0x400F87D: _dl_sysdep_start (in /lib/ld-2.3.2.so)
==5488==    by 0x4001092: _dl_start (in /lib/ld-2.3.2.so)
==5488==    by 0x4000C56: (within /lib/ld-2.3.2.so)
==5488==
==5488== ---- Attach to debugger ? --- [Return/N/n/Y/y/C/c] ---- n
==5488==
==5488== Invalid write of size 4
==5488==    at 0x8048466: main (test.cc:4)
==5488==  Address 0x4245050 is 0 bytes after a block of size 40 alloc'd
==5488==    at 0x401ADEB: operator new[](unsigned)
(m_replacemalloc/vg_replace_malloc.c:197)
==5488==    by 0x8048459: main (test.cc:3)
==5488==
==5488== ---- Attach to debugger ? --- [Return/N/n/Y/y/C/c] ----

Electric-Fence

Electric-Fence 是一组用于在基于 gdb 的环境中检测缓冲区上溢出或下溢出的库。在发生错误的内存访问时,这个工具(与 gdb 结合)会准确地指出源代码中导致问题的指令。例如,对于 清单 12中的代码,清单 14 显示在启用 Electric-Fence 的情况下 gdb 的表现。

清单 14. Electric-Fence 准确地指出源代码中导致崩溃的部分

(gdb) efence on
Enabled Electric Fence
(gdb) run
Starting program: /home/tintin/efence/a.out

Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens <bruce@perens.com>

Electric Fence 2.2.0 Copyright (C) 1987-1999 Bruce Perens <bruce@perens.com>

Program received signal SIGSEGV, Segmentation fault.
0x08048466 in main () at test.cc:4
<b>4         p_arr[10] = 5;</b>

在安装 Electric-Fence 之后,在 .gdbinit 文件中添加以下行:

define efence
set environment EF_PROTECT_BELOW 0
set environment LD_PRELOAD /usr/lib/libefence.so.0.0
echo Enabled Electric Fence\n
end

使用 gprof 进行代码覆盖

最常见的编程任务之一是提高代码性能。为了完成这个任务,一定要查明代码的哪些部分花费的执行时间最多。用技术术语来说,这称为剖析。GNU 剖析工具 gprof(见 参考资料 中的链接)很容易使用,提供了大量有用的特性。

为了收集程序的剖析信息,第一步是在调用编译器时指定 –pg 选项:

arpan@tintin# g++ database.cpp –pg

接下来,像一般情况下一样运行程序。成功地运行程序之后(也就是,没有出现崩溃或对 _exit system call 的调用),剖析信息被写入 gmon.out 文件中。生成 gmon.out 文件之后,对可执行文件运行 gprof(如下所示)。注意,如果没有指定可执行文件名,默认文件为 a.out。同样,如果没有指定剖析数据文件名,默认文件为当前工作目录中的 gmon.out。

arpan@tintin# gprof <options> <executable name>
<profile-data-file name> > outfile

在默认情况下,gprof 在标准输出中显示输出,所以需要把输出重定向到一个文件。gprof 提供两组信息:平面剖析数据和调用图,它们共同组成输出文件。平面剖析数据显示每个函数花费的总时间。Cumulative seconds 表示一个函数花费的总时间加上从这个函数调用的其他函数花费的时间。Self seconds 只计算这个函数本身花费的时间。

在 gdb 中显示源代码清单

开发人员常常需要通过非常缓慢的远程连接调试代码,所以不支持对 gdb 使用 Data Display Debugger (DDD) 等图形化界面。在这种情况下,在 gdb 中使用 Ctrl-X-A 组合键可以节省时间,因为它会在调试期间显示源代码清单。按 Ctrl-W-A 组合键就能够返回到 gdb 提示。另一种方法是用–tui 选项调用 gdb,这会直接启动文本模式的源代码清单。清单 15 显示以文本模式调用 gdb 的情况。

使用文本模式的 gdb 源代码清单

3using namespace std;
4
5int main ()
6         {
B+>7     vector<int> V;
8         V.push_back(9);
9         V.push_back(8);
10        for (int i=0; i < V.size(); i++)
11          cout << V[i] << "\n";
12        return 0;
13        }
14
--------------------------------------------------------------------------------------
child process 6069 In: main                                      Line: 7    PC: 0x804890e
(gdb) b main
Breakpoint 1 at 0x804890e: file test.cc, line 7.
(gdb) r

使用 CVS 维护有秩序源代码清单

不同的项目采用不同的编码风格。例如,一些开发人员喜欢在代码中使用制表符,而其他人不喜欢这样做。但重要的是,所有开发人员应该遵守相同的编码标准。但 是,现实情况常常不是这样的。通过使用 Concurrent Versions system (CVS) 等版本控制系统,可以针对一组编码标准检查要签入的文件,从而有效地实施统一的编码标准。为了完成这个任务,CVS 附带一组预定义的触发器脚本,当涉及特定的用户操作时会运行这些脚本。触发器脚本的格式很简单:

<REGULAR EXPRESSION>    <PROGRAM TO RUN>

预定义的触发器脚本之一是 $CVSROOT 文件夹中的 commitinfo 文件。为了检查要签入的文件是否包含制表符,使用以下 commitinfo 文件语法:

ALL     /usr/local/bin/validate-code.pl

commitinfo 文件识别出 ALL 关键字(这意味着应该检查提交的每个文件;也可以指定要检查的文件集)。然后运行相关联的脚本,根据源代码标准检查文件。

结束语

本文讨论了几种有助于提高 C++ 开发人员的生产力的免费工具。关于各个工具的详细信息,请查阅 参考资料。