1 /*
   2  * Copyright (c) 2002, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  *
  23  */
  24 
  25 package sun.jvm.hotspot.debugger.linux;
  26 
  27 import java.io.File;
  28 import java.io.IOException;
  29 import java.io.UncheckedIOException;
  30 import java.nio.file.Files;
  31 import java.nio.file.Path;
  32 import java.nio.file.Paths;
  33 import java.util.ArrayList;
  34 import java.util.List;
  35 import java.util.Map;
  36 import java.util.NoSuchElementException;
  37 import java.util.stream.Collectors;
  38 
  39 import sun.jvm.hotspot.debugger.Address;
  40 import sun.jvm.hotspot.debugger.DebuggerBase;
  41 import sun.jvm.hotspot.debugger.DebuggerException;
  42 import sun.jvm.hotspot.debugger.DebuggerUtilities;
  43 import sun.jvm.hotspot.debugger.MachineDescription;
  44 import sun.jvm.hotspot.debugger.NotInHeapException;
  45 import sun.jvm.hotspot.debugger.OopHandle;
  46 import sun.jvm.hotspot.debugger.ReadResult;
  47 import sun.jvm.hotspot.debugger.ThreadProxy;
  48 import sun.jvm.hotspot.debugger.UnalignedAddressException;
  49 import sun.jvm.hotspot.debugger.UnmappedAddressException;
  50 import sun.jvm.hotspot.debugger.cdbg.CDebugger;
  51 import sun.jvm.hotspot.debugger.cdbg.ClosestSymbol;
  52 import sun.jvm.hotspot.debugger.cdbg.LoadObject;
  53 import sun.jvm.hotspot.utilities.PlatformInfo;
  54 
  55 /** <P> An implementation of the JVMDebugger interface. The basic debug
  56     facilities are implemented through ptrace interface in the JNI code
  57     (libsaproc.so). Library maps and symbol table management are done in
  58     JNI. </P>
  59 
  60     <P> <B>NOTE</B> that since we have the notion of fetching "Java
  61     primitive types" from the remote process (which might have
  62     different sizes than we expect) we have a bootstrapping
  63     problem. We need to know the sizes of these types before we can
  64     fetch them. The current implementation solves this problem by
  65     requiring that it be configured with these type sizes before they
  66     can be fetched. The readJ(Type) routines here will throw a
  67     RuntimeException if they are called before the debugger is
  68     configured with the Java primitive type sizes. </P> */
  69 
  70 public class LinuxDebuggerLocal extends DebuggerBase implements LinuxDebugger {
  71     private boolean useGCC32ABI;
  72     private boolean attached;
  73     private long    p_ps_prochandle; // native debugger handle
  74     private boolean isCore;
  75 
  76     // CDebugger support
  77     private LinuxCDebugger cdbg;
  78 
  79     // threadList and loadObjectList are filled by attach0 method
  80     private List threadList;
  81     private List loadObjectList;
  82 
  83     // PID namespace support
  84     // It maps the LWPID in the host to the LWPID in the container.
  85     private Map<Integer, Integer> nspidMap;
  86 
  87     // called by native method lookupByAddress0
  88     private ClosestSymbol createClosestSymbol(String name, long offset) {
  89        return new ClosestSymbol(name, offset);
  90     }
  91 
  92     // called by native method attach0
  93     private LoadObject createLoadObject(String fileName, long textsize,
  94                                         long base) {
  95        File f = new File(fileName);
  96        Address baseAddr = newAddress(base);
  97        return new SharedObject(this, fileName, f.length(), baseAddr);
  98     }
  99 
 100     // native methods
 101 
 102     private native static void init0()
 103                                 throws DebuggerException;
 104     private native void setSAAltRoot0(String altroot);
 105     private native void attach0(int pid)
 106                                 throws DebuggerException;
 107     private native void attach0(String execName, String coreName)
 108                                 throws DebuggerException;
 109     private native void detach0()
 110                                 throws DebuggerException;
 111     private native long lookupByName0(String objectName, String symbol)
 112                                 throws DebuggerException;
 113     private native ClosestSymbol lookupByAddress0(long address)
 114                                 throws DebuggerException;
 115     private native long[] getThreadIntegerRegisterSet0(int lwp_id)
 116                                 throws DebuggerException;
 117     private native byte[] readBytesFromProcess0(long address, long numBytes)
 118                                 throws DebuggerException;
 119     public native static int  getAddressSize() ;
 120 
 121     @Override
 122     public native String demangle(String sym);
 123 
 124     public native long findLibPtrByAddress0(long pc);
 125 
 126     @Override
 127     public long findLibPtrByAddress(Address pc) {
 128       return findLibPtrByAddress0(pc.asLongValue());
 129     }
 130 
 131     // Note on Linux threads are really processes. When target process is
 132     // attached by a serviceability agent thread, only that thread can do
 133     // ptrace operations on the target. This is because from kernel's point
 134     // view, other threads are just separate processes and they are not
 135     // attached to the target. When they attempt to make ptrace calls,
 136     // an ESRCH error will be returned as kernel believes target is not
 137     // being traced by the caller.
 138     // To work around the problem, we use a worker thread here to handle
 139     // all JNI functions that are making ptrace calls.
 140 
 141     interface WorkerThreadTask {
 142        public void doit(LinuxDebuggerLocal debugger) throws DebuggerException;
 143     }
 144 
 145     class LinuxDebuggerLocalWorkerThread extends Thread {
 146        LinuxDebuggerLocal debugger;
 147        WorkerThreadTask task;
 148        DebuggerException lastException;
 149 
 150        public LinuxDebuggerLocalWorkerThread(LinuxDebuggerLocal debugger) {
 151          this.debugger = debugger;
 152          setDaemon(true);
 153        }
 154 
 155        public void run() {
 156           synchronized (workerThread) {
 157              for (;;) {
 158                 if (task != null) {
 159                    lastException = null;
 160                    try {
 161                       task.doit(debugger);
 162                    } catch (DebuggerException exp) {
 163                       lastException = exp;
 164                    }
 165                    task = null;
 166                    workerThread.notifyAll();
 167                 }
 168 
 169                 try {
 170                    workerThread.wait();
 171                 } catch (InterruptedException x) {}
 172              }
 173           }
 174        }
 175 
 176        public WorkerThreadTask execute(WorkerThreadTask task) throws DebuggerException {
 177           synchronized (workerThread) {
 178              this.task = task;
 179              workerThread.notifyAll();
 180              while (this.task != null) {
 181                 try {
 182                    workerThread.wait();
 183                 } catch (InterruptedException x) {}
 184              }
 185              if (lastException != null) {
 186                 throw new DebuggerException(lastException);
 187              } else {
 188                 return task;
 189              }
 190           }
 191        }
 192     }
 193 
 194     private LinuxDebuggerLocalWorkerThread workerThread = null;
 195 
 196     //----------------------------------------------------------------------
 197     // Implementation of Debugger interface
 198     //
 199 
 200     /** <P> machDesc may not be null. </P>
 201 
 202     <P> useCache should be set to true if debugging is being done
 203     locally, and to false if the debugger is being created for the
 204     purpose of supporting remote debugging. </P> */
 205     public LinuxDebuggerLocal(MachineDescription machDesc,
 206                               boolean useCache) throws DebuggerException {
 207         this.machDesc = machDesc;
 208         utils = new DebuggerUtilities(machDesc.getAddressSize(),
 209                                       machDesc.isBigEndian()) {
 210            public void checkAlignment(long address, long alignment) {
 211              // Need to override default checkAlignment because we need to
 212              // relax alignment constraints on Linux/x86
 213              if ( (address % alignment != 0)
 214                 &&(alignment != 8 || address % 4 != 0)) {
 215                 throw new UnalignedAddressException(
 216                         "Trying to read at address: "
 217                       + addressValueToString(address)
 218                       + " with alignment: " + alignment,
 219                         address);
 220              }
 221            }
 222         };
 223 
 224         if (useCache) {
 225             // FIXME: re-test necessity of cache on Linux, where data
 226             // fetching is faster
 227             // Cache portion of the remote process's address space.
 228             // Fetching data over the socket connection to dbx is slow.
 229             // Might be faster if we were using a binary protocol to talk to
 230             // dbx, but would have to test. For now, this cache works best
 231             // if it covers the entire heap of the remote process. FIXME: at
 232             // least should make this tunable from the outside, i.e., via
 233             // the UI. This is a cache of 4096 4K pages, or 16 MB. The page
 234             // size must be adjusted to be the hardware's page size.
 235             // (FIXME: should pick this up from the debugger.)
 236             initCache(4096, parseCacheNumPagesProperty(4096));
 237         }
 238 
 239         workerThread = new LinuxDebuggerLocalWorkerThread(this);
 240         workerThread.start();
 241     }
 242 
 243     /** From the Debugger interface via JVMDebugger */
 244     public boolean hasProcessList() throws DebuggerException {
 245         return false;
 246     }
 247 
 248     /** From the Debugger interface via JVMDebugger */
 249     public List getProcessList() throws DebuggerException {
 250         throw new DebuggerException("getProcessList not implemented yet");
 251     }
 252 
 253     private void checkAttached() throws DebuggerException {
 254         if (attached) {
 255             if (isCore) {
 256                 throw new DebuggerException("attached to a core dump already");
 257             } else {
 258                 throw new DebuggerException("attached to a process already");
 259             }
 260         }
 261     }
 262 
 263     private void requireAttach() {
 264         if (! attached) {
 265             throw new RuntimeException("not attached to a process or a core!");
 266         }
 267     }
 268 
 269     /* called from attach methods */
 270     private void findABIVersion() throws DebuggerException {
 271         if (lookupByName0("libjvm.so", "__vt_10JavaThread") != 0) {
 272             // old C++ ABI
 273             useGCC32ABI = false;
 274         } else {
 275             // new C++ ABI
 276             useGCC32ABI = true;
 277         }
 278     }
 279 
 280     // Get namespace PID from /proc/<PID>/status.
 281     private int getNamespacePID(Path statusPath) {
 282         try (var lines = Files.lines(statusPath)) {
 283             return lines.map(s -> s.split("\\s+"))
 284                         .filter(a -> a.length == 3)
 285                         .filter(a -> a[0].equals("NSpid:"))
 286                         .mapToInt(a -> Integer.valueOf(a[2]))
 287                         .findFirst()
 288                         .getAsInt();
 289         } catch (IOException | NoSuchElementException e) {
 290             return Integer.valueOf(statusPath.getParent()
 291                                              .toFile()
 292                                              .getName());
 293         }
 294     }
 295 
 296     // Get LWPID in the host from the container's LWPID.
 297     // Returns -1 if the process is running in the host.
 298     public int getHostPID(int id) {
 299         return (nspidMap == null) ? -1 : nspidMap.get(id);
 300     }
 301 
 302     // Fill namespace PID map from procfs.
 303     // This method scans all tasks (/proc/<PID>/task) in the process.
 304     private void fillNSpidMap(Path proc) {
 305         Path task = Paths.get(proc.toString(), "task");
 306         try (var tasks = Files.list(task)) {
 307             nspidMap = tasks.filter(p -> !p.toString().startsWith("."))
 308                             .collect(Collectors.toMap(p -> Integer.valueOf(getNamespacePID(Paths.get(p.toString(), "status"))),
 309                                                       p -> Integer.valueOf(p.toFile().getName())));
 310         } catch (IOException e) {
 311             throw new UncheckedIOException(e);
 312         }
 313     }
 314 
 315     /** From the Debugger interface via JVMDebugger */
 316     public synchronized void attach(int processID) throws DebuggerException {
 317         checkAttached();
 318         threadList = new ArrayList();
 319         loadObjectList = new ArrayList();
 320 
 321         Path proc = Paths.get("/proc", Integer.toString(processID));
 322         int NSpid = getNamespacePID(Paths.get(proc.toString(), "status"));
 323         if (NSpid != processID) {
 324             // If PID different from namespace PID, we can assume the process
 325             // is running in the container.
 326             // So we need to set SA_ALTROOT environment variable that SA reads
 327             // binaries in the container.
 328             setSAAltRoot0(Paths.get(proc.toString(), "root").toString());
 329             fillNSpidMap(proc);
 330         }
 331 
 332         class AttachTask implements WorkerThreadTask {
 333            int pid;
 334            public void doit(LinuxDebuggerLocal debugger) {
 335               debugger.attach0(pid);
 336               debugger.attached = true;
 337               debugger.isCore = false;
 338               findABIVersion();
 339            }
 340         }
 341 
 342         AttachTask task = new AttachTask();
 343         task.pid = processID;
 344         workerThread.execute(task);
 345     }
 346 
 347     /** From the Debugger interface via JVMDebugger */
 348     public synchronized void attach(String execName, String coreName) {
 349         checkAttached();
 350         threadList = new ArrayList();
 351         loadObjectList = new ArrayList();
 352         attach0(execName, coreName);
 353         attached = true;
 354         isCore = true;
 355         findABIVersion();
 356     }
 357 
 358     /** From the Debugger interface via JVMDebugger */
 359     public synchronized boolean detach() {
 360         if (!attached) {
 361             return false;
 362         }
 363 
 364         threadList = null;
 365         loadObjectList = null;
 366 
 367         if (isCore) {
 368             detach0();
 369             attached = false;
 370             return true;
 371         } else {
 372             class DetachTask implements WorkerThreadTask {
 373                 boolean result = false;
 374 
 375                 public void doit(LinuxDebuggerLocal debugger) {
 376                     debugger.detach0();
 377                     debugger.attached = false;
 378                     result = true;
 379                 }
 380             }
 381 
 382             DetachTask task = new DetachTask();
 383             workerThread.execute(task);
 384             return task.result;
 385         }
 386     }
 387 
 388     /** From the Debugger interface via JVMDebugger */
 389     public Address parseAddress(String addressString)
 390             throws NumberFormatException {
 391         long addr = utils.scanAddress(addressString);
 392         if (addr == 0) {
 393             return null;
 394         }
 395         return new LinuxAddress(this, addr);
 396     }
 397 
 398     /** From the Debugger interface via JVMDebugger */
 399     public String getOS() {
 400         return PlatformInfo.getOS();
 401     }
 402 
 403     /** From the Debugger interface via JVMDebugger */
 404     public String getCPU() {
 405         return PlatformInfo.getCPU();
 406     }
 407 
 408     public boolean hasConsole() throws DebuggerException {
 409         return false;
 410     }
 411 
 412     public String consoleExecuteCommand(String cmd) throws DebuggerException {
 413         throw new DebuggerException("No debugger console available on Linux");
 414     }
 415 
 416     public String getConsolePrompt() throws DebuggerException {
 417         return null;
 418     }
 419 
 420     /* called from lookup */
 421     private long handleGCC32ABI(long addr, String symbol) throws DebuggerException {
 422         if (useGCC32ABI && symbol.startsWith("_ZTV")) {
 423             return addr + (2 * machDesc.getAddressSize());
 424         } else {
 425             return addr;
 426         }
 427     }
 428 
 429     /** From the SymbolLookup interface via Debugger and JVMDebugger */
 430     public synchronized Address lookup(String objectName, String symbol) {
 431         requireAttach();
 432         if (!attached) {
 433             return null;
 434         }
 435 
 436         if (isCore) {
 437             long addr = lookupByName0(objectName, symbol);
 438             return (addr == 0)? null : new LinuxAddress(this, handleGCC32ABI(addr, symbol));
 439         } else {
 440             class LookupByNameTask implements WorkerThreadTask {
 441                 String objectName, symbol;
 442                 Address result;
 443 
 444                 public void doit(LinuxDebuggerLocal debugger) {
 445                     long addr = debugger.lookupByName0(objectName, symbol);
 446                     result = (addr == 0 ? null : new LinuxAddress(debugger, handleGCC32ABI(addr, symbol)));
 447                 }
 448             }
 449 
 450             LookupByNameTask task = new LookupByNameTask();
 451             task.objectName = objectName;
 452             task.symbol = symbol;
 453             workerThread.execute(task);
 454             return task.result;
 455         }
 456     }
 457 
 458     /** From the SymbolLookup interface via Debugger and JVMDebugger */
 459     public synchronized OopHandle lookupOop(String objectName, String symbol) {
 460         Address addr = lookup(objectName, symbol);
 461         if (addr == null) {
 462             return null;
 463         }
 464         return addr.addOffsetToAsOopHandle(0);
 465     }
 466 
 467     /** From the Debugger interface */
 468     public MachineDescription getMachineDescription() {
 469         return machDesc;
 470     }
 471 
 472     //----------------------------------------------------------------------
 473     // Implementation of ThreadAccess interface
 474     //
 475 
 476     /** From the ThreadAccess interface via Debugger and JVMDebugger */
 477     public ThreadProxy getThreadForIdentifierAddress(Address addr) {
 478         return new LinuxThread(this, addr);
 479     }
 480 
 481     /** From the ThreadAccess interface via Debugger and JVMDebugger */
 482     public ThreadProxy getThreadForThreadId(long id) {
 483         return new LinuxThread(this, id);
 484     }
 485 
 486     //----------------------------------------------------------------------
 487     // Internal routines (for implementation of LinuxAddress).
 488     // These must not be called until the MachineDescription has been set up.
 489     //
 490 
 491     /** From the LinuxDebugger interface */
 492     public String addressValueToString(long address) {
 493         return utils.addressValueToString(address);
 494     }
 495 
 496     /** From the LinuxDebugger interface */
 497     public LinuxAddress readAddress(long address)
 498             throws UnmappedAddressException, UnalignedAddressException {
 499         long value = readAddressValue(address);
 500         return (value == 0 ? null : new LinuxAddress(this, value));
 501     }
 502     public LinuxAddress readCompOopAddress(long address)
 503             throws UnmappedAddressException, UnalignedAddressException {
 504         long value = readCompOopAddressValue(address);
 505         return (value == 0 ? null : new LinuxAddress(this, value));
 506     }
 507 
 508     public LinuxAddress readCompKlassAddress(long address)
 509             throws UnmappedAddressException, UnalignedAddressException {
 510         long value = readCompKlassAddressValue(address);
 511         return (value == 0 ? null : new LinuxAddress(this, value));
 512     }
 513 
 514     /** From the LinuxDebugger interface */
 515     public LinuxOopHandle readOopHandle(long address)
 516             throws UnmappedAddressException, UnalignedAddressException,
 517                 NotInHeapException {
 518         long value = readAddressValue(address);
 519         return (value == 0 ? null : new LinuxOopHandle(this, value));
 520     }
 521     public LinuxOopHandle readCompOopHandle(long address)
 522             throws UnmappedAddressException, UnalignedAddressException,
 523                 NotInHeapException {
 524         long value = readCompOopAddressValue(address);
 525         return (value == 0 ? null : new LinuxOopHandle(this, value));
 526     }
 527 
 528     //----------------------------------------------------------------------
 529     // Thread context access
 530     //
 531 
 532     public synchronized long[] getThreadIntegerRegisterSet(int lwp_id)
 533                                             throws DebuggerException {
 534         requireAttach();
 535         if (isCore) {
 536             return getThreadIntegerRegisterSet0(lwp_id);
 537         } else {
 538             class GetThreadIntegerRegisterSetTask implements WorkerThreadTask {
 539                 int lwp_id;
 540                 long[] result;
 541                 public void doit(LinuxDebuggerLocal debugger) {
 542                     result = debugger.getThreadIntegerRegisterSet0(lwp_id);
 543                 }
 544             }
 545 
 546             GetThreadIntegerRegisterSetTask task = new GetThreadIntegerRegisterSetTask();
 547             task.lwp_id = lwp_id;
 548             workerThread.execute(task);
 549             return task.result;
 550         }
 551     }
 552 
 553     /** Need to override this to relax alignment checks on x86. */
 554     public long readCInteger(long address, long numBytes, boolean isUnsigned)
 555         throws UnmappedAddressException, UnalignedAddressException {
 556         // Only slightly relaxed semantics -- this is a hack, but is
 557         // necessary on x86 where it seems the compiler is
 558         // putting some global 64-bit data on 32-bit boundaries
 559         if (numBytes == 8) {
 560             utils.checkAlignment(address, 4);
 561         } else {
 562             utils.checkAlignment(address, numBytes);
 563         }
 564         byte[] data = readBytes(address, numBytes);
 565         return utils.dataToCInteger(data, isUnsigned);
 566     }
 567 
 568     // Overridden from DebuggerBase because we need to relax alignment
 569     // constraints on x86
 570     public long readJLong(long address)
 571         throws UnmappedAddressException, UnalignedAddressException {
 572         utils.checkAlignment(address, jintSize);
 573         byte[] data = readBytes(address, jlongSize);
 574         return utils.dataToJLong(data, jlongSize);
 575     }
 576 
 577     //----------------------------------------------------------------------
 578     // Address access. Can not be package private, but should only be
 579     // accessed by the architecture-specific subpackages.
 580 
 581     /** From the LinuxDebugger interface */
 582     public long getAddressValue(Address addr) {
 583       if (addr == null) return 0;
 584       return ((LinuxAddress) addr).getValue();
 585     }
 586 
 587     /** From the LinuxDebugger interface */
 588     public Address newAddress(long value) {
 589       if (value == 0) return null;
 590       return new LinuxAddress(this, value);
 591     }
 592 
 593     /** From the LinuxCDebugger interface */
 594     public List/*<ThreadProxy>*/ getThreadList() {
 595       requireAttach();
 596       return threadList;
 597     }
 598 
 599     /** From the LinuxCDebugger interface */
 600     public List/*<LoadObject>*/ getLoadObjectList() {
 601       requireAttach();
 602       return loadObjectList;
 603     }
 604 
 605     /** From the LinuxCDebugger interface */
 606     public synchronized ClosestSymbol lookup(long addr) {
 607        requireAttach();
 608        if (isCore) {
 609           return lookupByAddress0(addr);
 610        } else {
 611           class LookupByAddressTask implements WorkerThreadTask {
 612              long addr;
 613              ClosestSymbol result;
 614 
 615              public void doit(LinuxDebuggerLocal debugger) {
 616                  result = debugger.lookupByAddress0(addr);
 617              }
 618           }
 619 
 620           LookupByAddressTask task = new LookupByAddressTask();
 621           task.addr = addr;
 622           workerThread.execute(task);
 623           return task.result;
 624        }
 625     }
 626 
 627     public CDebugger getCDebugger() {
 628       if (cdbg == null) {
 629          cdbg = new LinuxCDebugger(this);
 630       }
 631       return cdbg;
 632     }
 633 
 634     /** This reads bytes from the remote process. */
 635     public synchronized ReadResult readBytesFromProcess(long address,
 636             long numBytes) throws UnmappedAddressException, DebuggerException {
 637         requireAttach();
 638         if (isCore) {
 639             byte[] res = readBytesFromProcess0(address, numBytes);
 640             return (res != null)? new ReadResult(res) : new ReadResult(address);
 641         } else {
 642             class ReadBytesFromProcessTask implements WorkerThreadTask {
 643                 long address, numBytes;
 644                 ReadResult result;
 645                 public void doit(LinuxDebuggerLocal debugger) {
 646                     byte[] res = debugger.readBytesFromProcess0(address, numBytes);
 647                     if (res != null)
 648                         result = new ReadResult(res);
 649                     else
 650                         result = new ReadResult(address);
 651                 }
 652             }
 653 
 654             ReadBytesFromProcessTask task = new ReadBytesFromProcessTask();
 655             task.address = address;
 656             task.numBytes = numBytes;
 657             workerThread.execute(task);
 658             return task.result;
 659         }
 660     }
 661 
 662     public void writeBytesToProcess(long address, long numBytes, byte[] data)
 663         throws UnmappedAddressException, DebuggerException {
 664         // FIXME
 665         throw new DebuggerException("Unimplemented");
 666     }
 667 
 668     static {
 669         System.loadLibrary("saproc");
 670         init0();
 671     }
 672 }