C++ `strace` / `ltrace`:追踪 C++ 程序的系统调用与库函数调用

哈喽,各位好!今天咱们来聊聊C++程序调试中的两个好帮手:straceltrace。它们就像是给你的程序装上了窃听器,能让你听到程序在运行时都跟操作系统和库函数嘀咕了些啥。

一、为啥我们需要 straceltrace

想象一下,你写了一个复杂的C++程序,运行起来总是出错,但错误信息又语焉不详。你用GDB调试,一步一步跟踪,但代码量太大,跟踪起来效率太低。这时候,straceltrace 就能派上大用场了。

  • strace:追踪系统调用

    系统调用是用户程序与操作系统内核交互的唯一途径。例如,打开文件、读取数据、创建进程等等,都需要通过系统调用来完成。strace 可以告诉你程序在运行时都发起了哪些系统调用,以及这些调用的参数和返回值。这对于理解程序的行为、发现性能瓶颈以及定位错误非常有帮助。

  • ltrace:追踪库函数调用

    C++程序通常会使用大量的库函数,例如标准C库、数学库、网络库等等。ltrace 可以告诉你程序在运行时都调用了哪些库函数,以及这些函数的参数和返回值。这对于理解程序如何使用库函数、发现库函数调用中的错误以及分析程序依赖关系非常有帮助。

二、strace 的用法

strace 的基本语法很简单:

strace [options] command [arguments]

其中 command 是你要追踪的程序,arguments 是程序的参数,optionsstrace 的选项。

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

    例如,只追踪 openread 系统调用:

    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

    例如,只追踪 fopenfread 库函数:

    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++ 例子:使用 printfsqrt

#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,我们可以清晰地看到程序调用的库函数以及它们的参数和返回值。

四、straceltrace 的配合使用

straceltrace 可以配合使用,以便更全面地了解程序的行为。 例如,如果你的程序在调用某个库函数时出错,你可以先用 ltrace 找到出错的库函数,然后用 strace 追踪该库函数内部的系统调用,以便更深入地了解错误的原因。

五、一些高级用法和技巧

  1. 分析网络程序的 strace 输出

    对于网络程序,strace 可以帮助你了解程序的网络通信过程。 你可以追踪 socket, bind, listen, accept, connect, send, recv 等系统调用,以便了解程序的网络连接建立、数据发送和接收情况。

  2. 分析多线程程序的 strace 输出

    对于多线程程序,strace -f 可以追踪所有线程的系统调用。 但是,多线程程序的 strace 输出可能会非常混乱,难以阅读。 你可以使用 tsort 命令对 strace 输出进行排序,以便更容易理解线程之间的交互关系。

  3. 使用 awkgrepstraceltrace 输出进行过滤和分析

    straceltrace 的输出通常非常冗长,你需要使用 awkgrep 等工具对输出进行过滤和分析,以便找到你关心的信息。 例如,你可以使用 grep 过滤出所有与文件读写相关的系统调用,或者使用 awk 提取出所有系统调用的执行时间。

  4. 结合 perf 进行性能分析

    straceltrace 可以告诉你程序在做什么,但不能告诉你程序为什么这么慢。 你可以使用 perf 等性能分析工具来分析程序的性能瓶颈,然后结合 straceltrace 来定位问题。

六、strace vs ltrace:表格总结

特性 strace ltrace
追踪对象 系统调用 库函数调用
用途 理解程序与内核的交互、定位系统级错误、性能分析 理解程序如何使用库函数、定位库函数调用错误
输出信息 系统调用名称、参数、返回值 库函数名称、参数、返回值
适用场景 需要了解程序底层行为、定位系统级问题的场景 需要了解程序如何使用库函数的场景

七、注意事项

  • straceltrace 会显著降低程序的运行速度,因此不建议在生产环境中使用。
  • straceltrace 的输出可能会包含敏感信息,例如密码、密钥等,因此需要注意保护输出结果。
  • straceltrace 需要 root 权限才能追踪其他用户的进程。

八、总结

straceltrace 是非常强大的调试工具,可以帮助你深入了解程序的行为,定位错误,分析性能瓶颈。 熟练掌握它们的使用方法,可以大大提高你的调试效率。

希望今天的讲座对大家有所帮助! 祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注