1

我正在调试一个仅在我的程序的 PPC64 端口中出现的问题。

我有一个测试用例,其中 C 库qsort被赋予一个 libffi 生成的闭包作为字符串比较回调。字符串被正确地传递给回调,返回值被精确地存储到 libffi 传递给闭包函数的返回值缓冲区中。

但是,该数组未正确排序qsort。此外,Valgrind 报告 C 库 qsort 代码正在访问未初始化的内存,并--track-orgins=yes显示该内存是由 Libffi 堆栈分配的。我强烈怀疑这是返回值,因此由于垃圾比较,排序不正确。

即 Libffi 为返回值分配了缓冲区,并将该值传播给回调调用者;但是我的闭包调度函数被赋予了错误的指针,因此没有将返回值放在正确的位置。

由于某些奇怪的原因,Valgrind 不报告 未初始化内存的地址,只报告代码中发生使用的位置和分配的位置。

我只是想将该位置的地址与传递给闭包函数的指针进行比较:它们甚至是远程关闭的吗?

有没有办法从 Valgrind 中获取这些信息?


更新:我正在使用没有 root 权限的 GCC Compile Farm 机器;安装的 libffi 没有调试信息。它是 3.0.13 版本。

libffi但是,我刚刚构建的 git head重现了该问题。

我已经确认它是未初始化的返回值区域。

我在闭包调度汇编代码中添加了一条指令,用于在闭包调度堆栈帧的部分ffi_closure_LINUX64底部初始化一个双字大小的区域。RETVAL这使得 Valgrind 错误消失了;但当然返回值是垃圾。它还证实了一个基本的理智:调用闭包调度助手之前的代码和之后的代码引用相同的返回值区域。(堆栈指针没有意外移动,并且帧引用是正确的。)用户代码最终获得的任何地址都没有指向该返回值。

接下来,我将返回区域的初始化向下移动到名为 的 C 函数ffi_closure_helper_LINUX64中,靠近函数的入口。这也仍然使未初始化的错误消失,确认助手正在通过%r6(参数 4)获取正确的返回值区域地址。

4

3 回答 3

1

由于某些奇怪的原因,Valgrind 不报告未初始化内存的地址,只报告代码中发生使用的位置和分配的位置。

这是 Valgrind Memcheck 工具的记录行为,请参阅手册的这一部分--track-orgins=yes

对于源自堆栈分配的未初始化值,Memcheck 可以告诉您哪个函数分配了该值,但仅此而已——通常它会显示函数左括号的源位置。因此,您应该仔细检查所有函数的局部变量是否已正确初始化。

于 2017-05-27T10:22:55.307 回答
1

valgrind 中没有报告 uninit 内存地址的功能,因为这(在大多数情况下)对用户没有帮助:堆栈地址或堆地址实际上并不能说明太多。

通过在 Valgrind 报告的帧中设置断点,您可能会获得更多信息,并使用 gdb+vgdb+memcheck 监视器命令将堆栈的各个部分标记为已初始化。将错误位置设置为已初始化时,valgrind 不应再报告错误。您可能必须执行多次运行,每次都标记堆栈的其他变量/区域。

请参阅 http://www.valgrind.org/docs/manual/mc-manual.html#mc-manual.monitor-commands和 GDB 用户手册,了解如何编写(复杂的)命令在到达断点时运行。

于 2017-05-26T11:56:40.140 回答
1

好的,我调试了问题。

问题是 LibFFI 中的 PPC64 代码包含与我的期望不符的大端情况。

我应用了这个测试补丁:

--- a/src/powerpc/linux64_closure.S
+++ b/src/powerpc/linux64_closure.S
@@ -27,7 +27,8 @@
 #define LIBFFI_ASM
 #include <fficonfig.h>
 #include <ffi.h>
-
+#undef __LITTLE_ENDIAN__
+#define __LITTLE_ENDIAN__ 1
        .file   "linux64_closure.S"

 #ifdef POWERPC64

我所有的测试都通过了。哪些__LITTLE_ENDIAN__控件是有条件地包含的代码块,如下所示:

# case FFI_TYPE_INT
# ifdef __LITTLE_ENDIAN__
        lwa %r3, RETVAL+0(%r1)
# else
        lwa %r3, RETVAL+4(%r1)
# endif
        mtlr %r0
        addi %r1, %r1, STACKFRAME
        .cfi_def_cfa_offset 0
        blr
        .cfi_def_cfa_offset STACKFRAME

客户端代码,大端,预计将替换存储的返回值,以便它与 8 字节字的顶部对齐。

因此,要存储一个int(四个字节),代码应该做*(int *)(retptr+4) = val而不是*(int *)retptr = val像我的代码那样简单。

似乎期望应用程序应该将一个 8 字节字存储到返回值中,而不管 FFI 类型如何:无论是 char、short、int 还是(64 位)long。也就是说:

(int64_t)retptr = val; / val 是字符,短,随便 */

这样,值的最低有效字节为 at retptr + 7,因此如果实际类型为char;则使用该地址 retptr + 6如果是则使用short,依此类推。FFI 代码以这种方式有意义。问题是它不方便且不一致;不需要以这种方式处理 FFI 参数。

例如int,以下调用中的参数不会被 4 个字节替换;它只是写入给 libffi 的缓冲区的基地址

This is the TXR Lisp interactive listener of TXR 176.
Use the :quit command or type Ctrl-D on empty line to exit.
1> (with-dyn-lib nil (deffi printf "printf" int (str : int)))
#:lib-0185
2> (printf "foo %d\n" 1)
foo 1 
0

但是,哦,看;返回值是假的!外部函数调用返回值也有类似的问题。

看起来我被一些 libffi 文档中的一个例子愚弄了,即这个:

 #include <stdio.h>
 #include <ffi.h>

 int main()
 {
   ffi_cif cif;
   ffi_type *args[1];
   void *values[1];
   char *s;
   int rc;

   /* ... abbreviated ... */
       s = "This is cool!";
       ffi_call(&cif, puts, &rc, values);
       /* rc now holds the result of the call to puts */

   /* ... */
 }

事实证明,这是不正确的;其他一些 libffi 文档说必须使用类型捕获返回值ffi_arg(令人困惑的是,它不用于参数)。所以我认为上面的示例应该做这样的事情:

ffi_arg rc_buf;
int rc;
/*...*/
s = "Turned out uncool, but we promise this is really cool now!";
ffi_call(&cif, puts, &rc_buf, values);
rc = (int) rc_buf;
于 2017-05-27T16:48:13.910 回答