| // Copyright 2009 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // go-specific code shared across loaders (5l, 6l, 8l). |
| |
| #include "l.h" |
| #include "../ld/lib.h" |
| |
| // accumulate all type information from .6 files. |
| // check for inconsistencies. |
| |
| // TODO: |
| // generate debugging section in binary. |
| // once the dust settles, try to move some code to |
| // libmach, so that other linkers and ar can share. |
| |
| /* |
| * package import data |
| */ |
| typedef struct Import Import; |
| struct Import |
| { |
| Import *hash; // next in hash table |
| char *prefix; // "type", "var", "func", "const" |
| char *name; |
| char *def; |
| char *file; |
| }; |
| enum { |
| NIHASH = 1024 |
| }; |
| static Import *ihash[NIHASH]; |
| static int nimport; |
| static void imported(char *pkg, char *import); |
| |
| static int |
| hashstr(char *name) |
| { |
| uint32 h; |
| char *cp; |
| |
| h = 0; |
| for(cp = name; *cp; h += *cp++) |
| h *= 1119; |
| h &= 0xffffff; |
| return h; |
| } |
| |
| static Import * |
| ilookup(char *name) |
| { |
| int h; |
| Import *x; |
| |
| h = hashstr(name) % NIHASH; |
| for(x=ihash[h]; x; x=x->hash) |
| if(x->name[0] == name[0] && strcmp(x->name, name) == 0) |
| return x; |
| x = mal(sizeof *x); |
| x->name = estrdup(name); |
| x->hash = ihash[h]; |
| ihash[h] = x; |
| nimport++; |
| return x; |
| } |
| |
| static void loadpkgdata(char*, char*, char*, int); |
| static void loadcgo(char*, char*, char*, int); |
| static int parsemethod(char**, char*, char**); |
| static int parsepkgdata(char*, char*, char**, char*, char**, char**, char**); |
| |
| void |
| ldpkg(Biobuf *f, char *pkg, int64 len, char *filename, int whence) |
| { |
| char *data, *p0, *p1, *name; |
| |
| if(debug['g']) |
| return; |
| |
| if((int)len != len) { |
| fprint(2, "%s: too much pkg data in %s\n", argv0, filename); |
| if(debug['u']) |
| errorexit(); |
| return; |
| } |
| data = mal(len+1); |
| if(Bread(f, data, len) != len) { |
| fprint(2, "%s: short pkg read %s\n", argv0, filename); |
| if(debug['u']) |
| errorexit(); |
| return; |
| } |
| data[len] = '\0'; |
| |
| // first \n$$ marks beginning of exports - skip rest of line |
| p0 = strstr(data, "\n$$"); |
| if(p0 == nil) { |
| if(debug['u'] && whence != ArchiveObj) { |
| fprint(2, "%s: cannot find export data in %s\n", argv0, filename); |
| errorexit(); |
| } |
| return; |
| } |
| p0 += 3; |
| while(*p0 != '\n' && *p0 != '\0') |
| p0++; |
| |
| // second marks end of exports / beginning of local data |
| p1 = strstr(p0, "\n$$"); |
| if(p1 == nil) { |
| fprint(2, "%s: cannot find end of exports in %s\n", argv0, filename); |
| if(debug['u']) |
| errorexit(); |
| return; |
| } |
| while(p0 < p1 && (*p0 == ' ' || *p0 == '\t' || *p0 == '\n')) |
| p0++; |
| if(p0 < p1) { |
| if(strncmp(p0, "package ", 8) != 0) { |
| fprint(2, "%s: bad package section in %s - %s\n", argv0, filename, p0); |
| if(debug['u']) |
| errorexit(); |
| return; |
| } |
| p0 += 8; |
| while(p0 < p1 && (*p0 == ' ' || *p0 == '\t' || *p0 == '\n')) |
| p0++; |
| name = p0; |
| while(p0 < p1 && *p0 != ' ' && *p0 != '\t' && *p0 != '\n') |
| p0++; |
| if(debug['u'] && whence != ArchiveObj && |
| (p0+6 > p1 || memcmp(p0, " safe\n", 6) != 0)) { |
| fprint(2, "%s: load of unsafe package %s\n", argv0, filename); |
| nerrors++; |
| errorexit(); |
| } |
| if(p0 < p1) { |
| if(*p0 == '\n') |
| *p0++ = '\0'; |
| else { |
| *p0++ = '\0'; |
| while(p0 < p1 && *p0++ != '\n') |
| ; |
| } |
| } |
| if(strcmp(pkg, "main") == 0 && strcmp(name, "main") != 0) { |
| fprint(2, "%s: %s: not package main (package %s)\n", argv0, filename, name); |
| nerrors++; |
| errorexit(); |
| } |
| loadpkgdata(filename, pkg, p0, p1 - p0); |
| } |
| |
| // __.PKGDEF has no cgo section - those are in the C compiler-generated object files. |
| if(whence == Pkgdef) |
| return; |
| |
| // look for cgo section |
| p0 = strstr(p1, "\n$$ // cgo"); |
| if(p0 != nil) { |
| p0 = strchr(p0+1, '\n'); |
| if(p0 == nil) { |
| fprint(2, "%s: found $$ // cgo but no newline in %s\n", argv0, filename); |
| if(debug['u']) |
| errorexit(); |
| return; |
| } |
| p1 = strstr(p0, "\n$$"); |
| if(p1 == nil) |
| p1 = strstr(p0, "\n!\n"); |
| if(p1 == nil) { |
| fprint(2, "%s: cannot find end of // cgo section in %s\n", argv0, filename); |
| if(debug['u']) |
| errorexit(); |
| return; |
| } |
| loadcgo(filename, pkg, p0 + 1, p1 - (p0+1)); |
| } |
| } |
| |
| static void |
| loadpkgdata(char *file, char *pkg, char *data, int len) |
| { |
| char *p, *ep, *prefix, *name, *def; |
| Import *x; |
| |
| file = estrdup(file); |
| p = data; |
| ep = data + len; |
| while(parsepkgdata(file, pkg, &p, ep, &prefix, &name, &def) > 0) { |
| x = ilookup(name); |
| if(x->prefix == nil) { |
| x->prefix = prefix; |
| x->def = estrdup(def); |
| x->file = file; |
| } else if(strcmp(x->prefix, prefix) != 0) { |
| fprint(2, "%s: conflicting definitions for %s\n", argv0, name); |
| fprint(2, "%s:\t%s %s ...\n", x->file, x->prefix, name); |
| fprint(2, "%s:\t%s %s ...\n", file, prefix, name); |
| nerrors++; |
| } else if(strcmp(x->def, def) != 0) { |
| fprint(2, "%s: conflicting definitions for %s\n", argv0, name); |
| fprint(2, "%s:\t%s %s %s\n", x->file, x->prefix, name, x->def); |
| fprint(2, "%s:\t%s %s %s\n", file, prefix, name, def); |
| nerrors++; |
| } |
| free(name); |
| free(def); |
| } |
| free(file); |
| } |
| |
| static int |
| parsepkgdata(char *file, char *pkg, char **pp, char *ep, char **prefixp, char **namep, char **defp) |
| { |
| char *p, *prefix, *name, *def, *edef, *meth; |
| int n, inquote; |
| |
| // skip white space |
| p = *pp; |
| loop: |
| while(p < ep && (*p == ' ' || *p == '\t' || *p == '\n')) |
| p++; |
| if(p == ep || strncmp(p, "$$\n", 3) == 0) |
| return 0; |
| |
| // prefix: (var|type|func|const) |
| prefix = p; |
| if(p + 7 > ep) |
| return -1; |
| if(strncmp(p, "var ", 4) == 0) |
| p += 4; |
| else if(strncmp(p, "type ", 5) == 0) |
| p += 5; |
| else if(strncmp(p, "func ", 5) == 0) |
| p += 5; |
| else if(strncmp(p, "const ", 6) == 0) |
| p += 6; |
| else if(strncmp(p, "import ", 7) == 0) { |
| p += 7; |
| while(p < ep && *p != ' ') |
| p++; |
| p++; |
| name = p; |
| while(p < ep && *p != '\n') |
| p++; |
| if(p >= ep) { |
| fprint(2, "%s: %s: confused in import line\n", argv0, file); |
| nerrors++; |
| return -1; |
| } |
| *p++ = '\0'; |
| imported(pkg, name); |
| goto loop; |
| } |
| else { |
| fprint(2, "%s: %s: confused in pkg data near <<%.40s>>\n", argv0, file, prefix); |
| nerrors++; |
| return -1; |
| } |
| p[-1] = '\0'; |
| |
| // name: a.b followed by space |
| name = p; |
| inquote = 0; |
| while(p < ep) { |
| if (*p == ' ' && !inquote) |
| break; |
| |
| if(*p == '\\') |
| p++; |
| else if(*p == '"') |
| inquote = !inquote; |
| |
| p++; |
| } |
| |
| if(p >= ep) |
| return -1; |
| *p++ = '\0'; |
| |
| // def: free form to new line |
| def = p; |
| while(p < ep && *p != '\n') |
| p++; |
| if(p >= ep) |
| return -1; |
| edef = p; |
| *p++ = '\0'; |
| |
| // include methods on successive lines in def of named type |
| while(parsemethod(&p, ep, &meth) > 0) { |
| *edef++ = '\n'; // overwrites '\0' |
| if(edef+1 > meth) { |
| // We want to indent methods with a single \t. |
| // 6g puts at least one char of indent before all method defs, |
| // so there will be room for the \t. If the method def wasn't |
| // indented we could do something more complicated, |
| // but for now just diagnose the problem and assume |
| // 6g will keep indenting for us. |
| fprint(2, "%s: %s: expected methods to be indented %p %p %.10s\n", argv0, |
| file, edef, meth, meth); |
| nerrors++; |
| return -1; |
| } |
| *edef++ = '\t'; |
| n = strlen(meth); |
| memmove(edef, meth, n); |
| edef += n; |
| } |
| |
| name = expandpkg(name, pkg); |
| def = expandpkg(def, pkg); |
| |
| // done |
| *pp = p; |
| *prefixp = prefix; |
| *namep = name; |
| *defp = def; |
| return 1; |
| } |
| |
| static int |
| parsemethod(char **pp, char *ep, char **methp) |
| { |
| char *p; |
| |
| // skip white space |
| p = *pp; |
| while(p < ep && (*p == ' ' || *p == '\t')) |
| p++; |
| if(p == ep) |
| return 0; |
| |
| // might be a comment about the method |
| if(p + 2 < ep && strncmp(p, "//", 2) == 0) |
| goto useline; |
| |
| // if it says "func (", it's a method |
| if(p + 6 < ep && strncmp(p, "func (", 6) == 0) |
| goto useline; |
| return 0; |
| |
| useline: |
| // definition to end of line |
| *methp = p; |
| while(p < ep && *p != '\n') |
| p++; |
| if(p >= ep) { |
| fprint(2, "%s: lost end of line in method definition\n", argv0); |
| *pp = ep; |
| return -1; |
| } |
| *p++ = '\0'; |
| *pp = p; |
| return 1; |
| } |
| |
| static void |
| loadcgo(char *file, char *pkg, char *p, int n) |
| { |
| char *pend, *next, *p0, *q; |
| char *f[10], *local, *remote, *lib; |
| int nf; |
| LSym *s; |
| |
| USED(file); |
| pend = p + n; |
| p0 = nil; |
| for(; p<pend; p=next) { |
| next = strchr(p, '\n'); |
| if(next == nil) |
| next = ""; |
| else |
| *next++ = '\0'; |
| |
| free(p0); |
| p0 = estrdup(p); // save for error message |
| nf = tokenize(p, f, nelem(f)); |
| |
| if(strcmp(f[0], "cgo_import_dynamic") == 0) { |
| if(nf < 2 || nf > 4) |
| goto err; |
| |
| local = f[1]; |
| remote = local; |
| if(nf > 2) |
| remote = f[2]; |
| lib = ""; |
| if(nf > 3) |
| lib = f[3]; |
| |
| if(debug['d']) { |
| fprint(2, "%s: %s: cannot use dynamic imports with -d flag\n", argv0, file); |
| nerrors++; |
| return; |
| } |
| |
| if(strcmp(local, "_") == 0 && strcmp(remote, "_") == 0) { |
| // allow #pragma dynimport _ _ "foo.so" |
| // to force a link of foo.so. |
| havedynamic = 1; |
| adddynlib(lib); |
| continue; |
| } |
| |
| local = expandpkg(local, pkg); |
| q = strchr(remote, '#'); |
| if(q) |
| *q++ = '\0'; |
| s = linklookup(ctxt, local, 0); |
| if(local != f[1]) |
| free(local); |
| if(s->type == 0 || s->type == SXREF || s->type == SHOSTOBJ) { |
| s->dynimplib = lib; |
| s->extname = remote; |
| s->dynimpvers = q; |
| if(s->type != SHOSTOBJ) |
| s->type = SDYNIMPORT; |
| havedynamic = 1; |
| } |
| continue; |
| } |
| |
| if(strcmp(f[0], "cgo_import_static") == 0) { |
| if(nf != 2) |
| goto err; |
| local = f[1]; |
| s = linklookup(ctxt, local, 0); |
| s->type = SHOSTOBJ; |
| s->size = 0; |
| continue; |
| } |
| |
| if(strcmp(f[0], "cgo_export_static") == 0 || strcmp(f[0], "cgo_export_dynamic") == 0) { |
| // TODO: Remove once we know Windows is okay. |
| if(strcmp(f[0], "cgo_export_static") == 0 && HEADTYPE == Hwindows) |
| continue; |
| |
| if(nf < 2 || nf > 3) |
| goto err; |
| local = f[1]; |
| if(nf > 2) |
| remote = f[2]; |
| else |
| remote = local; |
| local = expandpkg(local, pkg); |
| s = linklookup(ctxt, local, 0); |
| |
| if(flag_shared && s == linklookup(ctxt, "main", 0)) |
| continue; |
| |
| // export overrides import, for openbsd/cgo. |
| // see issue 4878. |
| if(s->dynimplib != nil) { |
| s->dynimplib = nil; |
| s->extname = nil; |
| s->dynimpvers = nil; |
| s->type = 0; |
| } |
| |
| if(s->cgoexport == 0) { |
| s->extname = remote; |
| if(ndynexp%32 == 0) |
| dynexp = erealloc(dynexp, (ndynexp+32)*sizeof dynexp[0]); |
| dynexp[ndynexp++] = s; |
| } else if(strcmp(s->extname, remote) != 0) { |
| fprint(2, "%s: conflicting cgo_export directives: %s as %s and %s\n", argv0, s->name, s->extname, remote); |
| nerrors++; |
| return; |
| } |
| if(strcmp(f[0], "cgo_export_static") == 0) |
| s->cgoexport |= CgoExportStatic; |
| else |
| s->cgoexport |= CgoExportDynamic; |
| if(local != f[1]) |
| free(local); |
| continue; |
| } |
| |
| if(strcmp(f[0], "cgo_dynamic_linker") == 0) { |
| if(nf != 2) |
| goto err; |
| |
| if(!debug['I']) { // not overridden by command line |
| if(interpreter != nil && strcmp(interpreter, f[1]) != 0) { |
| fprint(2, "%s: conflict dynlinker: %s and %s\n", argv0, interpreter, f[1]); |
| nerrors++; |
| return; |
| } |
| free(interpreter); |
| interpreter = estrdup(f[1]); |
| } |
| continue; |
| } |
| |
| if(strcmp(f[0], "cgo_ldflag") == 0) { |
| if(nf != 2) |
| goto err; |
| if(nldflag%32 == 0) |
| ldflag = erealloc(ldflag, (nldflag+32)*sizeof ldflag[0]); |
| ldflag[nldflag++] = estrdup(f[1]); |
| continue; |
| } |
| } |
| free(p0); |
| return; |
| |
| err: |
| fprint(2, "%s: %s: invalid dynimport line: %s\n", argv0, file, p0); |
| nerrors++; |
| } |
| |
| static LSym *markq; |
| static LSym *emarkq; |
| |
| static void |
| mark1(LSym *s, LSym *parent) |
| { |
| if(s == S || s->reachable) |
| return; |
| if(strncmp(s->name, "go.weak.", 8) == 0) |
| return; |
| s->reachable = 1; |
| s->reachparent = parent; |
| if(markq == nil) |
| markq = s; |
| else |
| emarkq->queue = s; |
| emarkq = s; |
| } |
| |
| void |
| mark(LSym *s) |
| { |
| mark1(s, nil); |
| } |
| |
| static void |
| markflood(void) |
| { |
| Auto *a; |
| LSym *s; |
| int i; |
| |
| for(s=markq; s!=S; s=s->queue) { |
| if(s->type == STEXT) { |
| if(debug['v'] > 1) |
| Bprint(&bso, "marktext %s\n", s->name); |
| for(a=s->autom; a; a=a->link) |
| mark1(a->gotype, s); |
| } |
| for(i=0; i<s->nr; i++) |
| mark1(s->r[i].sym, s); |
| if(s->pcln) { |
| for(i=0; i<s->pcln->nfuncdata; i++) |
| mark1(s->pcln->funcdata[i], s); |
| } |
| mark1(s->gotype, s); |
| mark1(s->sub, s); |
| mark1(s->outer, s); |
| } |
| } |
| |
| static char* |
| markextra[] = |
| { |
| "runtime.morestack", |
| "runtime.morestackx", |
| |
| "runtime.morestack00", |
| "runtime.morestack10", |
| "runtime.morestack01", |
| "runtime.morestack11", |
| |
| "runtime.morestack8", |
| "runtime.morestack16", |
| "runtime.morestack24", |
| "runtime.morestack32", |
| "runtime.morestack40", |
| "runtime.morestack48", |
| |
| // on arm, lock in the div/mod helpers too |
| "_div", |
| "_divu", |
| "_mod", |
| "_modu", |
| }; |
| |
| void |
| deadcode(void) |
| { |
| int i; |
| LSym *s, *last, *p; |
| Fmt fmt; |
| |
| if(debug['v']) |
| Bprint(&bso, "%5.2f deadcode\n", cputime()); |
| |
| mark(linklookup(ctxt, INITENTRY, 0)); |
| for(i=0; i<nelem(markextra); i++) |
| mark(linklookup(ctxt, markextra[i], 0)); |
| |
| for(i=0; i<ndynexp; i++) |
| mark(dynexp[i]); |
| |
| markflood(); |
| |
| // keep each beginning with 'typelink.' if the symbol it points at is being kept. |
| for(s = ctxt->allsym; s != S; s = s->allsym) { |
| if(strncmp(s->name, "go.typelink.", 12) == 0) |
| s->reachable = s->nr==1 && s->r[0].sym->reachable; |
| } |
| |
| // remove dead text but keep file information (z symbols). |
| last = nil; |
| for(s = ctxt->textp; s != nil; s = s->next) { |
| if(!s->reachable) |
| continue; |
| // NOTE: Removing s from old textp and adding to new, shorter textp. |
| if(last == nil) |
| ctxt->textp = s; |
| else |
| last->next = s; |
| last = s; |
| } |
| if(last == nil) |
| ctxt->textp = nil; |
| else |
| last->next = nil; |
| |
| for(s = ctxt->allsym; s != S; s = s->allsym) |
| if(strncmp(s->name, "go.weak.", 8) == 0) { |
| s->special = 1; // do not lay out in data segment |
| s->reachable = 1; |
| s->hide = 1; |
| } |
| |
| // record field tracking references |
| fmtstrinit(&fmt); |
| for(s = ctxt->allsym; s != S; s = s->allsym) { |
| if(strncmp(s->name, "go.track.", 9) == 0) { |
| s->special = 1; // do not lay out in data segment |
| s->hide = 1; |
| if(s->reachable) { |
| fmtprint(&fmt, "%s", s->name+9); |
| for(p=s->reachparent; p; p=p->reachparent) |
| fmtprint(&fmt, "\t%s", p->name); |
| fmtprint(&fmt, "\n"); |
| } |
| s->type = SCONST; |
| s->value = 0; |
| } |
| } |
| if(tracksym == nil) |
| return; |
| s = linklookup(ctxt, tracksym, 0); |
| if(!s->reachable) |
| return; |
| addstrdata(tracksym, fmtstrflush(&fmt)); |
| } |
| |
| void |
| doweak(void) |
| { |
| LSym *s, *t; |
| |
| // resolve weak references only if |
| // target symbol will be in binary anyway. |
| for(s = ctxt->allsym; s != S; s = s->allsym) { |
| if(strncmp(s->name, "go.weak.", 8) == 0) { |
| t = linkrlookup(ctxt, s->name+8, s->version); |
| if(t && t->type != 0 && t->reachable) { |
| s->value = t->value; |
| s->type = t->type; |
| s->outer = t; |
| } else { |
| s->type = SCONST; |
| s->value = 0; |
| } |
| continue; |
| } |
| } |
| } |
| |
| void |
| addexport(void) |
| { |
| int i; |
| |
| if(HEADTYPE == Hdarwin) |
| return; |
| |
| for(i=0; i<ndynexp; i++) |
| adddynsym(ctxt, dynexp[i]); |
| } |
| |
| /* %Z from gc, for quoting import paths */ |
| int |
| Zconv(Fmt *fp) |
| { |
| Rune r; |
| char *s, *se; |
| int n; |
| |
| s = va_arg(fp->args, char*); |
| if(s == nil) |
| return fmtstrcpy(fp, "<nil>"); |
| |
| se = s + strlen(s); |
| |
| // NOTE: Keep in sync with ../gc/go.c:/^Zconv. |
| while(s < se) { |
| n = chartorune(&r, s); |
| s += n; |
| switch(r) { |
| case Runeerror: |
| if(n == 1) { |
| fmtprint(fp, "\\x%02x", (uchar)*(s-1)); |
| break; |
| } |
| // fall through |
| default: |
| if(r < ' ') { |
| fmtprint(fp, "\\x%02x", r); |
| break; |
| } |
| fmtrune(fp, r); |
| break; |
| case '\t': |
| fmtstrcpy(fp, "\\t"); |
| break; |
| case '\n': |
| fmtstrcpy(fp, "\\n"); |
| break; |
| case '\"': |
| case '\\': |
| fmtrune(fp, '\\'); |
| fmtrune(fp, r); |
| break; |
| case 0xFEFF: // BOM, basically disallowed in source code |
| fmtstrcpy(fp, "\\uFEFF"); |
| break; |
| } |
| } |
| return 0; |
| } |
| |
| |
| typedef struct Pkg Pkg; |
| struct Pkg |
| { |
| uchar mark; |
| uchar checked; |
| Pkg *next; |
| char *path; |
| Pkg **impby; |
| int nimpby; |
| int mimpby; |
| Pkg *all; |
| }; |
| |
| static Pkg *phash[1024]; |
| static Pkg *pkgall; |
| |
| static Pkg* |
| getpkg(char *path) |
| { |
| Pkg *p; |
| int h; |
| |
| h = hashstr(path) % nelem(phash); |
| for(p=phash[h]; p; p=p->next) |
| if(strcmp(p->path, path) == 0) |
| return p; |
| p = mal(sizeof *p); |
| p->path = estrdup(path); |
| p->next = phash[h]; |
| phash[h] = p; |
| p->all = pkgall; |
| pkgall = p; |
| return p; |
| } |
| |
| static void |
| imported(char *pkg, char *import) |
| { |
| Pkg *p, *i; |
| |
| // everyone imports runtime, even runtime. |
| if(strcmp(import, "\"runtime\"") == 0) |
| return; |
| |
| pkg = smprint("\"%Z\"", pkg); // turn pkg path into quoted form, freed below |
| p = getpkg(pkg); |
| i = getpkg(import); |
| if(i->nimpby >= i->mimpby) { |
| i->mimpby *= 2; |
| if(i->mimpby == 0) |
| i->mimpby = 16; |
| i->impby = erealloc(i->impby, i->mimpby*sizeof i->impby[0]); |
| } |
| i->impby[i->nimpby++] = p; |
| free(pkg); |
| } |
| |
| static Pkg* |
| cycle(Pkg *p) |
| { |
| int i; |
| Pkg *bad; |
| |
| if(p->checked) |
| return 0; |
| |
| if(p->mark) { |
| nerrors++; |
| print("import cycle:\n"); |
| print("\t%s\n", p->path); |
| return p; |
| } |
| p->mark = 1; |
| for(i=0; i<p->nimpby; i++) { |
| if((bad = cycle(p->impby[i])) != nil) { |
| p->mark = 0; |
| p->checked = 1; |
| print("\timports %s\n", p->path); |
| if(bad == p) |
| return nil; |
| return bad; |
| } |
| } |
| p->checked = 1; |
| p->mark = 0; |
| return 0; |
| } |
| |
| void |
| importcycles(void) |
| { |
| Pkg *p; |
| |
| for(p=pkgall; p; p=p->all) |
| cycle(p); |
| } |
| |
| void |
| setlinkmode(char *arg) |
| { |
| if(strcmp(arg, "internal") == 0) |
| linkmode = LinkInternal; |
| else if(strcmp(arg, "external") == 0) |
| linkmode = LinkExternal; |
| else if(strcmp(arg, "auto") == 0) |
| linkmode = LinkAuto; |
| else { |
| fprint(2, "unknown link mode -linkmode %s\n", arg); |
| errorexit(); |
| } |
| } |