捕获崩溃子进程的"Segmentation fault"消息:在调用communicate()后没有输出和错误信息。

20

我使用subprocess模块获取崩溃程序的输出时遇到了问题。 我正在使用Python2.7和subprocess来调用一个带有奇怪参数的程序,以便获得一些段错误。 为了调用该程序,我使用以下代码:

proc = (subprocess.Popen(called,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE))
out,err=proc.communicate()
print out,err

called是一个包含程序名称和参数的列表(字符串,其中包含随机字节,除了subprocess完全不喜欢的NULL字节)

当程序没有崩溃时,该代码会表现出并显示stdout和stderr,但当它崩溃时,out和err为空,而不是显示著名的“Segmentation fault”。

我希望找到一种方法,即使程序崩溃了,也能获得out和err。

我还尝试了check_output / call / check_call方法

一些附加信息:

  • 我在python虚拟环境中的Archlinux 64位上运行此脚本(这里不应该有什么重要的东西,但你永远不知道:p)

  • segfault发生在我正在尝试运行的C程序中,是缓冲区溢出的结果

  • 问题在于当segfault发生时,我无法获取用subprocess发生的输出

  • 我正确地获得了返回码:-11(SIGSEGV)

  • 使用Python我得到:

  •   ./dumb2 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 
      ('Exit code was:', -11) 
      ('Output was:', '') 
      ('Errors were:', '')
    
  • 当我在python外部时,我得到:

     ./dumb2 $(perl -e "print 'A'x50")  
     BEGINNING OF PROGRAM 
     AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
     END OF THE PROGRAM
     Segmentation fault (core dumped)
    
  • Shell的返回值相同:echo $? 返回139,因此为-11($?&128)

3个回答

12

"分段错误"信息可能由shell生成。要确定进程是否被SIGSEGV终止,请检查proc.returncode == -signal.SIGSEGV

如果您想查看消息,可以在shell中运行以下命令:

#!/usr/bin/env python
from subprocess import Popen, PIPE

proc = Popen(shell_command, shell=True, stdout=PIPE, stderr=PIPE)
out, err = proc.communicate()
print out, err, proc.returncode

我已经使用shell_command="python -c 'from ctypes import *; memset(0,1,1)'"进行了测试,它会导致段错误,并且消息被捕获在err中。

如果消息直接打印到终端上,则可以使用pexpect模块来捕获:

#!/usr/bin/env python
from pipes import quote
from pexpect import run # $ pip install pexpect

out, returncode = run("sh -c " + quote(shell_command), withexitstatus=1)
signal = returncode - 128 # 128+n
print out, signal

或者直接使用 Python 标准库中的 pty 模块:

#!/usr/bin/env python
import os
import pty
from select import select
from subprocess import Popen, STDOUT

# use pseudo-tty to capture output printed directly to the terminal
master_fd, slave_fd = pty.openpty()
p = Popen(shell_command, shell=True, stdin=slave_fd, stdout=slave_fd,
          stderr=STDOUT, close_fds=True)
buf = []
while True:
    if select([master_fd], [], [], 0.04)[0]: # has something to read
        data = os.read(master_fd, 1 << 20)
        if data:
            buf.append(data)
        else: # EOF
            break
    elif p.poll() is not None: # process is done
        assert not select([master_fd], [], [], 0)[0] # nothing to read
        break
os.close(slave_fd)
os.close(master_fd)
print "".join(buf), p.returncode-128

我尝试使用shell选项,但是得到了相同的行为:./dumb2 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ('退出代码为:',-11) ('输出为:','') ('错误为:','')而在python外部,我得到了:./dumb2 $(perl -e“print Ax50”) 程序开始 分段错误(核心已转储) - Tic
这意味着错误消息直接打印到终端而不是 shell 的标准输出。您可以使用 pexpect、pty 模块来捕获此类输出。 - jfs
谢谢,pexpect似乎是一个很好的替代方案,我明天会尝试并发布结果。 - Tic
@Tic:我已经在我的机器上测试了这段代码,它可以正常工作(将消息捕获到err变量中),也就是说,在Ubuntu上至少不需要使用pexpect - jfs
1
它在我的电脑上不起作用,但是pexpect返回了stdout,所以谢谢 :) - Tic

0
回到这里:使用Python3的子进程非常好用,如果你在Linux上,还有一个名为subprocess32的向后移植到Python2的解决方案也可以很好地工作。
旧的解决方案是我使用pexpect,它也能正常工作。
def cmd_line_call(name, args):
    child = pexpect.spawn(name, args)
    # Wait for the end of the output
    child.expect(pexpect.EOF) 
    out = child.before # we get all the data before the EOF (stderr and stdout)
    child.close() # that will set the return code for us
    # signalstatus and existstatus read as the same (for my purpose only)
    if child.exitstatus is None:
        returncode = child.signalstatus
    else:
        returncode = child.exitstatus
    return (out, returncode)
    

注意:速度会稍慢(因为它会生成一个伪终端)


注意:child.before是一个字符串,不可调用;请删除() - jfs
不要将 signalstatus 和 exitstatus 视为相同的值;它们是不同的。 - jfs
感谢 J.F Sebastian 的更正 :) 你的答案更加完整,成为了正确的答案。 - Tic
(在我的情况下,任何不为0的状态都意味着出现了错误。) - Tic
显示剩余2条评论

-1
proc = (subprocess.Popen(called, stdout=subprocess.PIPE, stderr=subprocess.PIPE))

print(proc.stdout.read())
print(proc.stderr.read())

这应该会更好。
个人而言,我会选择:

from subprocess import Popen, PIPE

handle = Popen(called, shell=True, stdout=PIPE, stderr=PIPE)
output = ''
error = ''

while handle.poll() is None:
    output += handle.stdout.readline() + '\n'
    error += handle.stderr.readline() + '\n'

handle.stdout.close()
handle.stderr.close()

print('Exit code was:', handle.poll())
print('Output was:', output)
print('Errors were:', error)

如果可能的话,最好使用epoll()来处理stderr,因为有时它会因为空而阻塞调用,这就是为什么我懒得时会使用stderr=STDOUT


感谢您的快速回答,我得到了相同的结果,除了 '\n' 之外,没有输出或错误。 在不出现段错误的情况下,我得到了一些奇怪的东西,输出被截断了:只显示了第一行。 - Tic
@Tic 你确定 called 应用程序没有发生段错误吗?这不是推荐的做法,因为如果输出过多,它会导致应用程序挂起,但是将 output += ..error += ... 替换为 pass,并让循环继续直到进程完成。在 while handle.poll() 之后,通过执行以下操作对输出进行扫描:output = handle.stdout.read(),对 stderr 也是同样的操作,看看是否有更多的信息可以捕获。它们应该执行相同类型的操作,但只需尝试一下,看看是否有帮助。 - Torxed
除非您使用的是Windows操作系统;否则指定shell=True会极大地改变called的含义。 - jfs
即使“handle.poll() is not None”,输出也可能被缓冲。这是可以使用“out += handle.stdout.read(); err += handle.stderr.read()”的地方。 - jfs
@J.F.Sebastian在测试这个解决方案时没有使用shell=True(抱歉忘了提到,我没有考虑就这么做了^^)。我将编辑我的系统配置:它是一个64位的ArchLinux。 - Tic
显示剩余3条评论

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