 |
Changeset 5393
- Timestamp:
- 03/01/10 16:17:41
(2 years ago)
- Author:
- Deewiant
- Message:
Oops; undo commit to release branch.
-
Files:
-
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
| r5392 |
r5393 |
|
| 75 | 75 | version(HP_LIBUNWIND) |
|---|
| 76 | 76 | { |
|---|
| 77 | | // Haven't checked whether and how it has _Unwind_Get{Text,Data}RelBase |
|---|
| 78 | | pragma (msg, "HP_LIBUNWIND interface is out of date and untested"); |
|---|
| 79 | | |
|---|
| 80 | 77 | void __libunwind_Unwind_Resume(_Unwind_Exception *); |
|---|
| 81 | 78 | _Unwind_Reason_Code __libunwind_Unwind_RaiseException(_Unwind_Exception *); |
|---|
| … | … | |
| 98 | 95 | alias __libunwind_Unwind_GetRegionStart _Unwind_GetRegionStart; |
|---|
| 99 | 96 | } |
|---|
| 100 | | else version(X86_UNWIND) |
|---|
| | 97 | else version(X86_UNWIND) |
|---|
| 101 | 98 | { |
|---|
| 102 | 99 | void _Unwind_Resume(_Unwind_Exception*); |
|---|
| … | … | |
| 108 | 105 | ptrdiff_t new_value); |
|---|
| 109 | 106 | ptrdiff_t _Unwind_GetRegionStart(_Unwind_Context_Ptr context); |
|---|
| 110 | | |
|---|
| 111 | | size_t _Unwind_GetTextRelBase(_Unwind_Context_Ptr); |
|---|
| 112 | | size_t _Unwind_GetDataRelBase(_Unwind_Context_Ptr); |
|---|
| 113 | 107 | } |
|---|
| 114 | 108 | else |
|---|
| … | … | |
| 136 | 130 | |
|---|
| 137 | 131 | |
|---|
| 138 | | // DWARF EH encoding enum |
|---|
| 139 | | // See e.g. http://refspecs.freestandards.org/LSB_3.1.1/LSB-Core-generic/LSB-Core-generic/dwarfext.html |
|---|
| 140 | | private enum : ubyte { |
|---|
| 141 | | DW_EH_PE_omit = 0xff, // value is not present |
|---|
| 142 | | |
|---|
| 143 | | // value format |
|---|
| 144 | | DW_EH_PE_absptr = 0x00, // literal pointer |
|---|
| 145 | | DW_EH_PE_uleb128 = 0x01, |
|---|
| 146 | | DW_EH_PE_udata2 = 0x02, // unsigned 2-byte |
|---|
| 147 | | DW_EH_PE_udata4 = 0x03, |
|---|
| 148 | | DW_EH_PE_udata8 = 0x04, |
|---|
| 149 | | DW_EH_PE_sleb128 = 0x09, |
|---|
| 150 | | DW_EH_PE_sdata2 = 0x0a, |
|---|
| 151 | | DW_EH_PE_sdata4 = 0x0b, |
|---|
| 152 | | DW_EH_PE_sdata8 = 0x0c, |
|---|
| 153 | | |
|---|
| 154 | | // value meaning |
|---|
| 155 | | DW_EH_PE_pcrel = 0x10, // relative to program counter |
|---|
| 156 | | DW_EH_PE_textrel = 0x20, // relative to .text |
|---|
| 157 | | DW_EH_PE_datarel = 0x30, // relative to .got or .eh_frame_hdr |
|---|
| 158 | | DW_EH_PE_funcrel = 0x40, // relative to beginning of function |
|---|
| 159 | | DW_EH_PE_aligned = 0x50, // is an aligned void* |
|---|
| 160 | | |
|---|
| 161 | | // value is a pointer to the actual value |
|---|
| 162 | | // this is a mask on top of one of the above |
|---|
| 163 | | DW_EH_PE_indirect = 0x80 |
|---|
| 164 | | } |
|---|
| 165 | | |
|---|
| 166 | | // Helpers for reading DWARF data |
|---|
| 167 | | |
|---|
| 168 | | // Given an encoding and a context, return the base to which the encoding is |
|---|
| 169 | | // relative |
|---|
| 170 | | private size_t base_of_encoded(_Unwind_Context_Ptr context, ubyte encoding) |
|---|
| 171 | | { |
|---|
| 172 | | if (encoding == DW_EH_PE_omit) |
|---|
| 173 | | return 0; |
|---|
| 174 | | |
|---|
| 175 | | switch (encoding & 0x70) // ignore DW_EH_PE_indirect |
|---|
| 176 | | { |
|---|
| 177 | | case DW_EH_PE_absptr, DW_EH_PE_pcrel, DW_EH_PE_aligned: |
|---|
| 178 | | return 0; |
|---|
| 179 | | |
|---|
| 180 | | case DW_EH_PE_textrel: return _Unwind_GetTextRelBase(context); |
|---|
| 181 | | case DW_EH_PE_datarel: return _Unwind_GetDataRelBase(context); |
|---|
| 182 | | case DW_EH_PE_funcrel: return _Unwind_GetRegionStart(context); |
|---|
| 183 | | |
|---|
| 184 | | default: fatalerror("Unrecognized base for DWARF value"); |
|---|
| 185 | | } |
|---|
| 186 | | } |
|---|
| 187 | | |
|---|
| 188 | | // Only defined for fixed-size encodings |
|---|
| 189 | | private size_t size_of_encoded(ubyte encoding) |
|---|
| 190 | | { |
|---|
| 191 | | if (encoding == DW_EH_PE_omit) |
|---|
| 192 | | return 0; |
|---|
| 193 | | |
|---|
| 194 | | switch (encoding & 0x07) // ignore leb128 |
|---|
| 195 | | { |
|---|
| 196 | | case DW_EH_PE_absptr: return (void*).sizeof; |
|---|
| 197 | | case DW_EH_PE_udata2: return 2; |
|---|
| 198 | | case DW_EH_PE_udata4: return 4; |
|---|
| 199 | | case DW_EH_PE_udata8: return 8; |
|---|
| 200 | | |
|---|
| 201 | | default: fatalerror("Unrecognized fixed-size DWARF value encoding"); |
|---|
| 202 | | } |
|---|
| 203 | | } |
|---|
| 204 | | |
|---|
| 205 | | // Actual value readers below: read a value from the given ubyte* into the |
|---|
| 206 | | // output parameter and return the pointer incremented past the value. |
|---|
| 207 | | |
|---|
| 208 | | // Like read_encoded_with_base but gets the base from the given context |
|---|
| 209 | | private ubyte* read_encoded(_Unwind_Context_Ptr context, ubyte encoding, ubyte* p, out size_t val) |
|---|
| 210 | | { |
|---|
| 211 | | return read_encoded_with_base(encoding, base_of_encoded(context, encoding), p, val); |
|---|
| 212 | | } |
|---|
| 213 | | |
|---|
| 214 | | private ubyte* read_encoded_with_base(ubyte encoding, size_t base, ubyte* p, out size_t val) |
|---|
| 215 | | { |
|---|
| 216 | | if (encoding == DW_EH_PE_aligned) |
|---|
| 217 | | { |
|---|
| 218 | | auto a = cast(size_t)p; |
|---|
| 219 | | a = (a + (void*).sizeof - 1) & -(void*).sizeof; |
|---|
| 220 | | val = *cast(size_t*)a; |
|---|
| 221 | | return cast(ubyte*)(a + (void*).sizeof); |
|---|
| 222 | | } |
|---|
| 223 | | |
|---|
| 224 | | union U |
|---|
| 225 | | { |
|---|
| 226 | | size_t ptr; |
|---|
| 227 | | ushort udata2; |
|---|
| 228 | | uint udata4; |
|---|
| 229 | | ulong udata8; |
|---|
| 230 | | short sdata2; |
|---|
| 231 | | int sdata4; |
|---|
| 232 | | long sdata8; |
|---|
| 233 | | } |
|---|
| 234 | | |
|---|
| 235 | | auto u = cast(U*)p; |
|---|
| 236 | | |
|---|
| 237 | | size_t result; |
|---|
| 238 | | |
|---|
| 239 | | switch (encoding & 0x0f) |
|---|
| 240 | | { |
|---|
| 241 | | case DW_EH_PE_absptr: |
|---|
| 242 | | result = u.ptr; |
|---|
| 243 | | p += (void*).sizeof; |
|---|
| 244 | | break; |
|---|
| 245 | | |
|---|
| 246 | | case DW_EH_PE_uleb128: |
|---|
| 247 | | { |
|---|
| 248 | | p = get_uleb128(p, result); |
|---|
| 249 | | break; |
|---|
| 250 | | } |
|---|
| 251 | | case DW_EH_PE_sleb128: |
|---|
| 252 | | { |
|---|
| 253 | | ptrdiff_t sleb128; |
|---|
| 254 | | p = get_sleb128(p, sleb128); |
|---|
| 255 | | result = cast(size_t)sleb128; |
|---|
| 256 | | break; |
|---|
| 257 | | } |
|---|
| 258 | | |
|---|
| 259 | | case DW_EH_PE_udata2: result = cast(size_t)u.udata2; p += 2; break; |
|---|
| 260 | | case DW_EH_PE_udata4: result = cast(size_t)u.udata4; p += 4; break; |
|---|
| 261 | | case DW_EH_PE_udata8: result = cast(size_t)u.udata8; p += 8; break; |
|---|
| 262 | | case DW_EH_PE_sdata2: result = cast(size_t)u.sdata2; p += 2; break; |
|---|
| 263 | | case DW_EH_PE_sdata4: result = cast(size_t)u.sdata4; p += 4; break; |
|---|
| 264 | | case DW_EH_PE_sdata8: result = cast(size_t)u.sdata8; p += 8; break; |
|---|
| 265 | | |
|---|
| 266 | | default: fatalerror("Unrecognized DWARF value encoding format"); |
|---|
| 267 | | } |
|---|
| 268 | | if (result) |
|---|
| 269 | | { |
|---|
| 270 | | if ((encoding & 0x70) == DW_EH_PE_pcrel) |
|---|
| 271 | | result += cast(size_t)u; |
|---|
| 272 | | else |
|---|
| 273 | | result += base; |
|---|
| 274 | | |
|---|
| 275 | | if (encoding & DW_EH_PE_indirect) |
|---|
| 276 | | result = *cast(size_t*)result; |
|---|
| 277 | | } |
|---|
| 278 | | val = result; |
|---|
| 279 | | return p; |
|---|
| 280 | | } |
|---|
| 281 | | |
|---|
| | 132 | // helpers for reading certain DWARF data |
|---|
| 282 | 133 | private ubyte* get_uleb128(ubyte* addr, ref size_t res) |
|---|
| 283 | 134 | { |
|---|
| … | … | |
| 351 | 202 | { |
|---|
| 352 | 203 | |
|---|
| 353 | | // Various stuff we need |
|---|
| 354 | | struct Region |
|---|
| 355 | | { |
|---|
| 356 | | ubyte* callsite_table; |
|---|
| 357 | | ubyte* action_table; |
|---|
| 358 | | |
|---|
| 359 | | // Note: classinfo_table points past the end of the table |
|---|
| 360 | | ubyte* classinfo_table; |
|---|
| 361 | | |
|---|
| 362 | | ptrdiff_t start; |
|---|
| 363 | | size_t lpStart_base; // landing pad base |
|---|
| 364 | | |
|---|
| 365 | | ubyte ttypeEnc; |
|---|
| 366 | | size_t ttype_base; // typeinfo base |
|---|
| 367 | | |
|---|
| 368 | | ubyte callSiteEnc; |
|---|
| 369 | | } |
|---|
| 370 | | |
|---|
| 371 | 204 | // the personality routine gets called by the unwind handler and is responsible for |
|---|
| 372 | 205 | // reading the EH tables and deciding what to do |
|---|
| … | … | |
| 385 | 218 | // Note: callsite and action tables do not contain static-length |
|---|
| 386 | 219 | // data and will be parsed as needed |
|---|
| 387 | | |
|---|
| 388 | | Region region; |
|---|
| 389 | | |
|---|
| 390 | | _d_getLanguageSpecificTables(context, region); |
|---|
| 391 | | if (!region.callsite_table) |
|---|
| | 220 | // Note: classinfo_table points past the end of the table |
|---|
| | 221 | ubyte* callsite_table; |
|---|
| | 222 | ubyte* action_table; |
|---|
| | 223 | ClassInfo* classinfo_table; |
|---|
| | 224 | _d_getLanguageSpecificTables(context, callsite_table, action_table, classinfo_table); |
|---|
| | 225 | if (!callsite_table) |
|---|
| 392 | 226 | return _Unwind_Reason_Code.CONTINUE_UNWIND; |
|---|
| 393 | 227 | |
|---|
| … | … | |
| 396 | 230 | the callsite_table |
|---|
| 397 | 231 | */ |
|---|
| 398 | | ubyte* callsite_walker = region.callsite_table; |
|---|
| | 232 | ubyte* callsite_walker = callsite_table; |
|---|
| 399 | 233 | |
|---|
| 400 | 234 | // get the instruction pointer |
|---|
| … | … | |
| 403 | 237 | ptrdiff_t ip = _Unwind_GetIP(context) - 1; |
|---|
| 404 | 238 | |
|---|
| | 239 | // address block_start is relative to |
|---|
| | 240 | ptrdiff_t region_start = _Unwind_GetRegionStart(context); |
|---|
| | 241 | |
|---|
| 405 | 242 | // table entries |
|---|
| 406 | | size_t landing_pad; |
|---|
| | 243 | uint block_start_offset, block_size; |
|---|
| | 244 | ptrdiff_t landing_pad; |
|---|
| 407 | 245 | size_t action_offset; |
|---|
| 408 | 246 | |
|---|
| 409 | 247 | while(true) { |
|---|
| 410 | 248 | // if we've gone through the list and found nothing... |
|---|
| 411 | | if(callsite_walker >= region.action_table) |
|---|
| | 249 | if(callsite_walker >= action_table) |
|---|
| 412 | 250 | return _Unwind_Reason_Code.CONTINUE_UNWIND; |
|---|
| 413 | 251 | |
|---|
| 414 | | size_t block_start, block_size; |
|---|
| 415 | | |
|---|
| 416 | | callsite_walker = read_encoded(null, region.callSiteEnc, callsite_walker, block_start); |
|---|
| 417 | | callsite_walker = read_encoded(null, region.callSiteEnc, callsite_walker, block_size); |
|---|
| 418 | | callsite_walker = read_encoded(null, region.callSiteEnc, callsite_walker, landing_pad); |
|---|
| 419 | | callsite_walker = get_uleb128(callsite_walker, action_offset); |
|---|
| 420 | | |
|---|
| 421 | | debug(EH_personality_verbose) printf("ip=%zx %d %d %zx\n", ip, block_start, block_size, landing_pad); |
|---|
| | 252 | block_start_offset = *cast(uint*)callsite_walker; |
|---|
| | 253 | block_size = *(cast(uint*)callsite_walker + 1); |
|---|
| | 254 | landing_pad = *(cast(uint*)callsite_walker + 2); |
|---|
| | 255 | if(landing_pad) |
|---|
| | 256 | landing_pad += region_start; |
|---|
| | 257 | callsite_walker = get_uleb128(callsite_walker + 3*uint.sizeof, action_offset); |
|---|
| | 258 | |
|---|
| | 259 | debug(EH_personality_verbose) printf("ip=%llx %d %d %llx\n", ip, block_start_offset, block_size, landing_pad); |
|---|
| 422 | 260 | |
|---|
| 423 | 261 | // since the list is sorted, as soon as we're past the ip |
|---|
| 424 | 262 | // there's no handler to be found |
|---|
| 425 | | if(ip < region.start + block_start) |
|---|
| | 263 | if(ip < region_start + block_start_offset) |
|---|
| 426 | 264 | return _Unwind_Reason_Code.CONTINUE_UNWIND; |
|---|
| 427 | 265 | |
|---|
| 428 | | if(landing_pad) |
|---|
| 429 | | landing_pad += region.lpStart_base; |
|---|
| 430 | | |
|---|
| 431 | 266 | // if we've found our block, exit |
|---|
| 432 | | if(ip < region.start + block_start + block_size) |
|---|
| | 267 | if(ip < region_start + block_start_offset + block_size) |
|---|
| 433 | 268 | break; |
|---|
| 434 | 269 | } |
|---|
| 435 | 270 | |
|---|
| 436 | | debug(EH_personality) printf("Found correct landing pad %zx and actionOffset %zx\n", landing_pad, action_offset); |
|---|
| | 271 | debug(EH_personality) printf("Found correct landing pad and actionOffset %d\n", action_offset); |
|---|
| 437 | 272 | |
|---|
| 438 | 273 | // now we need the exception's classinfo to find a handler |
|---|
| … | … | |
| 447 | 282 | // if there's no action offset but a landing pad, this is a cleanup handler |
|---|
| 448 | 283 | else if(!action_offset && landing_pad) |
|---|
| 449 | | return _d_eh_install_finally_context(actions, cast(ptrdiff_t)landing_pad, exception_struct, context); |
|---|
| | 284 | return _d_eh_install_finally_context(actions, landing_pad, exception_struct, context); |
|---|
| 450 | 285 | |
|---|
| 451 | 286 | /* |
|---|
| 452 | 287 | walk action table chain, comparing classinfos using _d_isbaseof |
|---|
| 453 | 288 | */ |
|---|
| 454 | | ubyte* action_walker = region.action_table + action_offset - 1; |
|---|
| 455 | | |
|---|
| | 289 | ubyte* action_walker = action_table + action_offset - 1; |
|---|
| | 290 | |
|---|
| | 291 | ptrdiff_t ti_offset, next_action_offset; |
|---|
| 456 | 292 | while(true) { |
|---|
| 457 | | ptrdiff_t ti_offset, next_action_offset; |
|---|
| 458 | | |
|---|
| 459 | 293 | action_walker = get_sleb128(action_walker, ti_offset); |
|---|
| 460 | 294 | // it is intentional that we not modify action_walker here |
|---|
| … | … | |
| 463 | 297 | |
|---|
| 464 | 298 | // negative are 'filters' which we don't use |
|---|
| 465 | | if(ti_offset < 0) |
|---|
| | 299 | if(!(ti_offset >= 0)) |
|---|
| 466 | 300 | fatalerror("Filter actions are unsupported"); |
|---|
| 467 | 301 | |
|---|
| 468 | 302 | // zero means cleanup, which we require to be the last action |
|---|
| 469 | 303 | if(ti_offset == 0) { |
|---|
| 470 | | if(next_action_offset != 0) |
|---|
| | 304 | if(!(next_action_offset == 0)) |
|---|
| 471 | 305 | fatalerror("Cleanup action must be last in chain"); |
|---|
| 472 | | return _d_eh_install_finally_context(actions, cast(ptrdiff_t)landing_pad, exception_struct, context); |
|---|
| | 306 | return _d_eh_install_finally_context(actions, landing_pad, exception_struct, context); |
|---|
| 473 | 307 | } |
|---|
| 474 | 308 | |
|---|
| 475 | 309 | // get classinfo for action and check if the one in the |
|---|
| 476 | 310 | // exception structure is a base |
|---|
| 477 | | size_t typeinfo; |
|---|
| 478 | | auto filter = ti_offset * size_of_encoded(region.ttypeEnc); |
|---|
| 479 | | read_encoded_with_base(region.ttypeEnc, region.ttype_base, region.classinfo_table - filter, typeinfo); |
|---|
| 480 | | |
|---|
| 481 | | debug(EH_personality_verbose) |
|---|
| 482 | | printf("classinfo at %zx (enc %zx (size %zx) base %zx ptr %zx)\n", typeinfo, region.ttypeEnc, size_of_encoded(region.ttypeEnc), region.ttype_base, region.classinfo_table - filter); |
|---|
| 483 | | |
|---|
| 484 | | auto catch_ci = *cast(ClassInfo*)&typeinfo; |
|---|
| 485 | | |
|---|
| | 311 | ClassInfo catch_ci = *(classinfo_table - ti_offset); |
|---|
| 486 | 312 | debug(EH_personality) printf("Comparing catch %s to exception %s\n", catch_ci.name.ptr, exception_struct.exception_object.classinfo.name.ptr); |
|---|
| 487 | 313 | if(_d_isbaseof(exception_struct.exception_object.classinfo, catch_ci)) |
|---|
| 488 | | return _d_eh_install_catch_context(actions, ti_offset, cast(ptrdiff_t)landing_pad, exception_struct, context); |
|---|
| | 314 | return _d_eh_install_catch_context(actions, ti_offset, landing_pad, exception_struct, context); |
|---|
| 489 | 315 | |
|---|
| 490 | 316 | // we've walked through all actions and found nothing... |
|---|
| … | … | |
| 548 | 374 | } |
|---|
| 549 | 375 | |
|---|
| 550 | | private void _d_getLanguageSpecificTables(_Unwind_Context_Ptr context, out Region region) |
|---|
| 551 | | { |
|---|
| 552 | | auto data = cast(ubyte*)_Unwind_GetLanguageSpecificData(context); |
|---|
| | 376 | private void _d_getLanguageSpecificTables(_Unwind_Context_Ptr context, ref ubyte* callsite, ref ubyte* action, ref ClassInfo* ci) |
|---|
| | 377 | { |
|---|
| | 378 | ubyte* data = cast(ubyte*)_Unwind_GetLanguageSpecificData(context); |
|---|
| 553 | 379 | if (!data) |
|---|
| | 380 | { |
|---|
| | 381 | callsite = null; |
|---|
| | 382 | action = null; |
|---|
| | 383 | ci = null; |
|---|
| 554 | 384 | return; |
|---|
| 555 | | |
|---|
| 556 | | region.start = _Unwind_GetRegionStart(context); |
|---|
| 557 | | |
|---|
| 558 | | // Read the C++-style LSDA: this is implementation-defined by GCC but LLVM |
|---|
| 559 | | // outputs the same kind of table |
|---|
| 560 | | |
|---|
| 561 | | // Get @LPStart: landing pad offsets are relative to it |
|---|
| 562 | | auto lpStartEnc = *data++; |
|---|
| 563 | | if (lpStartEnc == DW_EH_PE_omit) |
|---|
| 564 | | region.lpStart_base = region.start; |
|---|
| 565 | | else |
|---|
| 566 | | data = read_encoded(context, lpStartEnc, data, region.lpStart_base); |
|---|
| 567 | | |
|---|
| 568 | | // Get @TType: the offset to the handler and typeinfo |
|---|
| 569 | | region.ttypeEnc = *data++; |
|---|
| 570 | | if (region.ttypeEnc == DW_EH_PE_omit) |
|---|
| 571 | | // Not sure about this one... |
|---|
| 572 | | fatalerror("@TType must not be omitted from DWARF header"); |
|---|
| 573 | | |
|---|
| 574 | | size_t ciOffset; |
|---|
| 575 | | data = get_uleb128(data, ciOffset); |
|---|
| 576 | | region.classinfo_table = data + ciOffset; |
|---|
| 577 | | |
|---|
| 578 | | region.ttype_base = base_of_encoded(context, region.ttypeEnc); |
|---|
| 579 | | |
|---|
| 580 | | // Get encoding and length of the call site table, which precedes the action |
|---|
| 581 | | // table. |
|---|
| 582 | | region.callSiteEnc = *data++; |
|---|
| 583 | | if (region.callSiteEnc == DW_EH_PE_omit) |
|---|
| 584 | | fatalerror("Call site table encoding must not be omitted from DWARF header"); |
|---|
| 585 | | |
|---|
| 586 | | size_t callSiteLength; |
|---|
| 587 | | region.callsite_table = get_uleb128(data, callSiteLength); |
|---|
| 588 | | region.action_table = region.callsite_table + callSiteLength; |
|---|
| | 385 | } |
|---|
| | 386 | |
|---|
| | 387 | //TODO: Do proper DWARF reading here |
|---|
| | 388 | if(*data++ != 0xff) |
|---|
| | 389 | fatalerror("DWARF header has unexpected format 1"); |
|---|
| | 390 | |
|---|
| | 391 | if(*data++ != 0x00) |
|---|
| | 392 | fatalerror("DWARF header has unexpected format 2"); |
|---|
| | 393 | size_t cioffset; |
|---|
| | 394 | data = get_uleb128(data, cioffset); |
|---|
| | 395 | ci = cast(ClassInfo*)(data + cioffset); |
|---|
| | 396 | |
|---|
| | 397 | if(*data++ != 0x03) |
|---|
| | 398 | fatalerror("DWARF header has unexpected format 3"); |
|---|
| | 399 | size_t callsitelength; |
|---|
| | 400 | data = get_uleb128(data, callsitelength); |
|---|
| | 401 | action = data + callsitelength; |
|---|
| | 402 | |
|---|
| | 403 | callsite = data; |
|---|
| 589 | 404 | } |
|---|
| 590 | 405 | |
|---|
Download in other formats:
|
 |