Morten Krogh-Jespersen | 00d7980 | 2021-09-06 13:55:28 +0200 | [diff] [blame] | 1 | # R8, Retrace and map file versioning |
| 2 | |
| 3 | Programs compiled by R8 are not structurally the same as the input. For example, |
| 4 | names, invokes and line numbers can all change when compiling with R8. The |
| 5 | correspondence between the input and the output is recorded in a mapping file. |
| 6 | The mapping file can be used with retrace to recover the original stack trace |
| 7 | from the stack traces produced by running the R8 compiled program. More |
| 8 | information about R8 and mapping files can be found |
| 9 | [here](https://developer.android.com/studio/build/shrink-code#decode-stack-trace). |
| 10 | |
| 11 | ## Additional information appended as comments to the file |
| 12 | |
| 13 | The format for additional information is encoded as comments with json formatted |
| 14 | data. The data has an ID to disambiguate it from other information. |
| 15 | |
| 16 | ### Version |
| 17 | |
| 18 | The version information states what version the content of the mapping file is |
| 19 | using. |
| 20 | |
| 21 | The format of the version information is: |
| 22 | ``` |
| 23 | # {"id":"com.android.tools.r8.mapping","version":"1.0"} |
| 24 | ``` |
| 25 | Here `id` must be `com.android.tools.r8.mapping` and `version` is the version of |
| 26 | the mapping file. |
| 27 | |
| 28 | The version information applies to content of the file following the position of |
| 29 | the version entry until either the end of the file or another version entry is |
| 30 | present. |
| 31 | |
| 32 | If no version information is present, the content is assumed to have version |
| 33 | zero and no additional mapping information except [Source File](#source-file). |
| 34 | |
| 35 | When interpreting the mapping file, any additional mapping information |
| 36 | pertaining to a later version should be ignored. In other words, treated as a |
| 37 | normal comment. |
| 38 | |
| 39 | Retracing tools supporting this versioning scheme should issue a warning when |
| 40 | given mapping files with versions higher than the version supported by the tool. |
| 41 | |
Morten Krogh-Jespersen | 2e51529 | 2021-11-08 09:18:38 +0100 | [diff] [blame] | 42 | - Version 1.0 was introduced by R8 in version 3.1.21 |
| 43 | |
Morten Krogh-Jespersen | 00d7980 | 2021-09-06 13:55:28 +0200 | [diff] [blame] | 44 | ### Source File |
| 45 | |
| 46 | The source file information states what source file a class originated from. |
| 47 | |
| 48 | The source file information must be placed directly below the class mapping it |
| 49 | pertains to. The format of the source file information is: |
| 50 | ``` |
| 51 | some.package.Class -> foo.a: |
| 52 | # {"id":"sourceFile","fileName":"R8Test.kt"} |
| 53 | ``` |
| 54 | Here `id` must be the string `sourceFile` and `fileName` is the source file name |
| 55 | as a string value (in this example `R8Test.kt`). |
| 56 | |
| 57 | Note that the `id` of the source file information is unqualified. It is the only |
| 58 | allowed unqualified identifier as it was introduced prior to the versioning |
| 59 | scheme. |
| 60 | |
| 61 | ### Synthesized (Introduced at version 1.0) |
| 62 | |
| 63 | The synthesized information states what parts of the compiled output program are |
| 64 | synthesized by the compiler. A retracing tool should use the synthesized |
| 65 | information to strip out synthesized method frames from retraced stacks. |
| 66 | |
| 67 | The synthesized information must be placed directly below the class, field or |
| 68 | method mapping it pertains to. The format of the synthesized information is: |
| 69 | ``` |
| 70 | # {'id':'com.android.tools.r8.synthesized'} |
| 71 | ``` |
| 72 | Here `id` must be `com.android.tools.r8.synthesized`. There is no other content. |
| 73 | |
| 74 | A class mapping would be: |
| 75 | ``` |
| 76 | some.package.SomeSynthesizedClass -> x.y.z: |
| 77 | # {'id':'com.android.tools.r8.synthesized'} |
| 78 | ``` |
| 79 | This specifies that the class `x.y.z` has been synthesized by the compiler and |
| 80 | thus it did not exist in the original input program. |
| 81 | |
| 82 | Notice that the left-hand side and right-hand side, here |
| 83 | `some.package.SomeSynthesizedClass` and `x.y.z` respectively, could be any class |
| 84 | name. It is likely that the mapping for synthetics is the identity, but a useful |
| 85 | name can be placed here if possible which legacy retrace tools would use when |
| 86 | retracing. |
| 87 | |
| 88 | A field mapping would be: |
| 89 | ``` |
| 90 | some.package.Class -> x.y.z: |
| 91 | int someField -> a |
| 92 | # {'id':'com.android.tools.r8.synthesized'} |
| 93 | ``` |
| 94 | This specifies that the field `x.y.z.a` has been synthesized by the compiler. As |
| 95 | for classes, since the field is not part of the original input program, the |
| 96 | left- and right-hand side names could be anything. Note that a field can be |
| 97 | synthesized without the class being synthesized. |
| 98 | |
| 99 | A method mapping would be: |
| 100 | ``` |
| 101 | some.package.SomeSynthesizedClass -> x.y.z: |
| 102 | void someMethod() -> a |
| 103 | # {'id':'com.android.tools.r8.synthesized'} |
| 104 | ``` |
| 105 | This specifies that the method `x.y.z.a()` has been synthesized by the compiler. |
| 106 | As for classes, since the method is not part of the original input program, the |
| 107 | left- and right-hand side names could be anything. Note that a method can be |
| 108 | synthesized without the class being synthesized. |
| 109 | |
| 110 | For inline frames a mapping would be: |
| 111 | ``` |
| 112 | some.package.Class -> foo.a: |
| 113 | 4:4:void example.Foo.lambda$main$0():225 -> a |
| 114 | 4:4:void run(example.Foo):2 -> a |
| 115 | # {'id':'com.android.tools.r8.synthesized'} |
| 116 | 5:5:void example.Foo.lambda$main$1():228 -> a |
| 117 | 5:5:void run(example.Foo):4 -> a |
| 118 | # {'id':'com.android.tools.r8.synthesized'} <-- redundant |
| 119 | ``` |
| 120 | This specifies that line 4 in the method `foo.a.a` is in a method that has |
| 121 | been synthesized by the compiler. Since the method is either synthesized or not |
| 122 | any extra synthesized comments will have no effect. |
| 123 | |
| 124 | Synthesized information should never be placed on inlined frames: |
| 125 | ``` |
| 126 | some.package.Class -> foo.a: |
| 127 | 4:4:void example.Foo.syntheticThatIsInlined():225 -> a |
| 128 | # {'id':'com.android.tools.r8.synthesized'} |
| 129 | 4:4:void run(example.Foo):2 -> a |
| 130 | ``` |
| 131 | In the above, the mapping information suggests that the inline frame |
| 132 | `example.Foo.syntheticThatIsInlined` should be marked as `synthesized`. However, |
| 133 | since that method was not part of the input program it should not be in the |
| 134 | output mapping information at all. |
| 135 | |
Morten Krogh-Jespersen | 2552900 | 2021-11-09 00:05:11 +0100 | [diff] [blame] | 136 | ### RewriteFrame (Introduced at version 2.0) |
| 137 | |
| 138 | The RewriteFrame information informs the retrace tool that when retracing a |
| 139 | frame it should rewrite it. The mapping information has the form: |
| 140 | |
| 141 | ``` |
| 142 | # { id: 'com.android.tools.r8.rewriteFrame', " |
| 143 | conditions: ['throws(<exceptionDescriptor>)'], |
| 144 | actions: ['removeInnerFrames(<count>)'] } |
| 145 | ``` |
| 146 | |
| 147 | The format is to specify conditions for when the rule should be applied and then |
| 148 | describe the actions to take. The following conditions exist: |
| 149 | |
| 150 | - `throws(<exceptionDescriptor>)`: Will be true if the thrown exception above is |
| 151 | `<exceptionDescriptor>` |
| 152 | |
| 153 | Conditions can be combined by adding more items to the list. The semantics of |
| 154 | having more elements in the list is that the conditions are AND'ed together. To |
| 155 | achieve OR one should duplicate the information. |
| 156 | |
| 157 | Actions describe what should happen to the retraced frames if the condition |
| 158 | holds. Multiple specified actions will be applied from left to right. The |
| 159 | following actions exist: |
| 160 | |
| 161 | - `removeInnerFrames(<count>)`: Will remove the number of frames starting with |
| 162 | inner most frame. It is an error to specify a count higher than all frames. |
| 163 | |
| 164 | An example could be to remove an inlined frame if a null-pointer-exception is |
| 165 | thrown: |
| 166 | |
| 167 | ``` |
| 168 | some.Class -> a: |
| 169 | 4:4:void other.Class.inlinee():23:23 -> a |
| 170 | 4:4:void caller(other.Class):7 -> a\n" |
| 171 | # { id: 'com.android.tools.r8.rewriteFrame', " |
| 172 | conditions: ['throws(Ljava/lang/NullPointerException;)'], |
| 173 | actions: ['removeInnerFrames(1)'] } |
| 174 | ``` |
| 175 | |
| 176 | When retracing: |
| 177 | ``` |
| 178 | Exception in thread "main" java.lang.NullPointerException: ... |
| 179 | at a.a(:4) |
| 180 | ``` |
| 181 | |
| 182 | It will normally retrace to: |
| 183 | ``` |
| 184 | Exception in thread "main" java.lang.NullPointerException: ... |
| 185 | at other.Class.inlinee(Class.java:23) |
| 186 | at some.Class.caller(Class.java:7) |
| 187 | ``` |
| 188 | |
| 189 | Amending the last mapping with the above inline information instructs the |
| 190 | retracer to discard frames above, resulting in the retrace result: |
| 191 | ``` |
| 192 | Exception in thread "main" java.lang.NullPointerException: ... |
| 193 | at some.Class.caller(Class.java:7) |
| 194 | ``` |
| 195 | |
| 196 | The `rewriteFrame` information will only be applied if the line that is being |
| 197 | retraced is directly under the exception line. |
| 198 | |
| 199 | ### Outline (Introduced at version 2.0) |
| 200 | |
| 201 | The outline information can be used by compilers to specify that a method is an |
| 202 | outline. It has the following format: |
| 203 | |
| 204 | ``` |
| 205 | # { 'id':'com.android.tools.r8.outline' } |
| 206 | ``` |
| 207 | |
| 208 | When a retracer retraces a frame that has the outline mapping information it |
| 209 | should carry the reported position to the next frame and use the |
| 210 | `outlineCallsite` to obtain the correct position. |
| 211 | |
Morten Krogh-Jespersen | e212426 | 2023-01-10 00:21:55 +0100 | [diff] [blame] | 212 | ### Outline Call Site (Introduced at version 2.0, updated at 2.2) |
Morten Krogh-Jespersen | 2552900 | 2021-11-09 00:05:11 +0100 | [diff] [blame] | 213 | |
| 214 | A position in an outline can correspond to multiple different positions |
| 215 | depending on the context. The information can be stored in the mapping file with |
| 216 | the following format: |
| 217 | |
| 218 | ``` |
| 219 | # { 'id':'com.android.tools.r8.outlineCallsite', |
| 220 | 'positions': { |
| 221 | 'outline_pos_1': callsite_pos_1, |
| 222 | 'outline_pos_2': callsite_pos_2, |
| 223 | ... |
Morten Krogh-Jespersen | e212426 | 2023-01-10 00:21:55 +0100 | [diff] [blame] | 224 | }, |
| 225 | 'outline': 'outline':'La;a()I' |
Morten Krogh-Jespersen | 2552900 | 2021-11-09 00:05:11 +0100 | [diff] [blame] | 226 | } |
| 227 | ``` |
| 228 | |
Morten Krogh-Jespersen | e212426 | 2023-01-10 00:21:55 +0100 | [diff] [blame] | 229 | The `outline` key was added in 2.2 and should be the residual descriptor of the |
| 230 | outline. |
| 231 | |
Morten Krogh-Jespersen | 2552900 | 2021-11-09 00:05:11 +0100 | [diff] [blame] | 232 | The retracer should when seeing the `outline` information carry the line number |
| 233 | to the next frame. The position should be rewritten by using the positions map |
| 234 | before using the resulting position for further retracing. Here is an example: |
| 235 | |
| 236 | ``` |
| 237 | # { id: 'com.android.tools.r8.mapping', version: '2.0' } |
| 238 | outline.Class -> a: |
| 239 | 1:2:int outline() -> a |
| 240 | # { 'id':'com.android.tools.r8.outline' } |
| 241 | some.Class -> b: |
| 242 | 1:1:void foo.bar.Baz.qux():42:42 -> s |
| 243 | 4:4:int outlineCaller(int):98:98 -> s |
| 244 | 5:5:int outlineCaller(int):100:100 -> s |
| 245 | 27:27:int outlineCaller(int):0:0 -> s |
| 246 | # { 'id':'com.android.tools.r8.outlineCallsite', |
Morten Krogh-Jespersen | e212426 | 2023-01-10 00:21:55 +0100 | [diff] [blame] | 247 | 'positions': { '1': 4, '2': 5 }, |
| 248 | 'outline':'La;a()I'} |
Morten Krogh-Jespersen | 2552900 | 2021-11-09 00:05:11 +0100 | [diff] [blame] | 249 | ``` |
| 250 | |
| 251 | Retracing the following stack trace lines: |
| 252 | |
| 253 | ``` |
| 254 | at a.a(:1) |
| 255 | at b.s(:27) |
| 256 | ``` |
| 257 | |
| 258 | Should first retrace the first line and see it is an `outline` and then use |
| 259 | the `outlineCallsite` for `b.s` at position `27` to map the read position `1` to |
| 260 | position `4` and then use that to find the actual mapping, resulting in the |
| 261 | retraced stack: |
| 262 | |
| 263 | ``` |
| 264 | at some.Class.outlineCaller(Class.java:98) |
| 265 | ``` |
| 266 | |
| 267 | It should be such that for all stack traces, if a retracer ever see an outline |
| 268 | the next obfuscated line should contain `outlineCallSite` information. |
| 269 | |
| 270 | ### Catch all range for methods with a single unique position |
| 271 | |
| 272 | If only a single position is needed for retracing a method correctly one can |
| 273 | skip emitting the position and rely on retrace to retrace correctly. To ensure |
| 274 | compatibility R8 emits a catch-all range `0:65535` as such: |
| 275 | |
| 276 | ``` |
| 277 | 0:65535:void foo():33:33 -> a |
| 278 | ``` |
| 279 | |
| 280 | It does not matter if the mapping is an inline frame. Catch all ranges should |
Morten Krogh-Jespersen | e212426 | 2023-01-10 00:21:55 +0100 | [diff] [blame] | 281 | never be used for overloads. |
| 282 | |
| 283 | ### Residual signature (Introduced at 2.2) |
| 284 | |
| 285 | The residual signature information was added to mitigate the problem with no |
| 286 | right hand side signature in mapping files. The information should be placed |
| 287 | directly under the first occurrence of a field or method. |
| 288 | |
| 289 | ``` |
| 290 | com.bar -> a: |
| 291 | com.foo -> b: |
| 292 | com.bar m1(int) -> m2, |
| 293 | # { id: 'com.android.tools.r8.residualsignature', signature:'(Z)a' } |
| 294 | ``` |
| 295 | |
| 296 | Similar for fields: |
| 297 | ``` |
| 298 | com.bar -> a: |
| 299 | com.foo -> b: |
| 300 | com.bar f1 -> f2, |
| 301 | # { id: 'com.android.tools.r8.residualsignature', signature:'a' } |
| 302 | ``` |
| 303 | |
| 304 | If the residual definition has changed arguments or return type then the |
| 305 | signature should be emitted. The residual signature has no effect on retracing |
| 306 | stack traces but they are necessary when interacting with residual signatures |
| 307 | through the Retrace Api or for composing mapping files. |