binary-parser icon indicating copy to clipboard operation
binary-parser copied to clipboard

JVM Class File parser example does not work for classes that contain Longs and Doubles in the Constant Pool

Open kylestev opened this issue 10 years ago • 0 comments

Background

... this is due to them taking up two indices in the Constant Pool:

All 8-byte constants take up two entries in the constant_pool table of the class file. If a CONSTANT_Long_info or CONSTANT_Double_info structure is the item in the constant_pool table at index n, then the next usable item in the pool is located at index n+2. The constant_pool index n+1 must be valid but is considered unusable.

In retrospect, making 8-byte constants take two constant pool entries was a poor choice.

I know this isn't a repo focused on JVM class file parsing, but thought it might spark a discussion or feature that allows for better interoperability non-standard binary formats like this.

Reproducing

$ javap -v <path to class file with at least one long or double> can verify my claims if you compare the output from the constant pool to that of the example Parser instance. The only problem with that, is there will be an error thrown before you can grab the contents from the parser.

Here's what I did to fix it and it works across Java 6/7/8:


export const ClassFileParser =
  Parser.start()
    .endianess('big')
    .uint32('magic', { assert: JVM_CLASS_FILE_MAGIC_NUMBER })
    .uint16('minor_version')
    .uint16('major_version')
    .uint16('constant_pool_count')
    .array('constant_pool', {
        type: ConstantPoolInfo,
        length: function () {
          let previousIndex = this.constant_pool.length - 1;
          let previousEntry = this.constant_pool[previousIndex];
          if (previousEntry && (previousEntry.tag === 6 || previousEntry.tag === 5)) {
            this.constant_pool[previousIndex - 1] = this.constant_pool[previousIndex];
            this.constant_pool_count -= 1;
            this.constant_pool.push(false);
          }

          return this.constant_pool_count - 1;
        }
    })
    .uint16('access_flags')
    .uint16('this_class')
    .uint16('super_class')
    .uint16('interface_count')
    .array('interfaces', {
      type: InterfaceInfo,
      length: function () {
        return this.interface_count - 1;
      }
    })
    .uint16('field_count')
    .array('fields', {
      type: ClassMemberInfo,
      length: function () {
        return this.field_count;
      }
    })
    .uint16('method_count')
    .array('methods', {
      type: ClassMemberInfo,
      length: function () {
        return this.method_count;
      }
    })
    .uint16('attribute_count')
    .array('attributes', {
      type: AttributeInfo,
      length: function () {
        return this.attribute_count;
      }
    });

While this does work, it's not very pretty having to do the following if the previous entry was a Long or Double:

  • move the previous entry back one slot in the array
  • subtract from the pool count
  • push a value that somewhat resembles a sentinel value (if I see an entry === false I know there was an incorrect index used)

But at least it makes it work :wink:

Thanks for releasing this library open source -- really made my life a lot easier and it has great performance :smile:


Output from running the above code.

1102 'NameAndType'
1103 'Fieldref'
1104 'Long'
1106 'Long'
1108 'Long'
1110 'NameAndType'
1111 'Fieldref'

Here's some validation for this by grepping javap's output:

image

kylestev avatar Oct 30 '15 14:10 kylestev