为什么我在使用libexpect.so编写的简单C++程序中会出现分段错误?

8
我正在忙一个项目,需要在bash或ssh中自动化一些流程,因此我决定使用libexpect.so库。如果你不知道libexpect是什么,它提供了一个expect扩展,我可以在c++程序中使用,而expect只是一个可以运行自动化脚本的程序,用于诸如ssh之类的事情。因此,我可以执行一个尝试连接某个地方的脚本……当expect发现密码提示时,我可能已经给expect发送了一个密码。

我的问题是,当我运行程序,即使是一个非常简单的程序,我也会收到一个段错误,我用gdb将其缩小到了libexpect.so中的一个名为exp_spawnv的函数。

我知道我已经正确链接了库,它可以编译通过,实际上,在ubuntu中编译和运行时整个问题都不存在,但在我的arch linux安装中,我得到了分段错误,稍后我会详细说明。我在arch上构建它的原因是我希望最终能够在大多数发行版上构建该项目。

我的想法是,在我的arch安装中,当调用exp_spawnv函数时,可能存在权限失败的管道、fork或其他问题。

为了证明我没有做什么奇怪的事情,这里有一个简单的main.cpp以说明目的。

#include <tcl8.5/expect.h>

int main()
{
  FILE* file = exp_popen("bash");
}

这只是有史以来最简单的expect程序。以下是我编译和链接它的过程。 $ g++ -ggdb -c main.cpp main.cpp: 在函数‘int main()’中: main.cpp:5:32: 警告:从字符串常量转换为‘char*’已弃用[-Wwrite-strings] $ g++ main.o -lexpect -o mainprog
于是我得到了可执行文件mainprog……但只运行它会导致分段错误,没有其他输出。
如果我在gdb中运行mainprog,它会告诉我exp_spawnv中有一个分段错误。以下是我在gdb中执行的操作及末尾的回溯信息。

(gdb) run

开始运行程序:/home/user/testlibexpect/mainprog

警告:无法加载linux-vdso.so.1的共享库符号。

您需要“set solib-search-path”或“set sysroot”吗?

程序收到SIGSEGV信号,分段错误。

0x00007ffff7bc8836在/usr/lib/libexpect.so中的exp_spawnv()。

(gdb) backtrace

0 0x00007ffff7bc8836在/usr/lib/libexpect.so中的exp_spawnv()。

1 0x00007ffff7bc8cb4在/usr/lib/libexpect.so中的exp_spawnl()。

2 0x00007ffff7bc8d01在/usr/lib/libexpect.so中的exp_popen()。

3 0x000000000040069e在main.cpp的main()中:5

有两件事让我担忧。

  1. 查看libexpect的man页面,我知道exp_spawnv会fork一个新进程,并且我可以通过FILE*进行通信。那么我猜测收到SIGSEGV信号是因为fork出了一些问题吗?

  2. 回溯中的那行警告信息(warning: Could not load shared library symbols for linux-vdso.so.1.)看起来有问题?

总之,我的问题是我应该查找什么来解决这个问题?我已经尝试从源代码构建expect库,也尝试通过arch软件包管理器pacman获取它...但问题仍然存在,所以我不认为库构建是有问题的,如果你明白我的意思的话。

编辑:根据我所做的研究,我的第二个担忧点并不是问题,只是表面问题。

eclipse中的反汇编如下:

00007ffff7bc87c6:   mov 0x20c68b(%rip),%rax        # 0x7ffff7dd4e58
00007ffff7bc87cd:   mov (%rax),%rax
00007ffff7bc87d0:   test %rax,%rax
00007ffff7bc87d3:   je 0x7ffff7bc87d7 <exp_spawnv+935>
00007ffff7bc87d5:   callq *%rax
00007ffff7bc87d7:   mov %r12,%rsi
00007ffff7bc87da:   mov %rbp,%rdi
00007ffff7bc87dd:   callq 0x7ffff7bb2330 <execvp@plt>
00007ffff7bc87e2:   callq 0x7ffff7bb1720 <__errno_location@plt>
00007ffff7bc87e7:   mov 0x24(%rsp),%edi
00007ffff7bc87eb:   mov %rax,%rsi
00007ffff7bc87ee:   mov $0x4,%edx
00007ffff7bc87f3:   xor %eax,%eax
00007ffff7bc87f5:   callq 0x7ffff7bb1910 <write@plt>
00007ffff7bc87fa:   mov $0xffffffff,%edi
00007ffff7bc87ff:   callq 0x7ffff7bb23d0 <exit@plt>
00007ffff7bc8804:   nopl 0x0(%rax)
00007ffff7bc8808:   xor %eax,%eax
00007ffff7bc880a:   movl $0x0,0x20dd3c(%rip)        # 0x7ffff7dd6550
00007ffff7bc8814:   callq 0x7ffff7bb1700 <exp_init_pty@plt>
00007ffff7bc8819:   xor %eax,%eax
00007ffff7bc881b:   callq 0x7ffff7bb2460 <exp_init_tty@plt>
00007ffff7bc8820:   lea -0x1c97(%rip),%rdi        # 0x7ffff7bc6b90
00007ffff7bc8827:   callq 0x7ffff7bb2540 <expDiagLogPtrSet@plt>
00007ffff7bc882c:   mov 0x20c555(%rip),%rax        # 0x7ffff7dd4d88
00007ffff7bc8833:   mov (%rax),%rax
00007ffff7bc8836:   mov 0x410(%rax),%rdi

我想到的答案

这是我最终想到的解决方案,我接受了szx的答案,因为它让我走上了这条路,一旦我知道我在寻找什么,这就变得微不足道了。

//do not use TCL stubs as this is a main
#undef USE_TCL_STUBS


#include <iostream>
using std::cout;
using std::endl;

//headers that must be included when using expectTcl as an extension to c++ program
#include <stdio.h>
#include <stdlib.h>
#include <expectTcl/tcl.h>
#include <expectTcl/expect_tcl.h>
#include <expectTcl/expect.h>

//enums representing cases of what expect found in loop
enum{FOUNDSEARCH, PROMPT};

int main()
{
  /* initialise expect and tcl */
  Tcl_Interp *interp = Tcl_CreateInterp();

  if(Tcl_Init(interp) == TCL_ERROR)
    {
      cout << "TCL failed to initialize." << endl;
    }
  if(Expect_Init(interp) == TCL_ERROR)
    {
      cout << "Expect failed to initialize." << endl;
    }

  /* end of intialisation procedure */

  //open a shell with a pipe
  char shellType[] = "sh";
  FILE* fp = exp_popen(shellType);

  //should we exit from the loop which is studying sh output
  bool shouldBreak = false;
  //did we find the pwd
  bool foundSearch = false;
  //does it look like expect is working
  bool expectWorking = false;
  //did we receive a prompt...therefore we should send a command
  bool receivedPrompt = false;

  while(shouldBreak == false)
    {
      switch(exp_fexpectl(fp,
              exp_glob, "/tools/test*", FOUNDSEARCH,  //different
              exp_glob,"# ", PROMPT, //cases are shown here
              exp_end))  //that the expect loop could encounter
    {
    case FOUNDSEARCH:
      foundSearch = true;
      break;
    case PROMPT:
      if (receivedPrompt)
        {
          shouldBreak = true;
          expectWorking = true;
        }
      else
        {
          receivedPrompt = true;
          fprintf(fp, "%s\r", "pwd");
        }
      break;
    case EXP_TIMEOUT:
      shouldBreak = true;
      break;
    case EXP_EOF:
      shouldBreak = true;
      break;
    }

      //cout << "exp_match : " << exp_match << endl;
    }

  cout << endl;
  if (foundSearch)
    {
      cout << "Expect found output of pwd" << endl;
    }
  else
    {
      cout << "Expect failed to find output of pwd" << endl;
    }
  if(expectWorking)
    {
      cout << "The expect interface is working" << endl;
    }
  else
    {
      cout << "The expect interface is not working" << endl;
    }


  cout << "The test program successfully reached the end" << endl;
}

我在这里所做的只是展示了如何初始化expect/tcl以防止szx所提到的问题。然后,我只是做了一个典型的expect问题,基本上说如果shell提示您输入密码,则发送pwd。然后,如果它给出当前目录,expect就可以工作了。这种结构对于像ssh这样的东西非常有用。比如说,如果你想自动化地ssh到某个地方,做一些事情,然后离开那里。特别是如果你想做几百次,而且你不想确认每个主机的真实性并每次输入密码。
请注意,由于我没有从源代码构建而是只使用了apt-get,因此在Ubuntu上我从未不得不这样做。然而,我的项目要求我从源代码构建,所以我找到了一个非常好的、整洁的方法,在http://www.linuxfromscratch.org/lfs/view/development/chapter05/tcl.htmlhttp://www.linuxfromscratch.org/lfs/view/development/chapter05/expect.html上...实际上,整个网站看起来都非常有用。
感谢szx的再次帮助。

我将其编辑到了我的问题中。 - benzeno
没有运行和回溯,我已经说过反汇编。它仍然是相同的地址。 - benzeno
这是一个有用的答案:https://dev59.com/LmXWa4cB1Zd3GeqPIwII - Phil Wallach
为了完整起见,我们用来修复libexpect的补丁在这里:http://patches.osdyson.org/patch/series/view/expect/5.45-2+dyson1/22-segfault-with-stubs.patch - Phil Wallach
在CentOS6上,我能够毫无问题地运行您的第一个示例,但在CentOS7上,启动时出现了相同的segv错误。我添加了您的“Tcl_Init”代码,问题就解决了。 - Mark Lakata
显示剩余2条评论
2个回答

4

exp_spawnv试图访问定义在Tcl内部的全局变量TclStubs *tclStubsPtr时,该变量恰好为NULL,而Tcl_ErrnoMsg被定义为该结构体成员(见tcl.h):

#ifndef Tcl_ErrnoMsg
#define Tcl_ErrnoMsg \
    (tclStubsPtr->tcl_ErrnoMsg) /* 128 */
#endif

我不熟悉expect和Tcl,但以上内容表明你可能应该调用一些初始化子例程(如果存在的话)或手动设置它。

啊,是的,那可能是个问题……我在 tcl.h 中没有找到它,但在 tclDecls.h 中找到了。
到目前为止,我在源代码目录中使用了 grep -r "tclStubsPtr" * 命令来查找指针的任何提及或初始化,并在 tclStubLib.c 中找到了它。此外,greplibtcl8.5.so 库中也找到了该指针。您会注意到我还没有与该库链接。
因此,我的下一步是链接它并查看是否解决了任何问题……如果没有,我将调查是否需要调用 tcl。
- benzeno
好的,原来你给了我一个很好的线索...tclStubsPtr指针保持空指针可能有很多原因。但无论出于什么根本原因,最大的问题是expect/tcl环境没有正确初始化。经过一番调查,我终于在 man libexpect(我开始的地方) 上找到了答案。Don Libes 作者在源代码分发中提供了 exp_main_exp.c 作为原型主程序。它涉及包括tcl头文件和expect_tcl联合头文件。 - benzeno
然后我强制让expect/tcl全局实体初始化。不幸的是,这有点像黑客行为...我不应该初始化tcl甚至包括tcl头文件。无论如何,我接受了你的答案,因为我从来没有想过要查找为什么tcl不能正确初始化。对于任何感兴趣的人,我已经将一个样本主函数编辑到我的问题中以实现此目的。 - benzeno

1
我最担心的是编译时的警告。接口显然要求您传递一个可写字符串,但您却传递了一个字符串常量。如果它确实进行写操作,那么将导致分段错误。因此,这看起来是您问题的一个很好的候选。
如果您尝试创建一个可写缓冲区并传递它会发生什么:
char name[] = "bash";
FILE* file = exp_popen(name);

更新:我已经测试了您的程序(使用上述更改和在结尾处添加“return 0;”),对我来说它运行良好。也许您的系统有问题,比如半安装的库?您可以检查是否在链接时使用-static也会失败。如果这样做,您可以确保编译时链接的库与运行时使用的库相同(因为它将在编译时包含在可执行文件中)。

谢谢你快速回复,Bas。 这确实解决了警告问题,但是同样的问题仍然存在。 - benzeno
抱歉回复简短,这是我在Stack Overflow上的第一个问题,我没有意识到在评论中按下回车键会发送它。对于我的糟糕编程实践(我的疏忽),我深表歉意,您确实有权提醒我。但不幸的是,这并不能解决主要问题,但可以在以后避免许多麻烦。 - benzeno
很遗憾它不能修复它。至于这里的评论,您可以随时删除或编辑它。 - Bas Wijnen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接