1 /* $OpenBSD: savecore.c,v 1.46 2009/01/17 13:48:50 miod Exp $ */ 2 /* $NetBSD: savecore.c,v 1.26 1996/03/18 21:16:05 leo Exp $ */ 3 4 /*- 5 * Copyright (c) 1986, 1992, 1993 6 * The Regents of the University of California. All rights reserved. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 1. Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * 2. Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in the 15 * documentation and/or other materials provided with the distribution. 16 * 3. Neither the name of the University nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 21 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 24 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 26 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 27 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 28 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 29 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 30 * SUCH DAMAGE. 31 */ 32 33 #ifndef lint 34 static char copyright[] = 35 "@(#) Copyright (c) 1986, 1992, 1993\n\ 36 The Regents of the University of California. All rights reserved.\n"; 37 #endif /* not lint */ 38 39 #ifndef lint 40 #if 0 41 static char sccsid[] = "@(#)savecore.c 8.3 (Berkeley) 1/2/94"; 42 #else 43 static char rcsid[] = "$OpenBSD: savecore.c,v 1.46 2009/01/17 13:48:50 miod Exp $"; 44 #endif 45 #endif /* not lint */ 46 47 #include <sys/param.h> 48 #include <sys/stat.h> 49 #include <sys/mount.h> 50 #include <sys/syslog.h> 51 #include <sys/types.h> 52 #include <sys/time.h> 53 #include <sys/resource.h> 54 55 #include <dirent.h> 56 #include <errno.h> 57 #include <fcntl.h> 58 #include <nlist.h> 59 #include <paths.h> 60 #include <stdio.h> 61 #include <stdlib.h> 62 #include <string.h> 63 #include <tzfile.h> 64 #include <unistd.h> 65 #include <limits.h> 66 #include <zlib.h> 67 #include <kvm.h> 68 #include <vis.h> 69 70 extern FILE *zopen(const char *fname, const char *mode, int bits); 71 72 #define KREAD(kd, addr, p)\ 73 (kvm_read(kd, addr, (char *)(p), sizeof(*(p))) != sizeof(*(p))) 74 75 struct nlist current_nl[] = { /* Namelist for currently running system. */ 76 #define X_DUMPDEV 0 77 { "_dumpdev" }, 78 #define X_DUMPLO 1 79 { "_dumplo" }, 80 #define X_TIME 2 81 { "_time_second" }, 82 #define X_DUMPSIZE 3 83 { "_dumpsize" }, 84 #define X_VERSION 4 85 { "_version" }, 86 #define X_PANICSTR 5 87 { "_panicstr" }, 88 #define X_DUMPMAG 6 89 { "_dumpmag" }, 90 { NULL }, 91 }; 92 int cursyms[] = { X_DUMPDEV, X_DUMPLO, X_VERSION, X_DUMPMAG, -1 }; 93 int dumpsyms[] = { X_TIME, X_DUMPSIZE, X_VERSION, X_PANICSTR, X_DUMPMAG, -1 }; 94 95 struct nlist dump_nl[] = { /* Name list for dumped system. */ 96 { "_dumpdev" }, /* Entries MUST be the same as */ 97 { "_dumplo" }, /* those in current_nl[]. */ 98 { "_time_second" }, 99 { "_dumpsize" }, 100 { "_version" }, 101 { "_panicstr" }, 102 { "_dumpmag" }, 103 { NULL }, 104 }; 105 106 /* Types match kernel declarations. */ 107 long dumplo; /* where dump starts on dumpdev (in blocks) */ 108 off_t dumpoff; /* where dump starts on dumpdev (in bytes) */ 109 u_long dumpmag; /* magic number in dump */ 110 int dumppages; /* amount of memory dumped (in pages) */ 111 u_long dumpsize; /* amount of memory dumped */ 112 113 char *kernel; 114 char *dirn; /* directory to save dumps in */ 115 char *ddname; /* name of dump device */ 116 dev_t dumpdev; /* dump device */ 117 int dumpfd; /* read/write descriptor on block dev */ 118 kvm_t *kd_dump; /* kvm descriptor on block dev */ 119 time_t now; /* current date */ 120 char panic_mesg[1024]; 121 int panicstr; 122 char vers[1024]; 123 124 int clear, zcompress, force, verbose; /* flags */ 125 126 void check_kmem(void); 127 int check_space(void); 128 void clear_dump(void); 129 int Create(char *, int); 130 int dump_exists(void); 131 char *find_dev(dev_t, int); 132 int get_crashtime(void); 133 void kmem_setup(void); 134 void Lseek(int, off_t, int); 135 int Open(char *, int rw); 136 char *rawname(char *s); 137 void save_core(void); 138 void usage(void); 139 140 int 141 main(int argc, char *argv[]) 142 { 143 struct rlimit rl; 144 int ch; 145 146 openlog("savecore", LOG_PERROR, LOG_DAEMON); 147 148 /* Increase our data size to the max if we can. */ 149 if (getrlimit(RLIMIT_DATA, &rl) == 0) { 150 rl.rlim_cur = rl.rlim_max; 151 if (setrlimit(RLIMIT_DATA, &rl) < 0) 152 syslog(LOG_WARNING, "can't set rlimit data size: %m"); 153 } 154 155 while ((ch = getopt(argc, argv, "cdfN:vz")) != -1) 156 switch(ch) { 157 case 'c': 158 clear = 1; 159 break; 160 case 'd': /* Not documented. */ 161 case 'v': 162 verbose = 1; 163 break; 164 case 'f': 165 force = 1; 166 break; 167 case 'N': 168 kernel = optarg; 169 break; 170 case 'z': 171 zcompress = 1; 172 break; 173 case '?': 174 default: 175 usage(); 176 } 177 argc -= optind; 178 argv += optind; 179 180 if (!clear) { 181 if (argc != 1 && argc != 2) 182 usage(); 183 dirn = argv[0]; 184 } 185 if (argc == 2) 186 kernel = argv[1]; 187 188 (void)time(&now); 189 kmem_setup(); 190 191 if (clear) { 192 clear_dump(); 193 return (0); 194 } 195 196 if (!dump_exists() && !force) 197 return (1); 198 199 check_kmem(); 200 201 if (panicstr) 202 syslog(LOG_ALERT, "reboot after panic: %s", panic_mesg); 203 else 204 syslog(LOG_ALERT, "reboot"); 205 206 if ((!get_crashtime() || !check_space()) && !force) 207 return (1); 208 209 save_core(); 210 211 clear_dump(); 212 return (0); 213 } 214 215 char *dump_sys; 216 217 void 218 kmem_setup(void) 219 { 220 kvm_t *kd_kern; 221 char errbuf[_POSIX2_LINE_MAX]; 222 int i, hdrsz; 223 224 /* 225 * Some names we need for the currently running system, others for 226 * the system that was running when the dump was made. The values 227 * obtained from the current system are used to look for things in 228 * /dev/kmem that cannot be found in the dump_sys namelist, but are 229 * presumed to be the same (since the disk partitions are probably 230 * the same!) 231 */ 232 kd_kern = kvm_openfiles(NULL, NULL, NULL, O_RDONLY, errbuf); 233 if (kd_kern == NULL) { 234 syslog(LOG_ERR, "%s: kvm_openfiles: %s", _PATH_UNIX, errbuf); 235 exit(1); 236 } 237 if (kvm_nlist(kd_kern, current_nl) == -1) 238 syslog(LOG_ERR, "%s: kvm_nlist: %s", _PATH_UNIX, 239 kvm_geterr(kd_kern)); 240 241 for (i = 0; cursyms[i] != -1; i++) 242 if (current_nl[cursyms[i]].n_value == 0) { 243 syslog(LOG_ERR, "%s: %s not in namelist", 244 _PATH_UNIX, current_nl[cursyms[i]].n_name); 245 exit(1); 246 } 247 248 (void)KREAD(kd_kern, current_nl[X_DUMPDEV].n_value, &dumpdev); 249 if (dumpdev == NODEV) { 250 syslog(LOG_WARNING, "no core dump (no dumpdev)"); 251 exit(1); 252 } 253 (void)KREAD(kd_kern, current_nl[X_DUMPLO].n_value, &dumplo); 254 dumpoff = (off_t)dumplo * DEV_BSIZE; 255 if (verbose) 256 (void)printf("dumpoff = %lld (%ld * %d)\n", 257 (long long)dumpoff, dumplo, DEV_BSIZE); 258 (void) KREAD(kd_kern, current_nl[X_DUMPMAG].n_value, &dumpmag); 259 260 if (kernel == NULL) { 261 if (kvm_read(kd_kern, current_nl[X_VERSION].n_value, 262 vers, sizeof(vers)) == -1) { 263 syslog(LOG_ERR, "%s: kvm_read: version misread", _PATH_UNIX); 264 exit(1); 265 } 266 vers[sizeof(vers) - 1] = '\0'; 267 } 268 269 ddname = find_dev(dumpdev, S_IFBLK); 270 dumpfd = Open(ddname, O_RDWR); 271 272 dump_sys = kernel ? kernel : _PATH_UNIX; 273 274 kd_dump = kvm_openfiles(dump_sys, ddname, NULL, O_RDWR, errbuf); 275 if (kd_dump == NULL) { 276 syslog(LOG_ERR, "%s: kvm_openfiles: %s", dump_sys, errbuf); 277 exit(1); 278 } 279 280 if (kvm_nlist(kd_dump, dump_nl) == -1) 281 syslog(LOG_ERR, "%s: kvm_nlist: %s", dump_sys, 282 kvm_geterr(kd_dump)); 283 284 for (i = 0; dumpsyms[i] != -1; i++) 285 if (dump_nl[dumpsyms[i]].n_value == 0) { 286 syslog(LOG_ERR, "%s: %s not in namelist", 287 dump_sys, dump_nl[dumpsyms[i]].n_name); 288 exit(1); 289 } 290 hdrsz = kvm_dump_mkheader(kd_dump, dumpoff); 291 if (hdrsz == -1) { 292 if(verbose) 293 syslog(LOG_ERR, "%s: kvm_dump_mkheader: %s", dump_sys, 294 kvm_geterr(kd_dump)); 295 syslog(LOG_WARNING, "no core dump"); 296 exit(1); 297 } 298 dumpoff += hdrsz; 299 kvm_close(kd_kern); 300 } 301 302 void 303 check_kmem(void) 304 { 305 char *cp; 306 int panicloc; 307 char core_vers[1024]; 308 309 if (kvm_read(kd_dump, dump_nl[X_VERSION].n_value, core_vers, 310 sizeof(core_vers)) != sizeof(core_vers)) { 311 syslog(LOG_ERR, "%s: kvm_read: version misread", dump_sys); 312 exit(1); 313 } 314 core_vers[sizeof(core_vers) - 1] = '\0'; 315 316 if (strcmp(vers, core_vers) && kernel == 0) { 317 vers[strcspn(vers, "\n")] = '\0'; 318 core_vers[strcspn(core_vers, "\n")] = '\0'; 319 320 syslog(LOG_WARNING, 321 "warning: %s version mismatch:\n\t%s\nand\t%s\n", 322 _PATH_UNIX, vers, core_vers); 323 } 324 325 (void)KREAD(kd_dump, dump_nl[X_PANICSTR].n_value, &panicstr); 326 if (panicstr) { 327 char c, visout[5]; 328 size_t vislen; 329 330 cp = panic_mesg; 331 panicloc = panicstr; 332 for (;;) { 333 if (KREAD(kd_dump, panicloc, &c) != 0 || c == '\0') 334 break; 335 panicloc++; 336 337 vis(visout, c, VIS_SAFE|VIS_NOSLASH, 0); 338 vislen = strlen(visout); 339 if (cp - panic_mesg + vislen >= sizeof(panic_mesg)) 340 break; 341 strlcat(cp, visout, 342 panic_mesg + sizeof panic_mesg - cp); 343 cp += strlen(cp); 344 } 345 } 346 } 347 348 int 349 dump_exists(void) 350 { 351 u_long newdumpmag; 352 353 (void)KREAD(kd_dump, dump_nl[X_DUMPMAG].n_value, &newdumpmag); 354 355 /* Read the dump size. */ 356 (void)KREAD(kd_dump, dump_nl[X_DUMPSIZE].n_value, &dumppages); 357 dumpsize = (u_long)dumppages * getpagesize(); 358 359 /* 360 * Return zero if core dump doesn't seem to be there, and note 361 * it for syslog. This check and return happens after the dump size 362 * is read, so dumpsize is whether or not the core is valid (for -f). 363 */ 364 if (newdumpmag != dumpmag) { 365 if (verbose) 366 syslog(LOG_WARNING, 367 "magic number mismatch (%lx != %lx)", 368 newdumpmag, dumpmag); 369 syslog(LOG_WARNING, "no core dump"); 370 return (0); 371 } 372 return (1); 373 } 374 375 void 376 clear_dump(void) 377 { 378 if (kvm_dump_inval(kd_dump) == -1) 379 syslog(LOG_ERR, "%s: kvm_clear_dump: %s", ddname, 380 kvm_geterr(kd_dump)); 381 382 } 383 384 char buf[1024 * 1024]; 385 386 void 387 save_core(void) 388 { 389 FILE *fp; 390 int bounds, ifd, nr, nw, ofd = -1; 391 char *rawp, path[MAXPATHLEN]; 392 mode_t um; 393 394 um = umask(S_IRWXG|S_IRWXO); 395 396 /* 397 * Get the current number and update the bounds file. Do the update 398 * now, because may fail later and don't want to overwrite anything. 399 */ 400 (void)snprintf(path, sizeof(path), "%s/bounds", dirn); 401 if ((fp = fopen(path, "r")) == NULL) 402 goto err1; 403 if (fgets(buf, sizeof(buf), fp) == NULL) { 404 if (ferror(fp)) 405 err1: syslog(LOG_WARNING, "%s: %s", path, strerror(errno)); 406 bounds = 0; 407 } else 408 bounds = atoi(buf); 409 if (fp != NULL) 410 (void)fclose(fp); 411 if ((fp = fopen(path, "w")) == NULL) 412 syslog(LOG_ERR, "%s: %m", path); 413 else { 414 (void)fprintf(fp, "%d\n", bounds + 1); 415 (void)fclose(fp); 416 } 417 418 /* Create the core file. */ 419 (void)snprintf(path, sizeof(path), "%s%s.%d.core%s", 420 dirn, _PATH_UNIX, bounds, zcompress ? ".Z" : ""); 421 if (zcompress) { 422 if ((fp = zopen(path, "w", 0)) == NULL) { 423 syslog(LOG_ERR, "%s: %s", path, strerror(errno)); 424 exit(1); 425 } 426 } else { 427 ofd = Create(path, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); 428 fp = fdopen(ofd, "w"); 429 if (fp == NULL) { 430 syslog(LOG_ERR, "%s: fdopen: %s", path, strerror(errno)); 431 exit(1); 432 } 433 } 434 435 /* Open the raw device. */ 436 rawp = rawname(ddname); 437 if ((ifd = open(rawp, O_RDONLY)) == -1) { 438 syslog(LOG_WARNING, "%s: %m; using block device", rawp); 439 ifd = dumpfd; 440 } 441 442 /* Seek to the start of the core. */ 443 Lseek(ifd, dumpoff, SEEK_SET); 444 445 if (kvm_dump_wrtheader(kd_dump, fp, dumpsize) == -1) { 446 syslog(LOG_ERR, "kvm_dump_wrtheader: %s : %s", path, 447 kvm_geterr(kd_dump)); 448 exit(1); 449 } 450 451 /* Copy the core file. */ 452 syslog(LOG_NOTICE, "writing %score to %s", 453 zcompress ? "compressed " : "", path); 454 for (; dumpsize != 0; dumpsize -= nr) { 455 (void)printf("%8luK\r", dumpsize / 1024); 456 (void)fflush(stdout); 457 nr = read(ifd, buf, MIN(dumpsize, sizeof(buf))); 458 if (nr <= 0) { 459 if (nr == 0) 460 syslog(LOG_WARNING, 461 "WARNING: EOF on dump device"); 462 else 463 syslog(LOG_ERR, "%s: %m", rawp); 464 goto err2; 465 } 466 nw = fwrite(buf, 1, nr, fp); 467 if (nw != nr) { 468 syslog(LOG_ERR, "%s: %s", 469 path, strerror(nw == 0 ? EIO : errno)); 470 err2: syslog(LOG_WARNING, 471 "WARNING: core may be incomplete"); 472 (void)printf("\n"); 473 exit(1); 474 } 475 } 476 (void)close(ifd); 477 (void)fclose(fp); 478 479 /* Copy the kernel. */ 480 ifd = Open(kernel ? kernel : _PATH_UNIX, O_RDONLY); 481 (void)snprintf(path, sizeof(path), "%s%s.%d%s", 482 dirn, _PATH_UNIX, bounds, zcompress ? ".Z" : ""); 483 if (zcompress) { 484 if ((fp = zopen(path, "w", 0)) == NULL) { 485 syslog(LOG_ERR, "%s: %s", path, strerror(errno)); 486 exit(1); 487 } 488 } else 489 ofd = Create(path, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); 490 syslog(LOG_NOTICE, "writing %skernel to %s", 491 zcompress ? "compressed " : "", path); 492 while ((nr = read(ifd, buf, sizeof(buf))) > 0) { 493 if (zcompress) 494 nw = fwrite(buf, 1, nr, fp); 495 else 496 nw = write(ofd, buf, nr); 497 if (nw != nr) { 498 syslog(LOG_ERR, "%s: %s", 499 path, strerror(nw == 0 ? EIO : errno)); 500 syslog(LOG_WARNING, 501 "WARNING: kernel may be incomplete"); 502 exit(1); 503 } 504 } 505 if (nr < 0) { 506 syslog(LOG_ERR, "%s: %s", 507 kernel ? kernel : _PATH_UNIX, strerror(errno)); 508 syslog(LOG_WARNING, 509 "WARNING: kernel may be incomplete"); 510 exit(1); 511 } 512 if (zcompress) 513 (void)fclose(fp); 514 else 515 (void)close(ofd); 516 (void)umask(um); 517 } 518 519 char * 520 find_dev(dev_t dev, int type) 521 { 522 DIR *dfd; 523 struct dirent *dir; 524 struct stat sb; 525 char *dp, devname[MAXPATHLEN]; 526 527 if ((dfd = opendir(_PATH_DEV)) == NULL) { 528 syslog(LOG_ERR, "%s: %s", _PATH_DEV, strerror(errno)); 529 exit(1); 530 } 531 (void)strlcpy(devname, _PATH_DEV, sizeof devname); 532 while ((dir = readdir(dfd))) { 533 (void)strlcpy(devname + sizeof(_PATH_DEV) - 1, dir->d_name, 534 sizeof devname - (sizeof(_PATH_DEV) - 1)); 535 if (lstat(devname, &sb)) { 536 syslog(LOG_ERR, "%s: %s", devname, strerror(errno)); 537 continue; 538 } 539 if ((sb.st_mode & S_IFMT) != type) 540 continue; 541 if (dev == sb.st_rdev) { 542 closedir(dfd); 543 if ((dp = strdup(devname)) == NULL) { 544 syslog(LOG_ERR, "%s", strerror(errno)); 545 exit(1); 546 } 547 return (dp); 548 } 549 } 550 closedir(dfd); 551 syslog(LOG_ERR, "can't find device %d/%d", major(dev), minor(dev)); 552 exit(1); 553 } 554 555 char * 556 rawname(char *s) 557 { 558 char *sl, name[MAXPATHLEN]; 559 560 if ((sl = strrchr(s, '/')) == NULL || sl[1] == '0') { 561 syslog(LOG_ERR, 562 "can't make raw dump device name from %s", s); 563 return (s); 564 } 565 (void)snprintf(name, sizeof(name), "%.*s/r%s", (int)(sl - s), s, sl + 1); 566 if ((sl = strdup(name)) == NULL) { 567 syslog(LOG_ERR, "%s", strerror(errno)); 568 exit(1); 569 } 570 return (sl); 571 } 572 573 int 574 get_crashtime(void) 575 { 576 time_t dumptime; /* Time the dump was taken. */ 577 578 (void)KREAD(kd_dump, dump_nl[X_TIME].n_value, &dumptime); 579 if (dumptime == 0) { 580 if (verbose) 581 syslog(LOG_ERR, "dump time is zero"); 582 return (0); 583 } 584 (void)printf("savecore: system went down at %s", ctime(&dumptime)); 585 #define LEEWAY (7 * SECSPERDAY) 586 if (dumptime < now - LEEWAY || dumptime > now + LEEWAY) { 587 (void)printf("dump time is unreasonable\n"); 588 return (0); 589 } 590 return (1); 591 } 592 593 int 594 check_space(void) 595 { 596 FILE *fp; 597 char *tkernel; 598 off_t minfree, spacefree, kernelsize, needed; 599 struct stat st; 600 struct statfs fsbuf; 601 char buf[100], path[MAXPATHLEN]; 602 int fd; 603 604 tkernel = kernel ? kernel : _PATH_UNIX; 605 if (stat(tkernel, &st) < 0) { 606 syslog(LOG_ERR, "%s: %m", tkernel); 607 exit(1); 608 } 609 kernelsize = st.st_blocks * S_BLKSIZE; 610 if ((fd = open(dirn, O_RDONLY, 0)) < 0 || fstatfs(fd, &fsbuf) < 0) { 611 syslog(LOG_ERR, "%s: %m", dirn); 612 exit(1); 613 } 614 close(fd); 615 spacefree = ((off_t)fsbuf.f_bavail * fsbuf.f_bsize) / 1024; 616 617 (void)snprintf(path, sizeof(path), "%s/minfree", dirn); 618 if ((fp = fopen(path, "r")) == NULL) 619 minfree = 0; 620 else { 621 if (fgets(buf, sizeof(buf), fp) == NULL) 622 minfree = 0; 623 else 624 minfree = atoi(buf); 625 (void)fclose(fp); 626 } 627 628 needed = (dumpsize + kernelsize) / 1024; 629 if (minfree > 0 && spacefree - needed < minfree) { 630 syslog(LOG_WARNING, 631 "no dump, not enough free space on device"); 632 return (0); 633 } 634 if (spacefree - needed < minfree) 635 syslog(LOG_WARNING, 636 "dump performed, but free space threshold crossed"); 637 return (1); 638 } 639 640 int 641 Open(char *name, int rw) 642 { 643 int fd; 644 645 if ((fd = open(name, rw, 0)) < 0) { 646 syslog(LOG_ERR, "%s: %m", name); 647 exit(1); 648 } 649 return (fd); 650 } 651 652 void 653 Lseek(int fd, off_t off, int flag) 654 { 655 off_t ret; 656 657 ret = lseek(fd, off, flag); 658 if (ret == -1) { 659 syslog(LOG_ERR, "lseek: %m"); 660 exit(1); 661 } 662 } 663 664 int 665 Create(char *file, int mode) 666 { 667 int fd; 668 669 fd = open(file, O_WRONLY | O_CREAT | O_TRUNC, mode); 670 if (fd < 0) { 671 syslog(LOG_ERR, "%s: %m", file); 672 exit(1); 673 } 674 return (fd); 675 } 676 677 void 678 usage(void) 679 { 680 extern char *__progname; 681 fprintf(stderr, "usage: %s [-cfvz] [-N system] directory\n", 682 __progname); 683 exit(1); 684 } 685