在Java中伪造堆栈跟踪

12

在Java中使用RMI时,当你收到异常时,远程堆栈跟踪信息将被添加在其前面,类似于以下内容:

ERROR Client received error when doing stuff:
myapp.FooBarException: bla
 at server.myMethod()
 at rmi.callHandler() // and now, on the next line comes the client
 at rmi.sendCall();
 at client.doServerMethod()
 at Thread.run()

这种类型的堆栈跟踪“伪造”是如何完成的?


我除了出于兴趣之外,还想用它做什么?嗯,如果我能做到这一点,它会帮助我:

outer() {

  thread = new Thread(...
      inner();
      // inner() throws
      // RuntimeException
      //  at inner();
      //  at Runnable.run();
      //  at Thread.run();
      //  at outer();
      //  at lalalala();
      //  ...

  ).start();

  thread.join();

}

为了记录日志,让inner()中抛出的异常也在堆栈跟踪信息中包含outer()(以及更低级别的方法)。

3个回答

15

这有点容易:

Throwable类有方法getStackTrace()setStackTrace()

来自我的项目之一(非开源,但也许我某天会公开远程调用引擎):

    /**
     * Setzt den Stack-Trace zusammen. Das untere Ende (tiefer in der
     * Aufrufhierarchie, am Anfang des Arrays/der Ausgabe) ist das,
     * welches im Throwable schon drin ist, das obere Ende wird aus
     * dem aktuellen Stack genommen. Dazwischen
     * kommt ein "Remote-Aufruf-Markierer".
     */

为您方便起见翻译:

合并堆栈跟踪信息。堆栈底部(在调用层次结构中更深,位于数组/输出的末尾)是已存在于堆栈中的内容,顶部将从当前堆栈中获取。在它们之间我们会放置一个远程调用标记

    private void mergeStackTraces(Throwable error)
    {
        StackTraceElement[] currentStack =
            new Throwable().getStackTrace();
        int currentStackLimit = 5; // TODO: raussuchen
        StackTraceElement[] oldStack =
            error.getStackTrace();
        StackTraceElement[] zusammen =
            new StackTraceElement[currentStack.length - currentStackLimit +
                                  oldStack.length + 1];
        System.arraycopy(oldStack, 0, zusammen, 0, oldStack.length);
        zusammen[oldStack.length] =
            new StackTraceElement("══════════════════════════",
                                  "<remote call %" +callID+ ">",
                                  "", -3);
        System.arraycopy(currentStack, currentStackLimit,
                         zusammen, oldStack.length+1,
                         currentStack.length - currentStackLimit);
        error.setStackTrace(zusammen);
    }

在服务器端,我已经切断了与方法调用本身无关的堆栈跟踪部分,即与消息处理相关的所有内容。

这会导致类似于以下的组合堆栈跟踪:

java.lang.SecurityException: Das Passwort für Nutzer »Paul« ist falsch.
        at de.fencing_game.db.userdb.Db4oUserDB.login(Db4oUserDB.java:304)
        at de.fencing_game.server.impl.StandardServers$SSServer$1.run(StandardServers.java:316)
        at de.fencing_game.server.impl.StandardServers$SSServer$1.run(StandardServers.java:314)
        at java.security.AccessController.doPrivileged(Native Method)
        at de.fencing_game.server.impl.StandardServers$SSServer.login(StandardServers.java:313)
        at de.fencing_game.transport.server.ServerTransport$ConnectionInfo$4.login(ServerTransport.java:460)
        at ══════════════════════════.<remote call %2>()
        at $Proxy1.login(Unknown Source)
        at de.fencing_game.gui.basics.LoginUtils.login(LoginUtils.java:80)
        at de.fencing_game.gui.Lobby.connectTo(Lobby.java:302)
        at de.fencing_game.gui.Lobby$20.run(Lobby.java:849)
        at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:226)
        at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:647)
        at java.awt.EventQueue.access$000(EventQueue.java:96)
        at java.awt.EventQueue$1.run(EventQueue.java:608)
        at java.awt.EventQueue$1.run(EventQueue.java:606)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.security.AccessControlContext$1.doIntersectionPrivilege(AccessControlContext.java:105)
        at java.awt.EventQueue.dispatchEvent(EventQueue.java:617)
        at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:275)
        at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:200)
        at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:190)
        at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:185)
        at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:177)
        at java.awt.EventDispatchThread.run(EventDispatchThread.java:138)

我想RMI系统做了类似的事情(只是没有══════════════════════════)。


编辑: 对于您的用例,当内部线程启动时,您需要保存外部线程的堆栈跟踪,然后在运行方法中捕获异常并将外部堆栈跟踪附加到内部异常的堆栈跟踪。不过,我真的建议添加某种类型的分隔符。


我希望内部异常具有外部异常的堆栈跟踪,即使它被内部线程中的某个深处捕获。我猜这是不可能的,因为我无法应用自定义异常。 - Bart van Heukelom
你的捕获代码必须执行此操作(或者异常本身在抛出之前必须执行此操作,覆盖fillInStackTrace或在构造函数中执行此操作)。这两种情况都无法处理由未知代码引发和捕获的未知异常。 - Paŭlo Ebermann
我喜欢你在一段代码片段中使用了三种语言 - 英语、德语和Java :) - bacar

2
你可以创建一个自定义异常,通过 setStackTrace() 从一个异常中提取堆栈跟踪并将其添加到另一个异常中。这对于像这样的操作或在不想维护原因异常的硬引用时维护堆栈跟踪非常有用。当将异常信息从服务器传递到客户端时,如果根本因素异常类可能不存在,则会导致序列化问题,因此这很方便。请注意保留HTML标记。

2
我想提出一种替代方案,虽然不是OP要求的,但可能更适合一些有类似问题的人。就像我一样。
我建议在外部创建一个可抛出对象,并将内部的可抛出对象作为外部可抛出对象的原因添加进去。这也会捕获线程切换的点……这可能有助于避免对堆栈跟踪的困惑。此外,线程信息可以存储在外部创建的可抛出对象中,这可能会更加有用。
以下是一些代码。
public class StackCaptor {
    public static Runnable capture(Runnable runnable) {
        // Capture the stack
        final Throwable prison = new Throwable();
        // Wrap run method to create a new throwable representing the creator of the original Runnable.
        return new Runnable() {
            @Override
            public void run() {
                try {
                    runnable.run();
                } catch (Throwable originalThrowable) {
                    RuntimeException callingThreadsException = new RuntimeException(originalThrowable);
                    callingThreadsException.setStackTrace(prison.getStackTrace());
                    throw callingThreadsException;
                }
            }
        };
    }
}

然后按照以下方式使用代码:

// This code has not be compiled or tested... You may need to use your
// smarts to get it working, but it should give you an idea.
public void outer() {
    Thread thread = new Thread(StackCaptor.capture(new Runnable() {
        public void run() { throw new RuntimeException("my ex"); }
    }));
    thread.start();
}

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