火车撞鸟
本帖最后由 火车撞鸟 于 2021-11-18 21:39 编辑

聊聊 Mod 开发中行号的问题


有时候我们在 debug 过程中看调用堆栈,通常堆栈后都会指示一个行号,但是有人会发现这个行号在生产环境中对于 Minecraft 的类与开发环境里的行号是不能对应的,因此本篇文章会讲述如何正确获取行号。


本篇以 Forge 为例。


堆栈中的行号是怎么来的?


源代码每行都有对应的行号,然而实际运行的时候并不存在源代码,只有编译好的 class 二进制文件,因此行号是被编译器塞进 class 字节码中去的,而调用堆栈显示的行号就是字节码中被编译器塞进去的行号。


反编译器中的行号是什么?


我们在 debug 一些 mod 时通常会反编译这些 mod,然而有人会发现反编译器中显示的行号与堆栈中的行号并不对应,我们以如下报错举例:


java.util.NoSuchElementException: null
    at java.util.ArrayDeque.getFirst(ArrayDeque.java:324) ~[?:1.8.0_51] {}
    at vazkii.patchouli.client.book.text.SpanState.peekStyle(SpanState.java:76) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.text.Span.<init>(Span.java:30) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.text.BookTextParser.processCommands(BookTextParser.java:231) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.text.BookTextParser.parse(BookTextParser.java:207) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.gui.BookTextRenderer.build(BookTextRenderer.java:50) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.gui.BookTextRenderer.<init>(BookTextRenderer.java:45) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.gui.BookTextRenderer.<init>(BookTextRenderer.java:28) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.page.abstr.PageWithText.onDisplayed(PageWithText.java:26) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.gui.GuiBookEntry.setupPages(GuiBookEntry.java:154) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.gui.GuiBookEntry.func_231160_c_(GuiBookEntry.java:54) ~[?:1.16.4-48] {re:classloading}
    at net.minecraft.client.gui.screen.Screen.func_231158_b_(Screen.java:325) ~[?:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:computing_frames,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:APP:quark.mixins.json:client.ScreenMixin,pl:mixin:A,pl:runtimedistcleaner:A}
    at net.minecraft.client.Minecraft.func_147108_a(Minecraft.java:852) ~[?:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,xf:fml:customwindowtitle:CustomWindowTitle,pl:mixin:APP:charm.mixins.json:accessor.MinecraftAccessor,pl:mixin:APP:assets/botania/botania.mixins.json:AccessorMinecraft,pl:mixin:A,pl:runtimedistcleaner:A}
    at vazkii.patchouli.client.book.BookContents.openLexiconGui(BookContents.java:83) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.gui.GuiBookEntry.displayOrBookmark(GuiBookEntry.java:228) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.gui.GuiBookEntryList.handleButtonEntry(GuiBookEntryList.java:167) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.gui.GuiBookEntryList$$Lambda$10790/1627619043.onPress(Unknown Source) ~[?:?] {}
    at net.minecraft.client.gui.widget.button.Button.func_230930_b_(SourceFile:33) ~[?:?] {re:classloading}
    at net.minecraft.client.gui.widget.button.AbstractButton.func_230982_a_(SourceFile:16) ~[?:?] {re:classloading}
    at net.minecraft.client.gui.widget.Widget.func_231044_a_(Widget.java:136) ~[?:?] {re:classloading,pl:runtimedistcleaner:A}
    at net.minecraft.client.gui.INestedGuiEventHandler.func_231044_a_(SourceFile:27) ~[?:?] {re:computing_frames,re:mixin,re:classloading}
    at vazkii.patchouli.client.book.gui.GuiBook.mouseClickedScaled(GuiBook.java:301) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.gui.GuiBookEntryList.mouseClickedScaled(GuiBookEntryList.java:125) ~[?:1.16.4-48] {re:classloading}
    at vazkii.patchouli.client.book.gui.GuiBook.func_231044_a_(GuiBook.java:278) ~[?:1.16.4-48] {re:classloading}
    at net.minecraft.client.MouseHelper.lambda$mouseButtonCallback$0(MouseHelper.java:87) ~[?:?] {re:classloading,pl:runtimedistcleaner:A}
    at net.minecraft.client.MouseHelper$$Lambda$8051/1190066743.run(Unknown Source) ~[?:?] {}
    at net.minecraft.client.gui.screen.Screen.func_231153_a_(Screen.java:427) ~[?:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:computing_frames,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:APP:quark.mixins.json:client.ScreenMixin,pl:mixin:A,pl:runtimedistcleaner:A}
    at net.minecraft.client.MouseHelper.func_198023_a(MouseHelper.java:85) ~[?:?] {re:classloading,pl:runtimedistcleaner:A}
    at net.minecraft.client.MouseHelper.lambda$null$4(MouseHelper.java:175) ~[?:?] {re:classloading,pl:runtimedistcleaner:A}
    at net.minecraft.client.MouseHelper$$Lambda$8050/373583099.run(Unknown Source) ~[?:?] {}
    at net.minecraft.util.concurrent.ThreadTaskExecutor.execute(SourceFile:94) ~[?:?] {re:computing_frames,pl:accesstransformer:B,re:mixin,pl:accesstransformer:B,re:classloading,pl:accesstransformer:B}
    at net.minecraft.client.MouseHelper.lambda$registerCallbacks$5(MouseHelper.java:174) ~[?:?] {re:classloading,pl:runtimedistcleaner:A}
    at net.minecraft.client.MouseHelper$$Lambda$6714/1514018399.invoke(Unknown Source) ~[?:?] {}
    at org.lwjgl.glfw.GLFWMouseButtonCallback$Container.invoke(GLFWMouseButtonCallback.java:81) ~[lwjgl-glfw-3.2.2.jar:build 10] {}
    at bre2el.fpsreducer.handler.glfw.InputEventHandler$MouseButtonEventHandler.invoke(InputEventHandler.java:99) ~[?:mc1.16.4-1.18] {re:classloading}
    at org.lwjgl.glfw.GLFWMouseButtonCallbackI.callback(GLFWMouseButtonCallbackI.java:36) ~[lwjgl-glfw-3.2.2.jar:build 10] {}
    at org.lwjgl.system.JNI.invokeV(Native Method) ~[lwjgl-3.2.2.jar:build 10] {}
    at org.lwjgl.glfw.GLFW.glfwPollEvents(GLFW.java:3101) ~[lwjgl-glfw-3.2.2.jar:build 10] {}
    at com.mojang.blaze3d.systems.RenderSystem.flipFrame(SourceFile:102) ~[?:?] {re:classloading}
    at net.minecraft.client.MainWindow.func_227802_e_(MainWindow.java:305) ~[?:?] {re:classloading,pl:runtimedistcleaner:A}
    at net.minecraft.client.Minecraft.func_195542_b(Minecraft.java:996) [?:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,xf:fml:customwindowtitle:CustomWindowTitle,pl:mixin:APP:charm.mixins.json:accessor.MinecraftAccessor,pl:mixin:APP:assets/botania/botania.mixins.json:AccessorMinecraft,pl:mixin:A,pl:runtimedistcleaner:A}
    at net.minecraft.client.Minecraft.func_99999_d(Minecraft.java:607) [?:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,xf:fml:customwindowtitle:CustomWindowTitle,pl:mixin:APP:charm.mixins.json:accessor.MinecraftAccessor,pl:mixin:APP:assets/botania/botania.mixins.json:AccessorMinecraft,pl:mixin:A,pl:runtimedistcleaner:A}
    at net.minecraft.client.main.Main.main(Main.java:184) [?:?] {re:classloading,re:mixin,pl:runtimedistcleaner:A,pl:mixin:A,pl:runtimedistcleaner:A}
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_51] {}
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_51] {}
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_51] {}
    at java.lang.reflect.Method.invoke(Method.java:497) ~[?:1.8.0_51] {}
    at net.minecraftForge.fml.loading.FMLClientLaunchProvider.lambda$launchService$0(FMLClientLaunchProvider.java:51) [Forge-1.16.5-36.0.13.jar:36.0] {}
    at net.minecraftForge.fml.loading.FMLClientLaunchProvider$$Lambda$485/1614427365.call(Unknown Source) [Forge-1.16.5-36.0.13.jar:36.0] {}
    at cpw.mods.modlauncher.LaunchServiceHandlerDecorator.launch(LaunchServiceHandlerDecorator.java:37) [modlauncher-8.0.9.jar:?] {}
    at cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:54) [modlauncher-8.0.9.jar:?] {}
    at cpw.mods.modlauncher.LaunchServiceHandler.launch(LaunchServiceHandler.java:72) [modlauncher-8.0.9.jar:?] {}
    at cpw.mods.modlauncher.Launcher.run(Launcher.java:82) [modlauncher-8.0.9.jar:?] {}
    at cpw.mods.modlauncher.Launcher.main(Launcher.java:66) [modlauncher-8.0.9.jar:?] {}

我们可以发现 vazkii.patchouli.client.book.text.SpanState 的第 76 行调用了 java.util.ArrayDeque.getFirst,但是我们反编译之后发现第 76 行并不是这样的:(反编译器是 Luyten-0.5.4

image.png

原因是反编译器中的行号仅仅只是用于指示反编译后的文件的行号,并没有其他作用,众所周知,反编译后的代码基本不等同于源代码。好在 Luyten 提供了一个非常有用的功能,能够显示原始代码中的行号(Settings -- Show Debug Line Numbers),照着这个行号,我们就能找到对应的地方:

image.png


为什么在开发环境中运行时的行号是准确的?


有人会发现生产环境中的调用堆栈行号无法和开发环境中 Minecraft 的类对应,但是在开发环境中运行游戏时调用堆栈行号都能正确对应。因此要回答这个问题,首先需要知道开发环境中的 Minecraft 源代码是怎么来的:

众所周知 Forge 在构建开发环境时需要反编译 Minecraft 以获得「源代码」,那么 Forge 反编译出的代码做了以下步骤:  



这样就构成了我们在开发环境中看到的 Minecraft 所谓的「源代码」,但它并不等同于 mojang 那里的源代码。所有以上步骤还不足以让 class 字节码中的行号和反编译的行号对应起来,它们是为了给接下来这个步骤做铺垫 —— recompile,即重新编译之前生成好的代码,这样开发环境中 Minecraft 的二进制 class 文件和源代码 java 文件的行号就对应起来了。

根据以上条件,很容易就能知道为什么生产环境运行时的堆栈和开发环境中的不对应了:



如何在生产环境中获得准确的行号?


由上一个问题可以知道,我们最好能拿到最终实际加载到 JVM 中去的类的二进制文件并进行反编译。

那么我们如何得到最终运行时的 class 二进制文件呢?



然后按上述使用 Luyten 的方式反编译它们就能获得准确的行号。




道家深湖
前来膜拜大佬,之前行号我都是当没有,直接看文件猜的……

君の名
牛逼,收藏了

2660759456
用开发带来一个不一样的Minecraft

bugjang的特性
经典1.13分界线
模组坟场

第一页 上一页 下一页 最后一页