Java打印unicode故障

时间:2018-11-23 19:23:41

标签: java unicode jvm classloader bytecode

我当前正在编写一个程序来读取Java类文件。目前,我正在读取类文件的常量池(读取here)并将其打印到控制台。但是当它被打印时,某些unicode似乎以这种方式弄乱了我的终端,看起来像这样(以防万一,我正在读取的类文件是Kotlin编译的,而Terminal I我正在使用的是IntelliJ IDEA终端,尽管在使用常规Ubuntu终端时似乎不会出现故障。): Messed up terminal on IntelliJ IDEA 我认为我发现的是一个奇怪的Unicode序列,我认为这可能是某种转义序列。

这里是没有奇怪的unicode序列的整个输出:

{1=UTF8: (42)'deerangle/decompiler/main/DecompilerMainKt', 2=Class index: 1, 3=UTF8: (16)'java/lang/Object', 4=Class index: 3, 5=UTF8: (4)'main', 6=UTF8: (22)'([Ljava/lang/String;)V', 7=UTF8: (35)'Lorg/jetbrains/annotations/NotNull;', 8=UTF8: (4)'args', 9=String index: 8, 10=UTF8: (30)'kotlin/jvm/internal/Intrinsics', 11=Class index: 10, 12=UTF8: (23)'checkParameterIsNotNull', 13=UTF8: (39)'(Ljava/lang/Object;Ljava/lang/String;)V', 14=Method name index: 12; Type descriptor index: 13, 15=Bootstrap method attribute index: 11; NameType index: 14, 16=UTF8: (12)'java/io/File', 17=Class index: 16, 18=UTF8: (6)'<init>', 19=UTF8: (21)'(Ljava/lang/String;)V', 20=Method name index: 18; Type descriptor index: 19, 21=Bootstrap method attribute index: 17; NameType index: 20, 22=UTF8: (15)'getAbsolutePath', 23=UTF8: (20)'()Ljava/lang/String;', 24=Method name index: 22; Type descriptor index: 23, 25=Bootstrap method attribute index: 17; NameType index: 24, 26=UTF8: (16)'java/lang/System', 27=Class index: 26, 28=UTF8: (3)'out', 29=UTF8: (21)'Ljava/io/PrintStream;', 30=Method name index: 28; Type descriptor index: 29, 31=Bootstrap method attribute index: 27; NameType index: 30, 32=UTF8: (19)'java/io/PrintStream', 33=Class index: 32, 34=UTF8: (5)'print', 35=UTF8: (21)'(Ljava/lang/Object;)V', 36=Method name index: 34; Type descriptor index: 35, 37=Bootstrap method attribute index: 33; NameType index: 36, 38=UTF8: (19)'[Ljava/lang/String;', 39=Class index: 38, 40=UTF8: (17)'Lkotlin/Metadata;', 41=UTF8: (2)'mv', 42=Int: 1, 43=Int: 11, 44=UTF8: (2)'bv', 45=Int: 0, 46=Int: 2, 47=UTF8: (1)'k', 48=UTF8: (2)'d1', 49=UTF8: (58)'WEIRD_UNICODE_SEQUENCE', 50=UTF8: (2)'d2', 51=UTF8: (0)'', 52=UTF8: (10)'Decompiler', 53=UTF8: (17)'DecompilerMain.kt', 54=UTF8: (4)'Code', 55=UTF8: (18)'LocalVariableTable', 56=UTF8: (15)'LineNumberTable', 57=UTF8: (13)'StackMapTable', 58=UTF8: (36)'RuntimeInvisibleParameterAnnotations', 59=UTF8: (10)'SourceFile', 60=UTF8: (20)'SourceDebugExtension', 61=UTF8: (25)'RuntimeVisibleAnnotations'}
AccessFlags: {ACC_PUBLIC, ACC_FINAL, ACC_SUPER}

这是在Sublime Text中打开的Unicode序列: Strange unicode in sublime text

关于这件事的我的问题是:为什么这种Unicode会破坏IntelliJ IDEA中的控制台,为什么在Kotlin-Class-Files中如此常见,以及在打印之前如何从字符串中删除所有这些“转义序列”?它吗?

3 个答案:

答案 0 :(得分:5)

由于某些不可思议的原因,当Sun Microsystems设计Java时,他们决定使用非UTF8编码对常量池中的字符串进行编码。它是仅Java编译器和类加载器使用的自定义编码。

更糟的是,他们决定在JVM文档中将其称为UTF8。但这不是 UTF8,他们对名称的选择引起了很多不必要的混乱。因此,我在这里推测的是,您看到他们将其称为UTF8,因此您将其视为 real UTF8,因此收到了垃圾。

您将需要在JVM规范中寻找CONSTANT_Utf8_info的描述,并编写一种根据其规范对字符串进行解码的算法。

为方便起见,以下是我为此编写的一些代码:

public static char[] charsFromBytes( byte[] bytes )
{
    int t = 0;
    int end = bytes.length;
    for( int s = 0;  s < end;  )
    {
        int b1 = bytes[s] & 0xff;
        if( b1 >> 4 >= 0 && b1 >> 4 <= 7 ) /* 0x0xxx_xxxx */
            s++;
        else if( b1 >> 4 >= 12 && b1 >> 4 <= 13 ) /* 0x110x_xxxx 0x10xx_xxxx */
            s += 2;
        else if( b1 >> 4 == 14 ) /* 0x1110_xxxx 0x10xx_xxxx 0x10xx_xxxx */
            s += 3;
        t++;
    }
    char[] chars = new char[t];
    t = 0;
    for( int s = 0;  s < end;  )
    {
        int b1 = bytes[s++] & 0xff;
        if( b1 >> 4 >= 0 && b1 >> 4 <= 7 ) /* 0x0xxx_xxxx */
            chars[t++] = (char)b1;
        else if( b1 >> 4 >= 12 && b1 >> 4 <= 13 ) /* 0x110x_xxxx 0x10xx_xxxx */
        {
            assert s < end : new IncompleteUtf8Exception( s );
            int b2 = bytes[s++] & 0xff;
            assert (b2 & 0xc0) == 0x80 : new MalformedUtf8Exception( s - 1 );
            chars[t++] = (char)(((b1 & 0x1f) << 6) | (b2 & 0x3f));
        }
        else if( b1 >> 4 == 14 ) /* 0x1110_xxxx 0x10xx_xxxx 0x10xx_xxxx */
        {
            assert s < end : new IncompleteUtf8Exception( s );
            int b2 = bytes[s++] & 0xff;
            assert (b2 & 0xc0) == 0x80 : new MalformedUtf8Exception( s - 1 );
            assert s < end : new IncompleteUtf8Exception( s );
            int b3 = bytes[s++] & 0xff;
            assert (b3 & 0xc0) == 0x80 : new MalformedUtf8Exception( s - 1 );
            chars[t++] = (char)(((b1 & 0x0f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f));
        }
        else
            assert false;
    }
    return chars;
}

答案 1 :(得分:4)

迈克的答案已经涵盖了Java类文件并不完全使用UTF8编码这一事实,但是我认为我将提供有关它的更多信息。

Java类文件中使用的编码称为修改的UTF-8(或MUTF-8)。它与常规UTF-8有两个不同之处:

  • 空字节使用两个字节序列进行编码
  • 与UTF16中一样,BLI之外的
  • 代码点用代理对表示。该对中的每个代码点又使用常规UTF8编码以三个字节进行编码。

第一个更改是使编码的数据不包含原始的空字节,这使编写C代码时处理起来更容易。第二个变化是由于90年代UTF-16风靡一时,而且UTF-8最终能否胜出尚不清楚。实际上,出于类似的原因,Java使用16位字符。使用代理对对星体字符进行编码使事情在16位环境中更加容易处理。请注意,大约在同一时间设计的Javascript在UTF-16字符串上也有类似的问题。

无论如何,对MUTF-8进行编码和解码非常简单。这很烦人,因为它没有内置在任何地方。解码时,您以与UTF-8相同的方式进行解码,只需要具有更大的容忍度,除了在技术上不是有效的UTF-8的序列(尽管使用相同的编码),然后替换适用的代理对。编码时,您要进行相反操作。

请注意,这仅适用于Java字节码。 Java的程序员通常不必处理MUTF-8,因为Java在其他地方都使用了UTF-16和真正的UTF-8的混合物。

答案 2 :(得分:3)

IntelliJ的控制台很可能会将字符串的某些字符解释为控制字符(与Colorize console output in Intellij products比较)。

最有可能的是ANSI终端仿真,您可以通过执行轻松验证

System.out.println("Hello "
    + "\33[31mc\33[32mo\33[33ml\33[34mo\33[35mr\33[36me\33[37md"
    + " \33[30mtext");

如果您看到此文本使用不同的颜色打印,则表示是ANSI终端兼容的解释。

但是从未知来源打印字符串时,最好删除控制字符。来自类文件的字符串常量不需要具有人类可读的内容。

一种简单的方法是

System.out.println(string.replaceAll("\\p{IsControl}", "."));

它将在打印之前用点替换所有控制字符。

如果您想获取有关实际char值的诊断信息,可以使用例如

System.out.println(Pattern.compile("\\p{IsControl}").matcher(string)
    .replaceAll(mr -> String.format("{%02X}", (int)string.charAt(mr.start()))));

这需要Java 9,但是当然,对于较早的Java版本也可以实现相同的逻辑。它只需要更多详细的代码。

Pattern返回的Pattern.compile("\\p{IsControl}")实例可以存储和重用。