哈喽,各位好!今天咱们来聊聊C++程序调试中的两个好帮手:strace
和 ltrace
。它们就像是给你的程序装上了窃听器,能让你听到程序在运行时都跟操作系统和库函数嘀咕了些啥。
一、为啥我们需要 strace
和 ltrace
?
想象一下,你写了一个复杂的C++程序,运行起来总是出错,但错误信息又语焉不详。你用GDB调试,一步一步跟踪,但代码量太大,跟踪起来效率太低。这时候,strace
和 ltrace
就能派上大用场了。
-
strace
:追踪系统调用系统调用是用户程序与操作系统内核交互的唯一途径。例如,打开文件、读取数据、创建进程等等,都需要通过系统调用来完成。
strace
可以告诉你程序在运行时都发起了哪些系统调用,以及这些调用的参数和返回值。这对于理解程序的行为、发现性能瓶颈以及定位错误非常有帮助。 -
ltrace
:追踪库函数调用C++程序通常会使用大量的库函数,例如标准C库、数学库、网络库等等。
ltrace
可以告诉你程序在运行时都调用了哪些库函数,以及这些函数的参数和返回值。这对于理解程序如何使用库函数、发现库函数调用中的错误以及分析程序依赖关系非常有帮助。
二、strace
的用法
strace
的基本语法很简单:
strace [options] command [arguments]
其中 command
是你要追踪的程序,arguments
是程序的参数,options
是 strace
的选项。
1. 最简单的例子:追踪 ls
命令
strace ls -l
这条命令会追踪 ls -l
命令的执行过程,并把所有的系统调用信息输出到终端。你会看到一大堆的输出,包括 execve
, open
, read
, write
, close
等等。
2. 常用选项
-
-o filename
:将输出结果保存到文件中。strace -o ls.strace ls -l
这条命令会将
ls -l
命令的系统调用信息保存到ls.strace
文件中。方便后续分析。 -
-p pid
:追踪指定的进程 ID。strace -p 1234
这条命令会追踪进程 ID 为 1234 的进程。适用于已经运行的程序。
-
-f
:追踪子进程。strace -f ./my_program
如果你的程序会创建子进程,那么加上
-f
选项可以同时追踪父进程和子进程的系统调用。 -
-e expr
:过滤需要追踪的系统调用。expr
可以是以下几种形式:trace=set
:只追踪指定的系统调用集合set
。trace=!set
:排除指定的系统调用集合set
。trace=syscall_name
:只追踪指定的系统调用syscall_name
。trace=!syscall_name
:排除指定的系统调用syscall_name
。
例如,只追踪
open
和read
系统调用:strace -e trace=open,read ls -l
排除
write
系统调用:strace -e trace=!write ls -l
-
-t
:在每行输出前加上时间戳。strace -t ls -l
可以帮助你了解系统调用的执行时间。
-
-T
:显示每个系统调用花费的时间。strace -T ls -l
更精确的系统调用时间分析。
-
-c
:统计系统调用的次数和时间。strace -c ls -l
在程序结束后,会输出一个统计报告,告诉你每个系统调用被调用了多少次,以及总共花费了多少时间。这对于性能分析非常有帮助。
-
-s strsize
:指定输出字符串的最大长度。 默认值通常是32。strace -s 200 ls -l
一些系统调用会传递字符串参数,如果字符串太长,默认情况下会被截断。使用
-s
选项可以增加输出的字符串长度。
3. 一个 C++ 例子:文件读写
#include <iostream>
#include <fstream>
#include <string>
int main() {
std::ofstream outfile("example.txt");
if (outfile.is_open()) {
outfile << "This is a line of text.n";
outfile << "Another line of text.n";
outfile.close();
} else {
std::cerr << "Unable to open file for writing.n";
return 1;
}
std::ifstream infile("example.txt");
std::string line;
if (infile.is_open()) {
while (getline(infile, line)) {
std::cout << line << 'n';
}
infile.close();
} else {
std::cerr << "Unable to open file for reading.n";
return 1;
}
return 0;
}
将这段代码保存为 file_io.cpp
,然后编译:
g++ file_io.cpp -o file_io
现在,使用 strace
追踪它的执行:
strace ./file_io
你会看到类似于下面的输出(省略部分):
execve("./file_io", ["./file_io"], 0x7ffc5e7a09a0 /* 22 vars */) = 0
brk(NULL) = 0x556775719000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG, st_size=173982, ...}) = 0
mmap(NULL, 173982, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f8236c0f000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libstdc++.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF21133>12602337"..., 832) = 832
fstat(3, {st_mode=S_IFREG, st_size=1684464, ...}) = 0
mmap(NULL, 3895296, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f823663a000
mprotect(0x7f82367d4000, 2097152, PROT_NONE) = 0
mmap(0x7f82369d4000, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19a000) = 0x7f82369d4000
mmap(0x7f82369e4000, 14592, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1aa000) = 0x7f82369e4000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF21133>1240342"..., 832) = 832
fstat(3, {st_mode=S_IFREG, st_size=1110512, ...}) = 0
mmap(NULL, 3215552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f8236329000
mprotect(0x7f8236436000, 2097152, PROT_NONE) = 0
mmap(0x7f8236636000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x10d000) = 0x7f8236636000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113>132022"..., 832) = 832
fstat(3, {st_mode=S_IFREG, st_size=103472, ...}) = 0
mmap(NULL, 2207888, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f823610f000
mprotect(0x7f8236128000, 2097152, PROT_NONE) = 0
mmap(0x7f8236328000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19000) = 0x7f8236328000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF21133>13402202"..., 832) = 832
fstat(3, {st_mode=S_IFREG, st_size=2266648, ...}) = 0
mmap(NULL, 4379840, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f8235d07000
mprotect(0x7f8235ee9000, 2097152, PROT_NONE) = 0
mmap(0x7f82360e9000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e2000) = 0x7f82360e9000
mmap(0x7f82360ef000, 14528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e8000) = 0x7f82360ef000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", O_RDONLY|O_CLOEXEC) = 3
read(3, "177ELF2113>1`21"..., 832) = 832
fstat(3, {st_mode=S_IFREG, st_size=163360, ...}) = 0
mmap(NULL, 3865120, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f8235af0000
mprotect(0x7f8235b14000, 2097152, PROT_NONE) = 0
mmap(0x7f8235d14000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x24000) = 0x7f8235d14000
close(3) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8236e19000
arch_prctl(ARCH_SET_FS, 0x7f8236e19740) = 0
set_tid_address(0x7f8236e19a10) = 2552
set_robust_list(0x7f8236e19a20, 24) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8388608, rlim_max=RLIM64_INFINITY}) = 0
prlimit64(0, RLIMIT_NPROC, NULL, {rlim_cur=30666, rlim_max=30666}) = 0
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=1024, rlim_max=4096}) = 0
getrandom(NULL, 24, GRND_NONBLOCK) = 24
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8236e1b000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8236e1d000
openat(AT_FDCWD, "example.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
write(3, "This is a line of text.n", 24) = 24
write(3, "Another line of text.n", 25) = 25
close(3) = 0
openat(AT_FDCWD, "example.txt", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG, st_size=49, ...}) = 0
mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f8236e19000
read(3, "This is a line of text.nAnother "..., 4096) = 49
write(1, "This is a line of text.n", 24) = 24
write(1, "Another line of text.n", 25) = 25
read(3, "", 4096) = 0
close(3) = 0
munmap(0x7f8236e19000, 4096) = 0
exit_group(0) = ?
+++ exited with 0 +++
分析输出:
openat(AT_FDCWD, "example.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
:程序打开文件 "example.txt" 用于写入 (O_WRONLY),如果文件不存在则创建 (O_CREAT),如果文件存在则截断 (O_TRUNC)。文件描述符是 3。write(3, "This is a line of text.n", 24) = 24
:程序向文件描述符 3 写入 24 字节的数据。close(3) = 0
:程序关闭文件描述符 3。openat(AT_FDCWD, "example.txt", O_RDONLY) = 3
:程序打开文件 "example.txt" 用于读取 (O_RDONLY)。文件描述符是 3。read(3, "This is a line of text.nAnother "..., 4096) = 49
:程序从文件描述符 3 读取 49 字节的数据。write(1, "This is a line of text.n", 24) = 24
:程序向文件描述符 1(标准输出)写入 24 字节的数据。close(3) = 0
:程序关闭文件描述符 3。
通过 strace
,我们可以清晰地看到程序的文件读写过程。
三、ltrace
的用法
ltrace
的基本语法和 strace
类似:
ltrace [options] command [arguments]
1. 最简单的例子:追踪 ls
命令
ltrace ls -l
这条命令会追踪 ls -l
命令的执行过程,并把所有的库函数调用信息输出到终端。
2. 常用选项
ltrace
的选项和 strace
类似,但有一些区别。
-
-o filename
:将输出结果保存到文件中。ltrace -o ls.ltrace ls -l
-
-p pid
:追踪指定的进程 ID。ltrace -p 1234
-
-f
:追踪子进程。ltrace -f ./my_program
-
-e expr
:过滤需要追踪的库函数调用。expr
可以是以下几种形式:trace=symbol
:只追踪指定的库函数symbol
。trace=!symbol
:排除指定的库函数symbol
。
例如,只追踪
fopen
和fread
库函数:ltrace -e trace=fopen,fread ls -l
排除
printf
库函数:ltrace -e trace=!printf ls -l
-
-T
:显示每个库函数花费的时间。ltrace -T ls -l
-
-S
:显示系统调用。 这个选项可以将系统调用和库函数调用都显示出来。ltrace -S ls -l
3. 一个 C++ 例子:使用 printf
和 sqrt
#include <iostream>
#include <cmath>
#include <cstdio>
int main() {
double x = 2.0;
double y = std::sqrt(x);
std::printf("The square root of %f is %fn", x, y);
return 0;
}
将这段代码保存为 math_example.cpp
,然后编译:
g++ math_example.cpp -o math_example -lm
注意:编译时需要链接数学库 -lm
。
现在,使用 ltrace
追踪它的执行:
ltrace ./math_example
你会看到类似于下面的输出:
__libc_start_main(0x55f04b344180, 1, 0x7ffc0391e3a8, 0x55f04b344280 <unfinished ...>
sqrt(2) = 1.4142135623730951
printf("The square root of %f is %fn", 2.000000, 1.414214) = 33
+++ exited (0) +++
分析输出:
sqrt(2) = 1.4142135623730951
:程序调用了sqrt
函数,参数是 2,返回值是 1.4142135623730951。printf("The square root of %f is %fn", 2.000000, 1.414214) = 33
:程序调用了printf
函数,打印了一条消息,返回值是 33(打印的字符数)。
通过 ltrace
,我们可以清晰地看到程序调用的库函数以及它们的参数和返回值。
四、strace
和 ltrace
的配合使用
strace
和 ltrace
可以配合使用,以便更全面地了解程序的行为。 例如,如果你的程序在调用某个库函数时出错,你可以先用 ltrace
找到出错的库函数,然后用 strace
追踪该库函数内部的系统调用,以便更深入地了解错误的原因。
五、一些高级用法和技巧
-
分析网络程序的
strace
输出对于网络程序,
strace
可以帮助你了解程序的网络通信过程。 你可以追踪socket
,bind
,listen
,accept
,connect
,send
,recv
等系统调用,以便了解程序的网络连接建立、数据发送和接收情况。 -
分析多线程程序的
strace
输出对于多线程程序,
strace -f
可以追踪所有线程的系统调用。 但是,多线程程序的strace
输出可能会非常混乱,难以阅读。 你可以使用tsort
命令对strace
输出进行排序,以便更容易理解线程之间的交互关系。 -
使用
awk
或grep
对strace
或ltrace
输出进行过滤和分析strace
和ltrace
的输出通常非常冗长,你需要使用awk
或grep
等工具对输出进行过滤和分析,以便找到你关心的信息。 例如,你可以使用grep
过滤出所有与文件读写相关的系统调用,或者使用awk
提取出所有系统调用的执行时间。 -
结合
perf
进行性能分析strace
和ltrace
可以告诉你程序在做什么,但不能告诉你程序为什么这么慢。 你可以使用perf
等性能分析工具来分析程序的性能瓶颈,然后结合strace
和ltrace
来定位问题。
六、strace
vs ltrace
:表格总结
特性 | strace |
ltrace |
---|---|---|
追踪对象 | 系统调用 | 库函数调用 |
用途 | 理解程序与内核的交互、定位系统级错误、性能分析 | 理解程序如何使用库函数、定位库函数调用错误 |
输出信息 | 系统调用名称、参数、返回值 | 库函数名称、参数、返回值 |
适用场景 | 需要了解程序底层行为、定位系统级问题的场景 | 需要了解程序如何使用库函数的场景 |
七、注意事项
strace
和ltrace
会显著降低程序的运行速度,因此不建议在生产环境中使用。strace
和ltrace
的输出可能会包含敏感信息,例如密码、密钥等,因此需要注意保护输出结果。strace
和ltrace
需要 root 权限才能追踪其他用户的进程。
八、总结
strace
和 ltrace
是非常强大的调试工具,可以帮助你深入了解程序的行为,定位错误,分析性能瓶颈。 熟练掌握它们的使用方法,可以大大提高你的调试效率。
希望今天的讲座对大家有所帮助! 祝大家编程愉快!