1 /** 2 * This module implements the runtime-part of LDC exceptions 3 * on Windows, based on the MSVC++ runtime. 4 */ 5 module ldc.eh_msvc; 6 7 version (CRuntime_Microsoft): 8 9 import core.sys.windows.windows; 10 import core.exception : onOutOfMemoryError, OutOfMemoryError; 11 import core.internal.container.common : xmalloc; 12 import core.stdc.stdlib : malloc, free, abort; 13 import core.stdc.string : memcpy; 14 import ldc.attributes; 15 import ldc.llvmasm; 16 17 // pointers are image relative for Win64 versions 18 version (Win64) 19 struct ImgPtr(T) { uint offset; } // offset into image 20 else 21 alias ImgPtr(T) = T*; 22 23 alias PMFN = ImgPtr!(void function(void*)); 24 25 struct TypeDescriptor 26 { 27 version (_RTTI) 28 const void * pVFTable; // Field overloaded by RTTI 29 else 30 uint hash; // Hash value computed from type's decorated name 31 32 void * spare; // reserved, possible for RTTI 33 char[1] name; // variable size, zero terminated 34 } 35 36 struct PMD 37 { 38 int mdisp; // Offset of intended data within base 39 int pdisp; // Displacement to virtual base pointer 40 int vdisp; // Index within vbTable to offset of base 41 } 42 43 struct CatchableType 44 { 45 uint properties; // Catchable Type properties (Bit field) 46 ImgPtr!TypeDescriptor pType; // Pointer to TypeDescriptor 47 PMD thisDisplacement; // Pointer to instance of catch type within thrown object. 48 int sizeOrOffset; // Size of simple-type object or offset into buffer of 'this' pointer for catch object 49 PMFN copyFunction; // Copy constructor or CC-closure 50 } 51 52 enum CT_IsSimpleType = 0x00000001; // type is a simple type (includes pointers) 53 enum CT_ByReferenceOnly = 0x00000002; // type must be caught by reference 54 enum CT_HasVirtualBase = 0x00000004; // type is a class with virtual bases 55 enum CT_IsWinRTHandle = 0x00000008; // type is a winrt handle 56 enum CT_IsStdBadAlloc = 0x00000010; // type is a a std::bad_alloc 57 58 struct CatchableTypeArray 59 { 60 int nCatchableTypes; 61 ImgPtr!CatchableType[1] arrayOfCatchableTypes; // variable size 62 } 63 64 struct _ThrowInfo 65 { 66 uint attributes; // Throw Info attributes (Bit field) 67 PMFN pmfnUnwind; // Destructor to call when exception has been handled or aborted. 68 PMFN pForwardCompat; // pointer to Forward compatibility frame handler 69 ImgPtr!CatchableTypeArray pCatchableTypeArray; // pointer to CatchableTypeArray 70 } 71 72 enum TI_IsConst = 0x00000001; // thrown object has const qualifier 73 enum TI_IsVolatile = 0x00000002; // thrown object has volatile qualifier 74 enum TI_IsUnaligned = 0x00000004; // thrown object has unaligned qualifier 75 enum TI_IsPure = 0x00000008; // object thrown from a pure module 76 enum TI_IsWinRT = 0x00000010; // object thrown is a WinRT Exception 77 78 extern(Windows) void RaiseException(DWORD dwExceptionCode, 79 DWORD dwExceptionFlags, 80 DWORD nNumberOfArguments, 81 ULONG_PTR* lpArguments); 82 83 enum int STATUS_MSC_EXCEPTION = 0xe0000000 | ('m' << 16) | ('s' << 8) | ('c' << 0); 84 85 enum EXCEPTION_NONCONTINUABLE = 0x01; 86 enum EXCEPTION_UNWINDING = 0x02; 87 88 enum EH_MAGIC_NUMBER1 = 0x19930520; 89 90 struct CxxExceptionInfo 91 { 92 size_t Magic; 93 Throwable* pThrowable; // null for rethrow 94 _ThrowInfo* ThrowInfo; 95 version (Win64) void* ImgBase; 96 } 97 98 // D runtime function 99 extern(C) int _d_isbaseof(ClassInfo oc, ClassInfo c); 100 101 // error and exit 102 extern(C) void fatalerror(const(char)* format, ...) @system 103 { 104 import core.stdc.stdarg; 105 import core.stdc.stdio; 106 107 va_list args; 108 va_start(args, format); 109 fprintf(stderr, "Fatal error in EH code: "); 110 vfprintf(stderr, format, args); 111 fprintf(stderr, "\n"); 112 abort(); 113 } 114 115 extern(C) void _d_createTrace(Throwable t, void* context); 116 117 extern(C) void _d_throw_exception(Throwable throwable) 118 { 119 if (throwable is null) 120 fatalerror("Cannot throw null exception"); 121 auto ti = typeid(throwable); 122 if (ti is null) 123 fatalerror("Cannot throw corrupt exception object with null classinfo"); 124 125 /* Increment reference count if `o` is a refcounted Throwable 126 */ 127 auto refcount = throwable.refcount(); 128 if (refcount) // non-zero means it's refcounted 129 throwable.refcount() = refcount + 1; 130 131 if (exceptionStack.length > 0) 132 { 133 // we expect that the terminate handler will be called, so hook 134 // it to avoid it actually terminating 135 if (!old_terminate_handler) 136 old_terminate_handler = set_terminate(&msvc_eh_terminate); 137 } 138 139 exceptionStack.push(throwable); 140 141 _d_createTrace(throwable, null); 142 143 CxxExceptionInfo info; 144 info.Magic = EH_MAGIC_NUMBER1; 145 info.pThrowable = &throwable; 146 info.ThrowInfo = getThrowInfo(ti).toPointer; 147 version (Win64) info.ImgBase = ehHeap.base; 148 149 RaiseException(STATUS_MSC_EXCEPTION, EXCEPTION_NONCONTINUABLE, 150 info.sizeof / size_t.sizeof, cast(ULONG_PTR*)&info); 151 } 152 153 /////////////////////////////////////////////////////////////// 154 155 import core.internal.container.hashtab; 156 import core.sync.mutex; 157 158 __gshared HashTab!(TypeInfo_Class, ImgPtr!_ThrowInfo) throwInfoHashtab; 159 __gshared HashTab!(TypeInfo_Class, ImgPtr!CatchableType) catchableHashtab; 160 __gshared Mutex throwInfoMutex; 161 162 // create and cache throwinfo for ti 163 ImgPtr!_ThrowInfo getThrowInfo(TypeInfo_Class ti) @system 164 { 165 throwInfoMutex.lock(); 166 if (auto p = ti in throwInfoHashtab) 167 { 168 throwInfoMutex.unlock(); 169 return *p; 170 } 171 172 int classes = 0; 173 for (TypeInfo_Class tic = ti; tic; tic = tic.base) 174 classes++; 175 176 size_t sz = int.sizeof + classes * ImgPtr!(CatchableType).sizeof; 177 ImgPtr!CatchableTypeArray cta = eh_malloc!CatchableTypeArray(sz); 178 toPointer(cta).nCatchableTypes = classes; 179 180 size_t c = 0; 181 for (TypeInfo_Class tic = ti; tic; tic = tic.base) 182 cta.toPointer.arrayOfCatchableTypes.ptr[c++] = getCatchableType(tic); 183 184 auto tinf = eh_malloc!_ThrowInfo(); 185 *(tinf.toPointer) = _ThrowInfo(0, PMFN(), PMFN(), cta); 186 throwInfoHashtab[ti] = tinf; 187 throwInfoMutex.unlock(); 188 return tinf; 189 } 190 191 ImgPtr!CatchableType getCatchableType(TypeInfo_Class ti) @system 192 { 193 if (auto p = ti in catchableHashtab) 194 return *p; 195 196 const sz = TypeDescriptor.sizeof + ti.name.length + 1; 197 auto td = eh_malloc!TypeDescriptor(sz); 198 auto ptd = td.toPointer; 199 200 ptd.hash = 0; 201 ptd.spare = null; 202 ptd.name.ptr[0] = 'D'; 203 memcpy(ptd.name.ptr + 1, ti.name.ptr, ti.name.length); 204 ptd.name.ptr[ti.name.length + 1] = 0; 205 206 auto ct = eh_malloc!CatchableType(); 207 ct.toPointer[0] = CatchableType(CT_IsSimpleType, td, PMD(0, -1, 0), size_t.sizeof, PMFN()); 208 catchableHashtab[ti] = ct; 209 return ct; 210 } 211 212 /////////////////////////////////////////////////////////////// 213 extern(C) Throwable _d_eh_enter_catch(void* ptr, ClassInfo catchType) 214 { 215 assert(ptr); 216 217 // is this a thrown D exception? 218 auto e = *(cast(Throwable*) ptr); 219 size_t pos = exceptionStack.find(e); 220 if (pos >= exceptionStack.length()) 221 return null; 222 223 auto caught = e; 224 // append inner unhandled thrown exceptions 225 for (size_t p = pos + 1; p < exceptionStack.length(); p++) 226 e = chainExceptions(e, exceptionStack[p]); 227 exceptionStack.shrink(pos); 228 229 // given the bad semantics of Errors, we are fine with passing 230 // the test suite with slightly inaccurate behaviour by just 231 // rethrowing a collateral Error here, though it might need to 232 // be caught by a catch handler in an inner scope 233 if (e !is caught) 234 { 235 if (_d_isbaseof(typeid(e), catchType)) 236 *cast(Throwable*) ptr = e; // the current catch can also catch this Error 237 else 238 _d_throw_exception(e); 239 } 240 return e; 241 } 242 243 Throwable chainExceptions(Throwable e, Throwable t) 244 { 245 if (!cast(Error) e) 246 if (auto err = cast(Error) t) 247 { 248 err.bypassedException = e; 249 return err; 250 } 251 252 return Throwable.chainTogether(e, t); 253 } 254 255 ExceptionStack exceptionStack; 256 257 static ~this() 258 { 259 // destructors not automatically run on globals 260 exceptionStack.destroy(); 261 } 262 263 struct ExceptionStack 264 { 265 nothrow: 266 ~this() 267 { 268 if (_p) 269 free(_p); 270 } 271 272 void push(Throwable e) @system 273 { 274 if (_length == _cap) 275 grow(); 276 _p[_length++] = e; 277 } 278 279 Throwable pop() @system 280 { 281 return _p[--_length]; 282 } 283 284 void shrink(size_t sz) @system 285 { 286 while (_length > sz) 287 _p[--_length] = null; 288 } 289 290 ref inout(Throwable) opIndex(size_t idx) inout @system 291 { 292 return _p[idx]; 293 } 294 295 size_t find(Throwable e) 296 { 297 for (size_t i = _length; i > 0; ) 298 if (exceptionStack[--i] is e) 299 return i; 300 return ~0; 301 } 302 303 @property size_t length() const { return _length; } 304 @property bool empty() const { return !length; } 305 306 void swap(ref ExceptionStack other) 307 { 308 static void swapField(T)(ref T a, ref T b) { T o = b; b = a; a = o; } 309 swapField(_length, other._length); 310 swapField(_p, other._p); 311 swapField(_cap, other._cap); 312 } 313 314 private: 315 void grow() 316 { 317 // alloc from GC? add array as a GC range? 318 immutable ncap = _cap ? 2 * _cap : 64; 319 auto p = cast(Throwable*)xmalloc(ncap * Throwable.sizeof); 320 p[0 .. _length] = _p[0 .. _length]; 321 free(_p); 322 _p = p; 323 _cap = ncap; 324 } 325 326 size_t _length; 327 Throwable* _p; 328 size_t _cap; 329 } 330 331 /////////////////////////////////////////////////////////////// 332 alias terminate_handler = void function(); 333 extern(C) terminate_handler set_terminate(terminate_handler new_handler); 334 terminate_handler old_terminate_handler; // explicitely per thread 335 336 // helper to access TLS from naked asm 337 size_t tlsUncaughtExceptions() nothrow @assumeUsed 338 { 339 return exceptionStack.length; 340 } 341 342 auto tlsOldTerminateHandler() nothrow @assumeUsed 343 { 344 return old_terminate_handler; 345 } 346 347 void msvc_eh_terminate() nothrow @naked 348 { 349 version (Win32) 350 { 351 __asm( 352 `call __D3ldc7eh_msvc21tlsUncaughtExceptionsFNbZk 353 cmp $$1, %eax 354 jle L_term 355 356 // hacking into the call chain to return EXCEPTION_EXECUTE_HANDLER 357 // as the return value of __FrameUnwindFilter so that 358 // __FrameUnwindToState continues with the next unwind block 359 360 // undo one level of exception frames from terminate() 361 mov %fs:(0), %eax 362 mov (%eax), %eax 363 mov %eax, %fs:(0) 364 365 // assume standard stack frames for callers 366 mov %ebp, %eax // frame pointer of terminate() 367 mov (%eax), %eax // frame pointer of __FrameUnwindFilter 368 mov %eax, %esp // restore stack 369 pop %ebp // and frame pointer 370 mov $$1, %eax // return EXCEPTION_EXECUTE_HANDLER 371 ret 372 373 L_term: 374 call __D3ldc7eh_msvc22tlsOldTerminateHandlerFNbNiNfZPFZv 375 cmp $$0, %eax 376 je L_ret 377 jmp *%eax 378 L_ret: 379 ret`, 380 "~{memory},~{flags},~{ebp},~{esp},~{eax}" 381 ); 382 } 383 else 384 { 385 __asm( 386 `push %rbx // align stack for better debuggability 387 call _D3ldc7eh_msvc21tlsUncaughtExceptionsFNbZm 388 cmp $$1, %rax 389 jle L_term 390 391 // update stack and IP so we just continue in __FrameUnwindHandler 392 // NOTE: these checks can fail if you have breakpoints set at 393 // the respective code locations 394 mov 8(%rsp), %rax // get return address 395 cmpb $$0xEB, (%rax) // jmp? 396 jne noJump 397 movsbq 1(%rax), %rdx // follow jmp 398 lea 2(%rax,%rdx), %rax 399 noJump: 400 cmpb $$0xE8, (%rax) // call abort? 401 jne L_term 402 add $$5, %rax 403 mov (%rax), %edx 404 mov $$0xFFFFFF, %rbx 405 and %rbx, %rdx 406 cmp $$0xC48348, %rdx // add ESP,nn (debug UCRT libs) 407 je L_addESP_found 408 cmp $$0x90, %dl // nop; (release libs) 409 jne L_term 410 411 L_release_ucrt: 412 mov 8(%rsp), %rdx 413 cmpw $$0xD3FF, -2(%rdx) // call ebx? 414 sete %bl // if not, it's UCRT 10.0.14393.0 415 movzbq %bl, %rbx 416 mov $$0x28, %rdx // release build of vcruntimelib 417 jmp L_retTerminate 418 419 L_addESP_found: 420 xor %rbx, %rbx // debug version: RBX not pushed inside terminate() 421 movzbq 3(%rax), %rdx // read nn 422 423 cmpb $$0xC3, 4(%rax) // ret? 424 jne L_term 425 426 L_retTerminate: 427 lea 0x10(%rsp,%rdx), %rdx // RSP before returning from terminate() 428 429 mov (%rdx), %rax // return address inside __FrameUnwindHandler 430 431 or %rbx, %rdx // RDX aligned, save RBX == 0 for UCRT 10.0.14393.0, 1 otherwise 432 433 cmpb $$0xEB, -19(%rax) // skip back to default jump inside "switch" (libvcruntimed.lib) 434 je L_switchFound 435 436 cmpb $$0xEB, -20(%rax) // skip back to default jump inside "switch" (vcruntime140d.dll) 437 je L_switchFound2 438 439 mov $$0xC48348C0333048FF, %rbx // dec [rax+30h]; xor eax,eax; add rsp,nn (libvcruntime.lib) 440 cmp -0x18(%rax), %rbx 441 je L_retFound 442 443 cmp 0x29(%rax), %rbx // dec [rax+30h]; xor eax,eax; add rsp,nn (vcruntime140.dll) 444 je L_retVC14_11 445 446 cmp 0x11(%rax), %rbx // dec [rax+30h]; xor eax,eax; add rsp,nn (vcruntime140.dll, 14.16.x.x) 447 je L_retVC14_16 448 449 cmp 0x1B(%rax), %rbx // dec [rax+30h]; xor eax,eax; add rsp,nn (vcruntime140.dll 14.14.x.y) 450 je L_retVC14_14 451 452 mov $$0x30245C8B483048FF, %rbx // dec [rax+30h]; mov rbx,qword ptr [rsp+30h] 453 cmp -0x2b(%rax), %rbx // (libcmt.lib, 14.23.x.x) 454 je L_retVC14_23_libcmt 455 cmp 0x11(%rax), %rbx // (vcruntime140.lib, 14.23.x.x) 456 je L_retVC14_23_msvcrt 457 458 mov $$0xccc348c48348c033, %rbx // xor eax,eax; add rsp,48h; ret; int 3 459 cmp 0x2d(%rax), %rbx // (libcmtd.lib, 14.23.x.x) 460 je L_retVC14_23_libcmtd 461 462 jmp L_term 463 464 L_retVC14_23_msvcrt: // vcruntime140.dll 14.23.28105 465 lea 0x1b(%rax), %rax 466 mov 0x38(%rdx), %rbx // restore RBX from stack 467 jmp L_rbxRestored 468 469 L_retVC14_23_libcmt: // libcmt.lib 14.23.28105 470 lea -0x21(%rax), %rax 471 mov 0x38(%rdx), %rbx // restore RBX from stack 472 jmp L_rbxRestored 473 474 L_retVC14_23_libcmtd: // libcmtd.lib/vcruntime140d.dll 14.23.28105 475 lea 0x2f(%rax), %rax 476 jmp L_rbxRestored // rbx not saved 477 478 L_retVC14_14: // (vcruntime140.dll 14.14.x.y) 479 lea 0x20(%rax), %rax 480 jmp L_retContinue 481 L_retVC14_16: // vcruntime140 14.16.27012.6 482 lea 0x16(%rax), %rax 483 jmp L_retContinue 484 L_retVC14_11: // vcruntime140 14.11.25415.0 or earlier 485 lea 0x2E(%rax), %rax 486 L_retContinue: // vcruntime140 14.00.23026.0 or later? 487 cmpw $$0x8348, (%rax) // add rsp,nn? 488 je L_xorSkipped 489 490 inc %rax // vcruntime140 earlier than 14.00.23026.0? 491 jmp L_xorSkipped 492 493 L_retFound: 494 lea -19(%rax), %rax 495 jmp L_xorSkipped 496 497 L_switchFound2: 498 dec %rax 499 L_switchFound: 500 movsbq -18(%rax), %rbx // follow jump 501 lea -17(%rax,%rbx), %rax 502 503 cmpw $$0xC033, (%rax) // xor EAX,EAX? 504 jne L_term 505 506 add $$2, %rax 507 L_xorSkipped: 508 mov %rdx, %rbx // extract UCRT marker from EDX 509 and $$~1, %rdx 510 and $$1, %rbx 511 512 cmovnz -8(%rdx), %rbx // restore RBX (pushed inside terminate()) 513 cmovz (%rsp), %rbx // RBX not changed in terminate inside UCRT 10.0.14393.0 514 515 L_rbxRestored: 516 lea 8(%rdx), %rsp 517 push %rax // new return after setting return value in __frameUnwindHandler 518 519 call __processing_throw 520 movq $$1, (%rax) 521 522 //add $$0x68, %rsp // TODO: needs to be verified for different CRT builds 523 mov $$1, %rax // return EXCEPTION_EXECUTE_HANDLER 524 ret 525 526 L_term: 527 call _D3ldc7eh_msvc22tlsOldTerminateHandlerFNbNiNfZPFZv 528 pop %rbx 529 cmp $$0, %rax 530 je L_ret 531 jmp *%rax 532 L_ret: 533 ret`, 534 "~{memory},~{flags},~{rbp},~{rsp},~{rax},~{rbx},~{rdx}" 535 ); 536 } 537 } 538 539 /////////////////////////////////////////////////////////////// 540 extern(C) void** __current_exception() nothrow; 541 extern(C) void** __current_exception_context() nothrow; 542 extern(C) int* __processing_throw() nothrow; 543 544 struct FiberContext 545 { 546 ExceptionStack exceptionStack; 547 void* currentException; 548 void* currentExceptionContext; 549 int processingContext; 550 } 551 552 FiberContext* fiberContext; 553 554 extern(C) void* _d_eh_swapContext(FiberContext* newContext) nothrow 555 { 556 import core.stdc.string : memset; 557 if (!fiberContext) 558 { 559 fiberContext = cast(FiberContext*) xmalloc(FiberContext.sizeof); 560 memset(fiberContext, 0, FiberContext.sizeof); 561 } 562 fiberContext.exceptionStack.swap(exceptionStack); 563 fiberContext.currentException = *__current_exception(); 564 fiberContext.currentExceptionContext = *__current_exception_context(); 565 fiberContext.processingContext = *__processing_throw(); 566 567 if (newContext) 568 { 569 exceptionStack.swap(newContext.exceptionStack); 570 *__current_exception() = newContext.currentException; 571 *__current_exception_context() = newContext.currentExceptionContext; 572 *__processing_throw() = newContext.processingContext; 573 } 574 else 575 { 576 exceptionStack = ExceptionStack(); 577 *__current_exception() = null; 578 *__current_exception_context() = null; 579 *__processing_throw() = 0; 580 } 581 582 FiberContext* old = fiberContext; 583 fiberContext = newContext; 584 return old; 585 } 586 587 static ~this() 588 { 589 import core.stdc.stdlib : free; 590 if (fiberContext) 591 { 592 destroy(*fiberContext); 593 free(fiberContext); 594 } 595 } 596 597 /////////////////////////////////////////////////////////////// 598 extern(C) bool _d_enter_cleanup(void* ptr) 599 { 600 // currently just used to avoid that a cleanup handler that can 601 // be inferred to not return, is removed by the LLVM optimizer 602 // 603 // TODO: setup an exception handler here (ptr passes the address 604 // of a 40 byte stack area in a parent fuction scope) to deal with 605 // unhandled exceptions during unwinding. 606 return true; 607 } 608 609 extern(C) void _d_leave_cleanup(void* ptr) 610 { 611 } 612 613 /////////////////////////////////////////////////////////////// 614 void msvc_eh_init() 615 { 616 throwInfoMutex = new Mutex; 617 618 version (Win64) ehHeap.initialize(0x10000); 619 620 // preallocate type descriptors likely to be needed 621 getThrowInfo(typeid(Exception)); 622 // better not have to allocate when this is thrown: 623 getThrowInfo(typeid(OutOfMemoryError)); 624 } 625 626 /////////////////////////////////////////////////////////////// 627 version (Win32) 628 { 629 ImgPtr!T eh_malloc(T)(size_t size = T.sizeof) 630 { 631 return cast(T*) xmalloc(size); 632 } 633 634 T* toPointer(T)(T* imgPtr) 635 { 636 return imgPtr; 637 } 638 } 639 else 640 { 641 /** 642 * Heap dedicated for CatchableTypeArray/CatchableType/TypeDescriptor 643 * structs of cached _ThrowInfos. 644 * The heap is used to keep these structs tightly together, as they are 645 * referenced via 32-bit offsets from a common base. We simply use the 646 * heap's start as base (instead of the actual image base), and malloc() 647 * returns an offset. 648 * The allocated structs are all cached and never released, so this heap 649 * can only grow. The offsets remain constant after a grow, so it's only 650 * the base which may change. 651 */ 652 struct EHHeap 653 { 654 void* base; 655 size_t capacity; 656 size_t length; 657 658 void initialize(size_t initialCapacity) 659 { 660 base = xmalloc(initialCapacity); 661 capacity = initialCapacity; 662 length = size_t.sizeof; // don't use offset 0, it has a special meaning 663 } 664 665 size_t malloc(size_t size) 666 { 667 auto offset = length; 668 enum alignmentMask = size_t.sizeof - 1; 669 auto newLength = (length + size + alignmentMask) & ~alignmentMask; 670 auto newCapacity = capacity; 671 while (newLength > newCapacity) 672 newCapacity *= 2; 673 if (newCapacity != capacity) 674 { 675 auto newBase = xmalloc(newCapacity); 676 newBase[0 .. length] = base[0 .. length]; 677 // old base just leaks, could be used by exceptions still in flight 678 base = newBase; 679 capacity = newCapacity; 680 } 681 length = newLength; 682 return offset; 683 } 684 } 685 686 __gshared EHHeap ehHeap; 687 688 ImgPtr!T eh_malloc(T)(size_t size = T.sizeof) 689 { 690 return ImgPtr!T(cast(uint) ehHeap.malloc(size)); 691 } 692 693 // NB: The returned pointer may be invalidated by a consequent grow of ehHeap! 694 T* toPointer(T)(ImgPtr!T imgPtr) 695 { 696 return cast(T*) (ehHeap.base + imgPtr.offset); 697 } 698 }