1 /*
   2  * Copyright (c) 2002, 2020, 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 Address findLibPtrByAddress(Address pc) {
 128       long ptr = findLibPtrByAddress0(pc.asLongValue());
 129       return (ptr == 0L) ? null
 130                          : new LinuxAddress(this, ptr);
 131     }
 132 
 133     // Note on Linux threads are really processes. When target process is
 134     // attached by a serviceability agent thread, only that thread can do
 135     // ptrace operations on the target. This is because from kernel's point
 136     // view, other threads are just separate processes and they are not
 137     // attached to the target. When they attempt to make ptrace calls,
 138     // an ESRCH error will be returned as kernel believes target is not
 139     // being traced by the caller.
 140     // To work around the problem, we use a worker thread here to handle
 141     // all JNI functions that are making ptrace calls.
 142 
 143     interface WorkerThreadTask {
 144        public void doit(LinuxDebuggerLocal debugger) throws DebuggerException;
 145     }
 146 
 147     class LinuxDebuggerLocalWorkerThread extends Thread {
 148        LinuxDebuggerLocal debugger;
 149        WorkerThreadTask task;
 150        DebuggerException lastException;
 151 
 152        public LinuxDebuggerLocalWorkerThread(LinuxDebuggerLocal debugger) {
 153          this.debugger = debugger;
 154          setDaemon(true);
 155        }
 156 
 157        public void run() {
 158           synchronized (workerThread) {
 159              for (;;) {
 160                 if (task != null) {
 161                    lastException = null;
 162                    try {
 163                       task.doit(debugger);
 164                    } catch (DebuggerException exp) {
 165                       lastException = exp;
 166                    }
 167                    task = null;
 168                    workerThread.notifyAll();
 169                 }
 170 
 171                 try {
 172                    workerThread.wait();
 173                 } catch (InterruptedException x) {}
 174              }
 175           }
 176        }
 177 
 178        public WorkerThreadTask execute(WorkerThreadTask task) throws DebuggerException {
 179           synchronized (workerThread) {
 180              this.task = task;
 181              workerThread.notifyAll();
 182              while (this.task != null) {
 183                 try {
 184                    workerThread.wait();
 185                 } catch (InterruptedException x) {}
 186              }
 187              if (lastException != null) {
 188                 throw new DebuggerException(lastException);
 189              } else {
 190                 return task;
 191              }
 192           }
 193        }
 194     }
 195 
 196     private LinuxDebuggerLocalWorkerThread workerThread = null;
 197 
 198     //----------------------------------------------------------------------
 199     // Implementation of Debugger interface
 200     //
 201 
 202     /** <P> machDesc may not be null. </P>
 203 
 204     <P> useCache should be set to true if debugging is being done
 205     locally, and to false if the debugger is being created for the
 206     purpose of supporting remote debugging. </P> */
 207     public LinuxDebuggerLocal(MachineDescription machDesc,
 208                               boolean useCache) throws DebuggerException {
 209         this.machDesc = machDesc;
 210         utils = new DebuggerUtilities(machDesc.getAddressSize(),
 211                                       machDesc.isBigEndian()) {
 212            public void checkAlignment(long address, long alignment) {
 213              // Need to override default checkAlignment because we need to
 214              // relax alignment constraints on Linux/x86
 215              if ( (address % alignment != 0)
 216                 &&(alignment != 8 || address % 4 != 0)) {
 217                 throw new UnalignedAddressException(
 218                         "Trying to read at address: "
 219                       + addressValueToString(address)
 220                       + " with alignment: " + alignment,
 221                         address);
 222              }
 223            }
 224         };
 225 
 226         if (useCache) {
 227             // FIXME: re-test necessity of cache on Linux, where data
 228             // fetching is faster
 229             // Cache portion of the remote process's address space.
 230             // Fetching data over the socket connection to dbx is slow.
 231             // Might be faster if we were using a binary protocol to talk to
 232             // dbx, but would have to test. For now, this cache works best
 233             // if it covers the entire heap of the remote process. FIXME: at
 234             // least should make this tunable from the outside, i.e., via
 235             // the UI. This is a cache of 4096 4K pages, or 16 MB. The page
 236             // size must be adjusted to be the hardware's page size.
 237             // (FIXME: should pick this up from the debugger.)
 238             initCache(4096, parseCacheNumPagesProperty(4096));
 239         }
 240 
 241         workerThread = new LinuxDebuggerLocalWorkerThread(this);
 242         workerThread.start();
 243     }
 244 
 245     /** From the Debugger interface via JVMDebugger */
 246     public boolean hasProcessList() throws DebuggerException {
 247         return false;
 248     }
 249 
 250     /** From the Debugger interface via JVMDebugger */
 251     public List getProcessList() throws DebuggerException {
 252         throw new DebuggerException("getProcessList not implemented yet");
 253     }
 254 
 255     private void checkAttached() throws DebuggerException {
 256         if (attached) {
 257             if (isCore) {
 258                 throw new DebuggerException("attached to a core dump already");
 259             } else {
 260                 throw new DebuggerException("attached to a process already");
 261             }
 262         }
 263     }
 264 
 265     private void requireAttach() {
 266         if (! attached) {
 267             throw new RuntimeException("not attached to a process or a core!");
 268         }
 269     }
 270 
 271     /* called from attach methods */
 272     private void findABIVersion() throws DebuggerException {
 273         if (lookupByName0("libjvm.so", "__vt_10JavaThread") != 0) {
 274             // old C++ ABI
 275             useGCC32ABI = false;
 276         } else {
 277             // new C++ ABI
 278             useGCC32ABI = true;
 279         }
 280     }
 281 
 282     // Get namespace PID from /proc/<PID>/status.
 283     private int getNamespacePID(Path statusPath) {
 284         try (var lines = Files.lines(statusPath)) {
 285             return lines.map(s -> s.split("\\s+"))
 286                         .filter(a -> a.length == 3)
 287                         .filter(a -> a[0].equals("NSpid:"))
 288                         .mapToInt(a -> Integer.valueOf(a[2]))
 289                         .findFirst()
 290                         .getAsInt();
 291         } catch (IOException | NoSuchElementException e) {
 292             return Integer.valueOf(statusPath.getParent()
 293                                              .toFile()
 294                                              .getName());
 295         }
 296     }
 297 
 298     // Get LWPID in the host from the container's LWPID.
 299     // Returns -1 if the process is running in the host.
 300     public int getHostPID(int id) {
 301         return (nspidMap == null) ? -1 : nspidMap.get(id);
 302     }
 303 
 304     // Fill namespace PID map from procfs.
 305     // This method scans all tasks (/proc/<PID>/task) in the process.
 306     private void fillNSpidMap(Path proc) {
 307         Path task = Paths.get(proc.toString(), "task");
 308         try (var tasks = Files.list(task)) {
 309             nspidMap = tasks.filter(p -> !p.toString().startsWith("."))
 310                             .collect(Collectors.toMap(p -> Integer.valueOf(getNamespacePID(Paths.get(p.toString(), "status"))),
 311                                                       p -> Integer.valueOf(p.toFile().getName())));
 312         } catch (IOException e) {
 313             throw new UncheckedIOException(e);
 314         }
 315     }
 316 
 317     /** From the Debugger interface via JVMDebugger */
 318     public synchronized void attach(int processID) throws DebuggerException {
 319         checkAttached();
 320         threadList = new ArrayList();
 321         loadObjectList = new ArrayList();
 322 
 323         Path proc = Paths.get("/proc", Integer.toString(processID));
 324         int NSpid = getNamespacePID(Paths.get(proc.toString(), "status"));
 325         if (NSpid != processID) {
 326             // If PID different from namespace PID, we can assume the process
 327             // is running in the container.
 328             // So we need to set SA_ALTROOT environment variable that SA reads
 329             // binaries in the container.
 330             setSAAltRoot0(Paths.get(proc.toString(), "root").toString());
 331             fillNSpidMap(proc);
 332         }
 333 
 334         class AttachTask implements WorkerThreadTask {
 335            int pid;
 336            public void doit(LinuxDebuggerLocal debugger) {
 337               debugger.attach0(pid);
 338               debugger.attached = true;
 339               debugger.isCore = false;
 340               findABIVersion();
 341            }
 342         }
 343 
 344         AttachTask task = new AttachTask();
 345         task.pid = processID;
 346         workerThread.execute(task);
 347     }
 348 
 349     /** From the Debugger interface via JVMDebugger */
 350     public synchronized void attach(String execName, String coreName) {
 351         checkAttached();
 352         threadList = new ArrayList();
 353         loadObjectList = new ArrayList();
 354         attach0(execName, coreName);
 355         attached = true;
 356         isCore = true;
 357         findABIVersion();
 358     }
 359 
 360     /** From the Debugger interface via JVMDebugger */
 361     public synchronized boolean detach() {
 362         if (!attached) {
 363             return false;
 364         }
 365 
 366         threadList = null;
 367         loadObjectList = null;
 368 
 369         if (isCore) {
 370             detach0();
 371             attached = false;
 372             return true;
 373         } else {
 374             class DetachTask implements WorkerThreadTask {
 375                 boolean result = false;
 376 
 377                 public void doit(LinuxDebuggerLocal debugger) {
 378                     debugger.detach0();
 379                     debugger.attached = false;
 380                     result = true;
 381                 }
 382             }
 383 
 384             DetachTask task = new DetachTask();
 385             workerThread.execute(task);
 386             return task.result;
 387         }
 388     }
 389 
 390     /** From the Debugger interface via JVMDebugger */
 391     public Address parseAddress(String addressString)
 392             throws NumberFormatException {
 393         long addr = utils.scanAddress(addressString);
 394         if (addr == 0) {
 395             return null;
 396         }
 397         return new LinuxAddress(this, addr);
 398     }
 399 
 400     /** From the Debugger interface via JVMDebugger */
 401     public String getOS() {
 402         return PlatformInfo.getOS();
 403     }
 404 
 405     /** From the Debugger interface via JVMDebugger */
 406     public String getCPU() {
 407         return PlatformInfo.getCPU();
 408     }
 409 
 410     public boolean hasConsole() throws DebuggerException {
 411         return false;
 412     }
 413 
 414     public String consoleExecuteCommand(String cmd) throws DebuggerException {
 415         throw new DebuggerException("No debugger console available on Linux");
 416     }
 417 
 418     public String getConsolePrompt() throws DebuggerException {
 419         return null;
 420     }
 421 
 422     /* called from lookup */
 423     private long handleGCC32ABI(long addr, String symbol) throws DebuggerException {
 424         if (useGCC32ABI && symbol.startsWith("_ZTV")) {
 425             return addr + (2 * machDesc.getAddressSize());
 426         } else {
 427             return addr;
 428         }
 429     }
 430 
 431     /** From the SymbolLookup interface via Debugger and JVMDebugger */
 432     public synchronized Address lookup(String objectName, String symbol) {
 433         requireAttach();
 434         if (!attached) {
 435             return null;
 436         }
 437 
 438         if (isCore) {
 439             long addr = lookupByName0(objectName, symbol);
 440             return (addr == 0)? null : new LinuxAddress(this, handleGCC32ABI(addr, symbol));
 441         } else {
 442             class LookupByNameTask implements WorkerThreadTask {
 443                 String objectName, symbol;
 444                 Address result;
 445 
 446                 public void doit(LinuxDebuggerLocal debugger) {
 447                     long addr = debugger.lookupByName0(objectName, symbol);
 448                     result = (addr == 0 ? null : new LinuxAddress(debugger, handleGCC32ABI(addr, symbol)));
 449                 }
 450             }
 451 
 452             LookupByNameTask task = new LookupByNameTask();
 453             task.objectName = objectName;
 454             task.symbol = symbol;
 455             workerThread.execute(task);
 456             return task.result;
 457         }
 458     }
 459 
 460     /** From the SymbolLookup interface via Debugger and JVMDebugger */
 461     public synchronized OopHandle lookupOop(String objectName, String symbol) {
 462         Address addr = lookup(objectName, symbol);
 463         if (addr == null) {
 464             return null;
 465         }
 466         return addr.addOffsetToAsOopHandle(0);
 467     }
 468 
 469     /** From the Debugger interface */
 470     public MachineDescription getMachineDescription() {
 471         return machDesc;
 472     }
 473 
 474     //----------------------------------------------------------------------
 475     // Implementation of ThreadAccess interface
 476     //
 477 
 478     /** From the ThreadAccess interface via Debugger and JVMDebugger */
 479     public ThreadProxy getThreadForIdentifierAddress(Address addr) {
 480         return new LinuxThread(this, addr);
 481     }
 482 
 483     /** From the ThreadAccess interface via Debugger and JVMDebugger */
 484     public ThreadProxy getThreadForThreadId(long id) {
 485         return new LinuxThread(this, id);
 486     }
 487 
 488     //----------------------------------------------------------------------
 489     // Internal routines (for implementation of LinuxAddress).
 490     // These must not be called until the MachineDescription has been set up.
 491     //
 492 
 493     /** From the LinuxDebugger interface */
 494     public String addressValueToString(long address) {
 495         return utils.addressValueToString(address);
 496     }
 497 
 498     /** From the LinuxDebugger interface */
 499     public LinuxAddress readAddress(long address)
 500             throws UnmappedAddressException, UnalignedAddressException {
 501         long value = readAddressValue(address);
 502         return (value == 0 ? null : new LinuxAddress(this, value));
 503     }
 504     public LinuxAddress readCompOopAddress(long address)
 505             throws UnmappedAddressException, UnalignedAddressException {
 506         long value = readCompOopAddressValue(address);
 507         return (value == 0 ? null : new LinuxAddress(this, value));
 508     }
 509 
 510     public LinuxAddress readCompKlassAddress(long address)
 511             throws UnmappedAddressException, UnalignedAddressException {
 512         long value = readCompKlassAddressValue(address);
 513         return (value == 0 ? null : new LinuxAddress(this, value));
 514     }
 515 
 516     /** From the LinuxDebugger interface */
 517     public LinuxOopHandle readOopHandle(long address)
 518             throws UnmappedAddressException, UnalignedAddressException,
 519                 NotInHeapException {
 520         long value = readAddressValue(address);
 521         return (value == 0 ? null : new LinuxOopHandle(this, value));
 522     }
 523     public LinuxOopHandle readCompOopHandle(long address)
 524             throws UnmappedAddressException, UnalignedAddressException,
 525                 NotInHeapException {
 526         long value = readCompOopAddressValue(address);
 527         return (value == 0 ? null : new LinuxOopHandle(this, value));
 528     }
 529 
 530     //----------------------------------------------------------------------
 531     // Thread context access
 532     //
 533 
 534     public synchronized long[] getThreadIntegerRegisterSet(int lwp_id)
 535                                             throws DebuggerException {
 536         requireAttach();
 537         if (isCore) {
 538             return getThreadIntegerRegisterSet0(lwp_id);
 539         } else {
 540             class GetThreadIntegerRegisterSetTask implements WorkerThreadTask {
 541                 int lwp_id;
 542                 long[] result;
 543                 public void doit(LinuxDebuggerLocal debugger) {
 544                     result = debugger.getThreadIntegerRegisterSet0(lwp_id);
 545                 }
 546             }
 547 
 548             GetThreadIntegerRegisterSetTask task = new GetThreadIntegerRegisterSetTask();
 549             task.lwp_id = lwp_id;
 550             workerThread.execute(task);
 551             return task.result;
 552         }
 553     }
 554 
 555     /** Need to override this to relax alignment checks on x86. */
 556     public long readCInteger(long address, long numBytes, boolean isUnsigned)
 557         throws UnmappedAddressException, UnalignedAddressException {
 558         // Only slightly relaxed semantics -- this is a hack, but is
 559         // necessary on x86 where it seems the compiler is
 560         // putting some global 64-bit data on 32-bit boundaries
 561         if (numBytes == 8) {
 562             utils.checkAlignment(address, 4);
 563         } else {
 564             utils.checkAlignment(address, numBytes);
 565         }
 566         byte[] data = readBytes(address, numBytes);
 567         return utils.dataToCInteger(data, isUnsigned);
 568     }
 569 
 570     // Overridden from DebuggerBase because we need to relax alignment
 571     // constraints on x86
 572     public long readJLong(long address)
 573         throws UnmappedAddressException, UnalignedAddressException {
 574         utils.checkAlignment(address, jintSize);
 575         byte[] data = readBytes(address, jlongSize);
 576         return utils.dataToJLong(data, jlongSize);
 577     }
 578 
 579     //----------------------------------------------------------------------
 580     // Address access. Can not be package private, but should only be
 581     // accessed by the architecture-specific subpackages.
 582 
 583     /** From the LinuxDebugger interface */
 584     public long getAddressValue(Address addr) {
 585       if (addr == null) return 0;
 586       return ((LinuxAddress) addr).getValue();
 587     }
 588 
 589     /** From the LinuxDebugger interface */
 590     public Address newAddress(long value) {
 591       if (value == 0) return null;
 592       return new LinuxAddress(this, value);
 593     }
 594 
 595     /** From the LinuxCDebugger interface */
 596     public List/*<ThreadProxy>*/ getThreadList() {
 597       requireAttach();
 598       return threadList;
 599     }
 600 
 601     /** From the LinuxCDebugger interface */
 602     public List/*<LoadObject>*/ getLoadObjectList() {
 603       requireAttach();
 604       return loadObjectList;
 605     }
 606 
 607     /** From the LinuxCDebugger interface */
 608     public synchronized ClosestSymbol lookup(long addr) {
 609        requireAttach();
 610        if (isCore) {
 611           return lookupByAddress0(addr);
 612        } else {
 613           class LookupByAddressTask implements WorkerThreadTask {
 614              long addr;
 615              ClosestSymbol result;
 616 
 617              public void doit(LinuxDebuggerLocal debugger) {
 618                  result = debugger.lookupByAddress0(addr);
 619              }
 620           }
 621 
 622           LookupByAddressTask task = new LookupByAddressTask();
 623           task.addr = addr;
 624           workerThread.execute(task);
 625           return task.result;
 626        }
 627     }
 628 
 629     public CDebugger getCDebugger() {
 630       if (cdbg == null) {
 631          cdbg = new LinuxCDebugger(this);
 632       }
 633       return cdbg;
 634     }
 635 
 636     /** This reads bytes from the remote process. */
 637     public synchronized ReadResult readBytesFromProcess(long address,
 638             long numBytes) throws UnmappedAddressException, DebuggerException {
 639         requireAttach();
 640         if (isCore) {
 641             byte[] res = readBytesFromProcess0(address, numBytes);
 642             return (res != null)? new ReadResult(res) : new ReadResult(address);
 643         } else {
 644             class ReadBytesFromProcessTask implements WorkerThreadTask {
 645                 long address, numBytes;
 646                 ReadResult result;
 647                 public void doit(LinuxDebuggerLocal debugger) {
 648                     byte[] res = debugger.readBytesFromProcess0(address, numBytes);
 649                     if (res != null)
 650                         result = new ReadResult(res);
 651                     else
 652                         result = new ReadResult(address);
 653                 }
 654             }
 655 
 656             ReadBytesFromProcessTask task = new ReadBytesFromProcessTask();
 657             task.address = address;
 658             task.numBytes = numBytes;
 659             workerThread.execute(task);
 660             return task.result;
 661         }
 662     }
 663 
 664     public void writeBytesToProcess(long address, long numBytes, byte[] data)
 665         throws UnmappedAddressException, DebuggerException {
 666         // FIXME
 667         throw new DebuggerException("Unimplemented");
 668     }
 669 
 670     static {
 671         System.loadLibrary("saproc");
 672         init0();
 673     }
 674 }
--- EOF ---