我如何处理内存、.so 文件名和十六进制偏移量

How do I proceed with memory, .so filenames and hex offsets

不要因此而喷我,但这是真的。我正在编写一个多线程 python 应用程序,该应用程序 运行 持续了很长时间,通常有 10 个进程需要 2-3 小时。这台机器并不慢,只是计算量大。

问题是有时由于外部工具,应用程序会挂起大约 85-90%。

我已将此测试分解成更小的部分,然后可以 运行 成功,但长 ​​运行ning 程序挂起。

例如,假设我必须分析一个包含 100,000,000 个项目的列表中的一些数据。

将它分成 20 个 5,000,000 列出所有较小的部分 运行s 完成。

正在努力完成它悬而未决的 100,000,000 项目。我使用了一些我无法更改的外部工具,所以我只是想看看发生了什么。

我设置了 Dtrace 和 运行

sudo dtrace -n 'syscall:::entry / execname == "python2.7" / { @[ustack()] = count() }'

在我的程序挂起时,我得到了如下代码示例的输出。

          libc.so.7`__sys_recvfrom+0xa
          _socket.so`0x804086ecd
          _socket.so`0x8040854ac
          libpython2.7.so.1`PyEval_EvalFrameEx+0x52d7
          libpython2.7.so.1`PyEval_EvalCodeEx+0x665
          libpython2.7.so.1`0x800b3317d
          libpython2.7.so.1`PyEval_EvalFrameEx+0x4e2f
          libpython2.7.so.1`0x800b33250
          libpython2.7.so.1`PyEval_EvalFrameEx+0x4e2f
          libpython2.7.so.1`PyEval_EvalCodeEx+0x665
          libpython2.7.so.1`0x800abb5a1
          libpython2.7.so.1`PyObject_Call+0x64
          libpython2.7.so.1`0x800aa3855
          libpython2.7.so.1`PyObject_Call+0x64
          libpython2.7.so.1`PyEval_EvalFrameEx+0x4de2
          libpython2.7.so.1`PyEval_EvalCodeEx+0x665
          libpython2.7.so.1`0x800abb5a1
          libpython2.7.so.1`PyObject_Call+0x64
          libpython2.7.so.1`0x800aa3855
          libpython2.7.so.1`PyObject_Call+0x64

该代码只是一遍又一遍地重复。我尝试查看 Dtrace python 探测器,但从星期二开始,它们似乎在两侧都被破坏了,所以这可能是我能得到的最接近的探测器。

我的问题,我有一个模糊的想法,即 libpython2.7.so.1 是在 0x64

的十六进制偏移处保存函数 pyObject_Call 的共享库

对吗?

我怎样才能破译这个?我什至不知道该怎么称呼它,这样我就可以 google 寻求答案或指导。

您可能应该先阅读 Showing the stack trace from a running Python application

你的具体 问题是关于 DTrace 的 ustack() 操作的解释和 所以这个回复可能比你需要的更多。这是因为其中之一 DTrace 的设计原则是显示系统的准确状态。 因此,即使您对 Python 方面感兴趣 程序,DTrace 正在揭示其底层实现。

您提供的输出是一个堆栈,这是一种 描述线程在其特定点的状态 执行。例如,如果您有代码

void c(void) { pause(); }
void b(void) { c(); }
void a(void) { b(); }

然后你在 pause() 内执行时请求了一个堆栈 你可能看到类似

的东西
pause()
c()
b()
a()

无论您使用什么工具都会找到当前指令及其 在找到 "return address" 之前封闭函数,即 指向该函数最终 return;重复这个 过程产生一个堆栈。因此,尽管应该读取堆栈 从上到下作为一系列 return 地址,通常是 作为一系列来电者从另一个方向阅读。注意 程序相应的方式的微妙之处 说明组装意味着这第二个解释 有时会产生误导。

为了扩展上面的示例,a()、b() 和 c() 很可能是 都存在于同一个库中——而且可能有 在其他库中具有相同名称的函数。因此它是 对于每个函数,显示它所针对的对象很有用 属于。因此上面的栈可以变成

libc.so`pause()
libfoo.so`c()
libfoo.so`b()
libfoo.so`a()

这在某种程度上允许开发人员识别 程序最终处于特定状态:libfoo 中的函数 c() 调用了 pause()。然而,还有更多工作要做:如果 c() 看起来像

void c() {
    pause();
    pause();
}

那么程序在等待哪个 pause() 调用?

函数a()、b()和c()将是序列 通常会占据连续区域的指令 记忆。调用其中一个函数仅涉及 记下完成后 return 的位置(即 return 地址),然后跳转到对应的内存地址 到函数的开始。函数的起始地址和大小是 记录在嵌入对象中的 "symbol table" 中;它是 通过阅读这个 table 调试器能够找到函数 包含给定位置,例如 return 地址。因此一个 中的特定点一个函数可以用一个偏移量来描述, 通常以十六进制表示,从头开始。所以一个更好的 上面堆栈的版本可能是

libc.so`pause()+0x12
libfoo.so`c()+0x42
libfoo.so`b()+0x12
libfoo.so`a()+0x12

此时,开发者可以在libfoo.so上使用"disassembler" 显示 c() 中的指令;与 c() 的比较 源代码将允许他揭示具体的行 调用了 pause()。

在结束对堆栈的描述之前,值得做的 再观察一次。鉴于存在足够的“调试 data" 在库中,例如 libfoo,更好的调试器将能够 加倍努力并显示源代码文件名和 行号而不是每个 "frame" 中的十六进制偏移量 堆栈。

所以现在,return 到你问题中的堆栈, libpython(2.7.so.1) 是一个库,其函数执行该作业 执行 Python 脚本。 Python 脚本中的函数是 即时转换为 executable 指令,所以我猜是 片段

libpython2.7.so.1`0x800b33250
libpython2.7.so.1`PyEval_EvalFrameEx+0x4e2f
libpython2.7.so.1`PyEval_EvalCodeEx+0x665

表示 PyEval_EvalFrameEx() 是 libpython 中的功能 本身调用 Python 函数(即写在 Python) 驻留在地址 0x800b33250 附近的内存中。一种 简单的调试器可以看到这个地址属于 libpython 但是 不会在库的符号 table 中找到相应的条目; 别无选择,它只打印 "raw" 地址。

所以,您需要查看 Python 脚本,看看它是什么 正在做,但不幸的是,没有迹象表明 堆栈的 Python 组件中的函数。

有几种方法可以继续。首先是找到一个 libpython 的版本,如果存在,带有 "DTrace helper"。这个 是一些额外的功能,可以让 DTrace 查看 Python 程序本身除了周围 执行。结果是每个 Python 帧将是 在Python源代码中用相应的点进行注释。

如果您使用的是 Solaris,另一个方法是使用 pstack(1);这有 对 Python.

的原生支持

最后,尝试特定的 Python 调试器。

同样值得指出的是,您的 dtrace 调用将显示 你看到的所有堆栈,按受欢迎程度排序,无论何时 程序 "python2.7" 进行系统调用。从你的描述来看, 这可能不是你想要的。如果你想了解 挂起的行为然后你可能想从一个开始 python2.7 进程 在 挂.