Programs compiled by R8 are not structurally the same as the input. For example, names, invokes and line numbers can all change when compiling with R8. The correspondence between the input and the output is recorded in a mapping file. The mapping file can be used with retrace to recover the original stack trace from the stack traces produced by running the R8 compiled program. More information about R8 and mapping files can be found here.
The format for additional information is encoded as comments with json formatted data. The data has an ID to disambiguate it from other information.
The version information states what version the content of the mapping file is using.
The format of the version information is:
# {"id":"com.android.tools.r8.mapping","version":"1.0"}
Here id
must be com.android.tools.r8.mapping
and version
is the version of
the mapping file.
The version information applies to content of the file following the position of the version entry until either the end of the file or another version entry is present.
If no version information is present, the content is assumed to have version zero and no additional mapping information except Source File.
When interpreting the mapping file, any additional mapping information pertaining to a later version should be ignored. In other words, treated as a normal comment.
Retracing tools supporting this versioning scheme should issue a warning when given mapping files with versions higher than the version supported by the tool.
- Version 1.0 was introduced by R8 in version 3.1.21
The source file information states what source file a class originated from.
The source file information must be placed directly below the class mapping it pertains to. The format of the source file information is:
some.package.Class -> foo.a:
# {"id":"sourceFile","fileName":"R8Test.kt"}
Here id
must be the string sourceFile
and fileName
is the source file name
as a string value (in this example R8Test.kt
).
Note that the id
of the source file information is unqualified. It is the only
allowed unqualified identifier as it was introduced prior to the versioning
scheme.
The synthesized information states what parts of the compiled output program are synthesized by the compiler. A retracing tool should use the synthesized information to strip out synthesized method frames from retraced stacks.
The synthesized information must be placed directly below the class, field or method mapping it pertains to. The format of the synthesized information is:
# {'id':'com.android.tools.r8.synthesized'}
Here id
must be com.android.tools.r8.synthesized
. There is no other content.
A class mapping would be:
some.package.SomeSynthesizedClass -> x.y.z:
# {'id':'com.android.tools.r8.synthesized'}
This specifies that the class x.y.z
has been synthesized by the compiler and
thus it did not exist in the original input program.
Notice that the left-hand side and right-hand side, here
some.package.SomeSynthesizedClass
and x.y.z
respectively, could be any class
name. It is likely that the mapping for synthetics is the identity, but a useful
name can be placed here if possible which legacy retrace tools would use when
retracing.
A field mapping would be:
some.package.Class -> x.y.z:
int someField -> a
# {'id':'com.android.tools.r8.synthesized'}
This specifies that the field x.y.z.a
has been synthesized by the compiler. As
for classes, since the field is not part of the original input program, the
left- and right-hand side names could be anything. Note that a field can be
synthesized without the class being synthesized.
A method mapping would be:
some.package.SomeSynthesizedClass -> x.y.z:
void someMethod() -> a
# {'id':'com.android.tools.r8.synthesized'}
This specifies that the method x.y.z.a()
has been synthesized by the compiler.
As for classes, since the method is not part of the original input program, the
left- and right-hand side names could be anything. Note that a method can be
synthesized without the class being synthesized.
For inline frames a mapping would be:
some.package.Class -> foo.a:
4:4:void example.Foo.lambda$main$0():225 -> a
4:4:void run(example.Foo):2 -> a
# {'id':'com.android.tools.r8.synthesized'}
5:5:void example.Foo.lambda$main$1():228 -> a
5:5:void run(example.Foo):4 -> a
# {'id':'com.android.tools.r8.synthesized'} <-- redundant
This specifies that line 4 in the method foo.a.a
is in a method that has
been synthesized by the compiler. Since the method is either synthesized or not
any extra synthesized comments will have no effect.
Synthesized information should never be placed on inlined frames:
some.package.Class -> foo.a:
4:4:void example.Foo.syntheticThatIsInlined():225 -> a
# {'id':'com.android.tools.r8.synthesized'}
4:4:void run(example.Foo):2 -> a
In the above, the mapping information suggests that the inline frame
example.Foo.syntheticThatIsInlined
should be marked as synthesized
. However,
since that method was not part of the input program it should not be in the
output mapping information at all.
The RewriteFrame information informs the retrace tool that when retracing a frame it should rewrite it. The mapping information has the form:
# { id: 'com.android.tools.r8.rewriteFrame', "
conditions: ['throws(<exceptionDescriptor>)'],
actions: ['removeInnerFrames(<count>)'] }
The format is to specify conditions for when the rule should be applied and then describe the actions to take. The following conditions exist:
throws(<exceptionDescriptor>)
: Will be true if the thrown exception above is<exceptionDescriptor>
Conditions can be combined by adding more items to the list. The semantics of having more elements in the list is that the conditions are AND'ed together. To achieve OR one should duplicate the information.
Actions describe what should happen to the retraced frames if the condition holds. Multiple specified actions will be applied from left to right. The following actions exist:
removeInnerFrames(<count>)
: Will remove the number of frames starting with inner most frame. It is an error to specify a count higher than all frames.
An example could be to remove an inlined frame if a null-pointer-exception is thrown:
some.Class -> a:
4:4:void other.Class.inlinee():23:23 -> a
4:4:void caller(other.Class):7 -> a\n"
# { id: 'com.android.tools.r8.rewriteFrame', "
conditions: ['throws(Ljava/lang/NullPointerException;)'],
actions: ['removeInnerFrames(1)'] }
When retracing:
Exception in thread "main" java.lang.NullPointerException: ...
at a.a(:4)
It will normally retrace to:
Exception in thread "main" java.lang.NullPointerException: ...
at other.Class.inlinee(Class.java:23)
at some.Class.caller(Class.java:7)
Amending the last mapping with the above inline information instructs the retracer to discard frames above, resulting in the retrace result:
Exception in thread "main" java.lang.NullPointerException: ...
at some.Class.caller(Class.java:7)
The rewriteFrame
information will only be applied if the line that is being
retraced is directly under the exception line.
The outline information can be used by compilers to specify that a method is an outline. It has the following format:
# { 'id':'com.android.tools.r8.outline' }
When a retracer retraces a frame that has the outline mapping information it
should carry the reported position to the next frame and use the
outlineCallsite
to obtain the correct position.
A position in an outline can correspond to multiple different positions depending on the context. The information can be stored in the mapping file with the following format:
# { 'id':'com.android.tools.r8.outlineCallsite',
'positions': {
'outline_pos_1': callsite_pos_1,
'outline_pos_2': callsite_pos_2,
...
},
'outline': 'outline':'La;a()I'
}
The outline
key was added in 2.2 and should be the residual descriptor of the
outline.
The retracer should when seeing the outline
information carry the line number
to the next frame. The position should be rewritten by using the positions map
before using the resulting position for further retracing. Here is an example:
# { id: 'com.android.tools.r8.mapping', version: '2.0' }
outline.Class -> a:
1:2:int outline() -> a
# { 'id':'com.android.tools.r8.outline' }
some.Class -> b:
1:1:void foo.bar.Baz.qux():42:42 -> s
4:4:int outlineCaller(int):98:98 -> s
5:5:int outlineCaller(int):100:100 -> s
27:27:int outlineCaller(int):0:0 -> s
# { 'id':'com.android.tools.r8.outlineCallsite',
'positions': { '1': 4, '2': 5 },
'outline':'La;a()I'}
Retracing the following stack trace lines:
at a.a(:1)
at b.s(:27)
Should first retrace the first line and see it is an outline
and then use
the outlineCallsite
for b.s
at position 27
to map the read position 1
to
position 4
and then use that to find the actual mapping, resulting in the
retraced stack:
at some.Class.outlineCaller(Class.java:98)
It should be such that for all stack traces, if a retracer ever see an outline
the next obfuscated line should contain outlineCallSite
information.
If only a single position is needed for retracing a method correctly one can
skip emitting the position and rely on retrace to retrace correctly. To ensure
compatibility R8 emits a catch-all range 0:65535
as such:
0:65535:void foo():33:33 -> a
It does not matter if the mapping is an inline frame. Catch all ranges should never be used for overloads.
The residual signature information was added to mitigate the problem with no right hand side signature in mapping files. The information should be placed directly under the first occurrence of a field or method.
com.bar -> a:
com.foo -> b:
com.bar m1(int) -> m2,
# { id: 'com.android.tools.r8.residualsignature', signature:'(Z)a' }
Similar for fields:
com.bar -> a:
com.foo -> b:
com.bar f1 -> f2,
# { id: 'com.android.tools.r8.residualsignature', signature:'a' }
If the residual definition has changed arguments or return type then the signature should be emitted. The residual signature has no effect on retracing stack traces but they are necessary when interacting with residual signatures through the Retrace Api or for composing mapping files.