github.com/gagliardetto/golang-go@v0.0.0-20201020153340-53909ea70814/cmd/link/internal/ld/pcln.go (about)

     1  // Copyright 2013 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package ld
     6  
     7  import (
     8  	"github.com/gagliardetto/golang-go/cmd/internal/obj"
     9  	"github.com/gagliardetto/golang-go/cmd/internal/objabi"
    10  	"github.com/gagliardetto/golang-go/cmd/internal/src"
    11  	"github.com/gagliardetto/golang-go/cmd/internal/sys"
    12  	"github.com/gagliardetto/golang-go/cmd/link/internal/sym"
    13  	"encoding/binary"
    14  	"fmt"
    15  	"log"
    16  	"os"
    17  	"path/filepath"
    18  	"strings"
    19  )
    20  
    21  func ftabaddstring(ftab *sym.Symbol, s string) int32 {
    22  	start := len(ftab.P)
    23  	ftab.Grow(int64(start + len(s) + 1)) // make room for s plus trailing NUL
    24  	copy(ftab.P[start:], s)
    25  	return int32(start)
    26  }
    27  
    28  // numberfile assigns a file number to the file if it hasn't been assigned already.
    29  func numberfile(ctxt *Link, file *sym.Symbol) {
    30  	if file.Type != sym.SFILEPATH {
    31  		ctxt.Filesyms = append(ctxt.Filesyms, file)
    32  		file.Value = int64(len(ctxt.Filesyms))
    33  		file.Type = sym.SFILEPATH
    34  		path := file.Name[len(src.FileSymPrefix):]
    35  		file.Name = expandGoroot(path)
    36  	}
    37  }
    38  
    39  func renumberfiles(ctxt *Link, files []*sym.Symbol, d *sym.Pcdata) {
    40  	// Give files numbers.
    41  	for _, f := range files {
    42  		numberfile(ctxt, f)
    43  	}
    44  
    45  	buf := make([]byte, binary.MaxVarintLen32)
    46  	newval := int32(-1)
    47  	var out sym.Pcdata
    48  	it := obj.NewPCIter(uint32(ctxt.Arch.MinLC))
    49  	for it.Init(d.P); !it.Done; it.Next() {
    50  		// value delta
    51  		oldval := it.Value
    52  
    53  		var val int32
    54  		if oldval == -1 {
    55  			val = -1
    56  		} else {
    57  			if oldval < 0 || oldval >= int32(len(files)) {
    58  				log.Fatalf("bad pcdata %d", oldval)
    59  			}
    60  			val = int32(files[oldval].Value)
    61  		}
    62  
    63  		dv := val - newval
    64  		newval = val
    65  
    66  		// value
    67  		n := binary.PutVarint(buf, int64(dv))
    68  		out.P = append(out.P, buf[:n]...)
    69  
    70  		// pc delta
    71  		pc := (it.NextPC - it.PC) / it.PCScale
    72  		n = binary.PutUvarint(buf, uint64(pc))
    73  		out.P = append(out.P, buf[:n]...)
    74  	}
    75  
    76  	// terminating value delta
    77  	// we want to write varint-encoded 0, which is just 0
    78  	out.P = append(out.P, 0)
    79  
    80  	*d = out
    81  }
    82  
    83  // onlycsymbol reports whether this is a symbol that is referenced by C code.
    84  func onlycsymbol(s *sym.Symbol) bool {
    85  	switch s.Name {
    86  	case "_cgo_topofstack", "__cgo_topofstack", "_cgo_panic", "crosscall2":
    87  		return true
    88  	}
    89  	if strings.HasPrefix(s.Name, "_cgoexp_") {
    90  		return true
    91  	}
    92  	return false
    93  }
    94  
    95  func emitPcln(ctxt *Link, s *sym.Symbol) bool {
    96  	if s == nil {
    97  		return true
    98  	}
    99  	if ctxt.BuildMode == BuildModePlugin && ctxt.HeadType == objabi.Hdarwin && onlycsymbol(s) {
   100  		return false
   101  	}
   102  	// We want to generate func table entries only for the "lowest level" symbols,
   103  	// not containers of subsymbols.
   104  	return !s.Attr.Container()
   105  }
   106  
   107  // pclntab initializes the pclntab symbol with
   108  // runtime function and file name information.
   109  
   110  var pclntabZpcln sym.FuncInfo
   111  
   112  // These variables are used to initialize runtime.firstmoduledata, see symtab.go:symtab.
   113  var pclntabNfunc int32
   114  var pclntabFiletabOffset int32
   115  var pclntabPclntabOffset int32
   116  var pclntabFirstFunc *sym.Symbol
   117  var pclntabLastFunc *sym.Symbol
   118  
   119  func (ctxt *Link) pclntab() {
   120  	funcdataBytes := int64(0)
   121  	ftab := ctxt.Syms.Lookup("runtime.pclntab", 0)
   122  	ftab.Type = sym.SPCLNTAB
   123  	ftab.Attr |= sym.AttrReachable
   124  
   125  	// See golang.org/s/go12symtab for the format. Briefly:
   126  	//	8-byte header
   127  	//	nfunc [thearch.ptrsize bytes]
   128  	//	function table, alternating PC and offset to func struct [each entry thearch.ptrsize bytes]
   129  	//	end PC [thearch.ptrsize bytes]
   130  	//	offset to file table [4 bytes]
   131  
   132  	// Find container symbols and mark them as such.
   133  	for _, s := range ctxt.Textp {
   134  		if s.Outer != nil {
   135  			s.Outer.Attr |= sym.AttrContainer
   136  		}
   137  	}
   138  
   139  	// Gather some basic stats and info.
   140  	var nfunc int32
   141  	prevSect := ctxt.Textp[0].Sect
   142  	for _, s := range ctxt.Textp {
   143  		if !emitPcln(ctxt, s) {
   144  			continue
   145  		}
   146  		nfunc++
   147  		if pclntabFirstFunc == nil {
   148  			pclntabFirstFunc = s
   149  		}
   150  		if s.Sect != prevSect {
   151  			// With multiple text sections, the external linker may insert functions
   152  			// between the sections, which are not known by Go. This leaves holes in
   153  			// the PC range covered by the func table. We need to generate an entry
   154  			// to mark the hole.
   155  			nfunc++
   156  			prevSect = s.Sect
   157  		}
   158  	}
   159  
   160  	pclntabNfunc = nfunc
   161  	ftab.Grow(8 + int64(ctxt.Arch.PtrSize) + int64(nfunc)*2*int64(ctxt.Arch.PtrSize) + int64(ctxt.Arch.PtrSize) + 4)
   162  	ftab.SetUint32(ctxt.Arch, 0, 0xfffffffb)
   163  	ftab.SetUint8(ctxt.Arch, 6, uint8(ctxt.Arch.MinLC))
   164  	ftab.SetUint8(ctxt.Arch, 7, uint8(ctxt.Arch.PtrSize))
   165  	ftab.SetUint(ctxt.Arch, 8, uint64(nfunc))
   166  	pclntabPclntabOffset = int32(8 + ctxt.Arch.PtrSize)
   167  
   168  	funcnameoff := make(map[string]int32)
   169  	nameToOffset := func(name string) int32 {
   170  		nameoff, ok := funcnameoff[name]
   171  		if !ok {
   172  			nameoff = ftabaddstring(ftab, name)
   173  			funcnameoff[name] = nameoff
   174  		}
   175  		return nameoff
   176  	}
   177  
   178  	pctaboff := make(map[string]uint32)
   179  	writepctab := func(off int32, p []byte) int32 {
   180  		start, ok := pctaboff[string(p)]
   181  		if !ok {
   182  			if len(p) > 0 {
   183  				start = uint32(len(ftab.P))
   184  				ftab.AddBytes(p)
   185  			}
   186  			pctaboff[string(p)] = start
   187  		}
   188  		newoff := int32(ftab.SetUint32(ctxt.Arch, int64(off), start))
   189  		return newoff
   190  	}
   191  
   192  	nfunc = 0 // repurpose nfunc as a running index
   193  	prevFunc := ctxt.Textp[0]
   194  	for _, s := range ctxt.Textp {
   195  		if !emitPcln(ctxt, s) {
   196  			continue
   197  		}
   198  
   199  		if s.Sect != prevFunc.Sect {
   200  			// With multiple text sections, there may be a hole here in the address
   201  			// space (see the comment above). We use an invalid funcoff value to
   202  			// mark the hole.
   203  			// See also runtime/symtab.go:findfunc
   204  			ftab.SetAddrPlus(ctxt.Arch, 8+int64(ctxt.Arch.PtrSize)+int64(nfunc)*2*int64(ctxt.Arch.PtrSize), prevFunc, prevFunc.Size)
   205  			ftab.SetUint(ctxt.Arch, 8+int64(ctxt.Arch.PtrSize)+int64(nfunc)*2*int64(ctxt.Arch.PtrSize)+int64(ctxt.Arch.PtrSize), ^uint64(0))
   206  			nfunc++
   207  		}
   208  		prevFunc = s
   209  
   210  		pcln := s.FuncInfo
   211  		if pcln == nil {
   212  			pcln = &pclntabZpcln
   213  		}
   214  
   215  		if len(pcln.InlTree) > 0 {
   216  			if len(pcln.Pcdata) <= objabi.PCDATA_InlTreeIndex {
   217  				// Create inlining pcdata table.
   218  				pcdata := make([]sym.Pcdata, objabi.PCDATA_InlTreeIndex+1)
   219  				copy(pcdata, pcln.Pcdata)
   220  				pcln.Pcdata = pcdata
   221  			}
   222  
   223  			if len(pcln.Funcdataoff) <= objabi.FUNCDATA_InlTree {
   224  				// Create inline tree funcdata.
   225  				funcdata := make([]*sym.Symbol, objabi.FUNCDATA_InlTree+1)
   226  				funcdataoff := make([]int64, objabi.FUNCDATA_InlTree+1)
   227  				copy(funcdata, pcln.Funcdata)
   228  				copy(funcdataoff, pcln.Funcdataoff)
   229  				pcln.Funcdata = funcdata
   230  				pcln.Funcdataoff = funcdataoff
   231  			}
   232  		}
   233  
   234  		funcstart := int32(len(ftab.P))
   235  		funcstart += int32(-len(ftab.P)) & (int32(ctxt.Arch.PtrSize) - 1) // align to ptrsize
   236  
   237  		ftab.SetAddr(ctxt.Arch, 8+int64(ctxt.Arch.PtrSize)+int64(nfunc)*2*int64(ctxt.Arch.PtrSize), s)
   238  		ftab.SetUint(ctxt.Arch, 8+int64(ctxt.Arch.PtrSize)+int64(nfunc)*2*int64(ctxt.Arch.PtrSize)+int64(ctxt.Arch.PtrSize), uint64(funcstart))
   239  
   240  		// Write runtime._func. Keep in sync with ../../../../runtime/runtime2.go:/_func
   241  		// and package debug/gosym.
   242  
   243  		// fixed size of struct, checked below
   244  		off := funcstart
   245  
   246  		end := funcstart + int32(ctxt.Arch.PtrSize) + 3*4 + 5*4 + int32(len(pcln.Pcdata))*4 + int32(len(pcln.Funcdata))*int32(ctxt.Arch.PtrSize)
   247  		if len(pcln.Funcdata) > 0 && (end&int32(ctxt.Arch.PtrSize-1) != 0) {
   248  			end += 4
   249  		}
   250  		ftab.Grow(int64(end))
   251  
   252  		// entry uintptr
   253  		off = int32(ftab.SetAddr(ctxt.Arch, int64(off), s))
   254  
   255  		// name int32
   256  		nameoff := nameToOffset(s.Name)
   257  		off = int32(ftab.SetUint32(ctxt.Arch, int64(off), uint32(nameoff)))
   258  
   259  		// args int32
   260  		// TODO: Move into funcinfo.
   261  		args := uint32(0)
   262  		if s.FuncInfo != nil {
   263  			args = uint32(s.FuncInfo.Args)
   264  		}
   265  		off = int32(ftab.SetUint32(ctxt.Arch, int64(off), args))
   266  
   267  		// deferreturn
   268  		deferreturn := uint32(0)
   269  		lastWasmAddr := uint32(0)
   270  		for _, r := range s.R {
   271  			if ctxt.Arch.Family == sys.Wasm && r.Type == objabi.R_ADDR {
   272  				// Wasm does not have a live variable set at the deferreturn
   273  				// call itself. Instead it has one identified by the
   274  				// resumption point immediately preceding the deferreturn.
   275  				// The wasm code has a R_ADDR relocation which is used to
   276  				// set the resumption point to PC_B.
   277  				lastWasmAddr = uint32(r.Add)
   278  			}
   279  			if r.Type.IsDirectCall() && r.Sym != nil && r.Sym.Name == "runtime.deferreturn" {
   280  				if ctxt.Arch.Family == sys.Wasm {
   281  					deferreturn = lastWasmAddr - 1
   282  				} else {
   283  					// Note: the relocation target is in the call instruction, but
   284  					// is not necessarily the whole instruction (for instance, on
   285  					// x86 the relocation applies to bytes [1:5] of the 5 byte call
   286  					// instruction).
   287  					deferreturn = uint32(r.Off)
   288  					switch ctxt.Arch.Family {
   289  					case sys.AMD64, sys.I386:
   290  						deferreturn--
   291  					case sys.PPC64, sys.ARM, sys.ARM64, sys.MIPS, sys.MIPS64:
   292  						// no change
   293  					case sys.RISCV64:
   294  						// TODO(jsing): The JALR instruction is marked with
   295  						// R_CALLRISCV, whereas the actual reloc is currently
   296  						// one instruction earlier starting with the AUIPC.
   297  						deferreturn -= 4
   298  					case sys.S390X:
   299  						deferreturn -= 2
   300  					default:
   301  						panic(fmt.Sprint("Unhandled architecture:", ctxt.Arch.Family))
   302  					}
   303  				}
   304  				break // only need one
   305  			}
   306  		}
   307  		off = int32(ftab.SetUint32(ctxt.Arch, int64(off), deferreturn))
   308  
   309  		if pcln != &pclntabZpcln {
   310  			renumberfiles(ctxt, pcln.File, &pcln.Pcfile)
   311  			if false {
   312  				// Sanity check the new numbering
   313  				it := obj.NewPCIter(uint32(ctxt.Arch.MinLC))
   314  				for it.Init(pcln.Pcfile.P); !it.Done; it.Next() {
   315  					if it.Value < 1 || it.Value > int32(len(ctxt.Filesyms)) {
   316  						Errorf(s, "bad file number in pcfile: %d not in range [1, %d]\n", it.Value, len(ctxt.Filesyms))
   317  						errorexit()
   318  					}
   319  				}
   320  			}
   321  		}
   322  
   323  		if len(pcln.InlTree) > 0 {
   324  			inlTreeSym := ctxt.Syms.Lookup("inltree."+s.Name, 0)
   325  			inlTreeSym.Type = sym.SRODATA
   326  			inlTreeSym.Attr |= sym.AttrReachable | sym.AttrDuplicateOK
   327  
   328  			for i, call := range pcln.InlTree {
   329  				// Usually, call.File is already numbered since the file
   330  				// shows up in the Pcfile table. However, two inlined calls
   331  				// might overlap exactly so that only the innermost file
   332  				// appears in the Pcfile table. In that case, this assigns
   333  				// the outer file a number.
   334  				numberfile(ctxt, call.File)
   335  				nameoff := nameToOffset(call.Func)
   336  
   337  				inlTreeSym.SetUint16(ctxt.Arch, int64(i*20+0), uint16(call.Parent))
   338  				inlTreeSym.SetUint8(ctxt.Arch, int64(i*20+2), uint8(objabi.GetFuncID(call.Func, "")))
   339  				// byte 3 is unused
   340  				inlTreeSym.SetUint32(ctxt.Arch, int64(i*20+4), uint32(call.File.Value))
   341  				inlTreeSym.SetUint32(ctxt.Arch, int64(i*20+8), uint32(call.Line))
   342  				inlTreeSym.SetUint32(ctxt.Arch, int64(i*20+12), uint32(nameoff))
   343  				inlTreeSym.SetUint32(ctxt.Arch, int64(i*20+16), uint32(call.ParentPC))
   344  			}
   345  
   346  			pcln.Funcdata[objabi.FUNCDATA_InlTree] = inlTreeSym
   347  			pcln.Pcdata[objabi.PCDATA_InlTreeIndex] = pcln.Pcinline
   348  		}
   349  
   350  		// pcdata
   351  		off = writepctab(off, pcln.Pcsp.P)
   352  		off = writepctab(off, pcln.Pcfile.P)
   353  		off = writepctab(off, pcln.Pcline.P)
   354  		off = int32(ftab.SetUint32(ctxt.Arch, int64(off), uint32(len(pcln.Pcdata))))
   355  
   356  		// funcID uint8
   357  		var file string
   358  		if s.FuncInfo != nil && len(s.FuncInfo.File) > 0 {
   359  			file = s.FuncInfo.File[0].Name
   360  		}
   361  		funcID := objabi.GetFuncID(s.Name, file)
   362  
   363  		off = int32(ftab.SetUint8(ctxt.Arch, int64(off), uint8(funcID)))
   364  
   365  		// unused
   366  		off += 2
   367  
   368  		// nfuncdata must be the final entry.
   369  		off = int32(ftab.SetUint8(ctxt.Arch, int64(off), uint8(len(pcln.Funcdata))))
   370  		for i := range pcln.Pcdata {
   371  			off = writepctab(off, pcln.Pcdata[i].P)
   372  		}
   373  
   374  		// funcdata, must be pointer-aligned and we're only int32-aligned.
   375  		// Missing funcdata will be 0 (nil pointer).
   376  		if len(pcln.Funcdata) > 0 {
   377  			if off&int32(ctxt.Arch.PtrSize-1) != 0 {
   378  				off += 4
   379  			}
   380  			for i := range pcln.Funcdata {
   381  				dataoff := int64(off) + int64(ctxt.Arch.PtrSize)*int64(i)
   382  				if pcln.Funcdata[i] == nil {
   383  					ftab.SetUint(ctxt.Arch, dataoff, uint64(pcln.Funcdataoff[i]))
   384  					continue
   385  				}
   386  				// TODO: Dedup.
   387  				funcdataBytes += pcln.Funcdata[i].Size
   388  				ftab.SetAddrPlus(ctxt.Arch, dataoff, pcln.Funcdata[i], pcln.Funcdataoff[i])
   389  			}
   390  			off += int32(len(pcln.Funcdata)) * int32(ctxt.Arch.PtrSize)
   391  		}
   392  
   393  		if off != end {
   394  			Errorf(s, "bad math in functab: funcstart=%d off=%d but end=%d (npcdata=%d nfuncdata=%d ptrsize=%d)", funcstart, off, end, len(pcln.Pcdata), len(pcln.Funcdata), ctxt.Arch.PtrSize)
   395  			errorexit()
   396  		}
   397  
   398  		nfunc++
   399  	}
   400  
   401  	last := ctxt.Textp[len(ctxt.Textp)-1]
   402  	pclntabLastFunc = last
   403  	// Final entry of table is just end pc.
   404  	ftab.SetAddrPlus(ctxt.Arch, 8+int64(ctxt.Arch.PtrSize)+int64(nfunc)*2*int64(ctxt.Arch.PtrSize), last, last.Size)
   405  
   406  	// Start file table.
   407  	start := int32(len(ftab.P))
   408  
   409  	start += int32(-len(ftab.P)) & (int32(ctxt.Arch.PtrSize) - 1)
   410  	pclntabFiletabOffset = start
   411  	ftab.SetUint32(ctxt.Arch, 8+int64(ctxt.Arch.PtrSize)+int64(nfunc)*2*int64(ctxt.Arch.PtrSize)+int64(ctxt.Arch.PtrSize), uint32(start))
   412  
   413  	ftab.Grow(int64(start) + (int64(len(ctxt.Filesyms))+1)*4)
   414  	ftab.SetUint32(ctxt.Arch, int64(start), uint32(len(ctxt.Filesyms)+1))
   415  	for i := len(ctxt.Filesyms) - 1; i >= 0; i-- {
   416  		s := ctxt.Filesyms[i]
   417  		ftab.SetUint32(ctxt.Arch, int64(start)+s.Value*4, uint32(ftabaddstring(ftab, s.Name)))
   418  	}
   419  
   420  	ftab.Size = int64(len(ftab.P))
   421  
   422  	if ctxt.Debugvlog != 0 {
   423  		ctxt.Logf("pclntab=%d bytes, funcdata total %d bytes\n", ftab.Size, funcdataBytes)
   424  	}
   425  }
   426  
   427  func gorootFinal() string {
   428  	root := objabi.GOROOT
   429  	if final := os.Getenv("GOROOT_FINAL"); final != "" {
   430  		root = final
   431  	}
   432  	return root
   433  }
   434  
   435  func expandGoroot(s string) string {
   436  	const n = len("$GOROOT")
   437  	if len(s) >= n+1 && s[:n] == "$GOROOT" && (s[n] == '/' || s[n] == '\\') {
   438  		return filepath.ToSlash(filepath.Join(gorootFinal(), s[n:]))
   439  	}
   440  	return s
   441  }
   442  
   443  const (
   444  	BUCKETSIZE    = 256 * MINFUNC
   445  	SUBBUCKETS    = 16
   446  	SUBBUCKETSIZE = BUCKETSIZE / SUBBUCKETS
   447  	NOIDX         = 0x7fffffff
   448  )
   449  
   450  // findfunctab generates a lookup table to quickly find the containing
   451  // function for a pc. See src/runtime/symtab.go:findfunc for details.
   452  func (ctxt *Link) findfunctab() {
   453  	t := ctxt.Syms.Lookup("runtime.findfunctab", 0)
   454  	t.Type = sym.SRODATA
   455  	t.Attr |= sym.AttrReachable
   456  	t.Attr |= sym.AttrLocal
   457  
   458  	// find min and max address
   459  	min := ctxt.Textp[0].Value
   460  	lastp := ctxt.Textp[len(ctxt.Textp)-1]
   461  	max := lastp.Value + lastp.Size
   462  
   463  	// for each subbucket, compute the minimum of all symbol indexes
   464  	// that map to that subbucket.
   465  	n := int32((max - min + SUBBUCKETSIZE - 1) / SUBBUCKETSIZE)
   466  
   467  	indexes := make([]int32, n)
   468  	for i := int32(0); i < n; i++ {
   469  		indexes[i] = NOIDX
   470  	}
   471  	idx := int32(0)
   472  	for i, s := range ctxt.Textp {
   473  		if !emitPcln(ctxt, s) {
   474  			continue
   475  		}
   476  		p := s.Value
   477  		var e *sym.Symbol
   478  		i++
   479  		if i < len(ctxt.Textp) {
   480  			e = ctxt.Textp[i]
   481  		}
   482  		for !emitPcln(ctxt, e) && i < len(ctxt.Textp) {
   483  			e = ctxt.Textp[i]
   484  			i++
   485  		}
   486  		q := max
   487  		if e != nil {
   488  			q = e.Value
   489  		}
   490  
   491  		//print("%d: [%lld %lld] %s\n", idx, p, q, s->name);
   492  		for ; p < q; p += SUBBUCKETSIZE {
   493  			i = int((p - min) / SUBBUCKETSIZE)
   494  			if indexes[i] > idx {
   495  				indexes[i] = idx
   496  			}
   497  		}
   498  
   499  		i = int((q - 1 - min) / SUBBUCKETSIZE)
   500  		if indexes[i] > idx {
   501  			indexes[i] = idx
   502  		}
   503  		idx++
   504  	}
   505  
   506  	// allocate table
   507  	nbuckets := int32((max - min + BUCKETSIZE - 1) / BUCKETSIZE)
   508  
   509  	t.Grow(4*int64(nbuckets) + int64(n))
   510  
   511  	// fill in table
   512  	for i := int32(0); i < nbuckets; i++ {
   513  		base := indexes[i*SUBBUCKETS]
   514  		if base == NOIDX {
   515  			Errorf(nil, "hole in findfunctab")
   516  		}
   517  		t.SetUint32(ctxt.Arch, int64(i)*(4+SUBBUCKETS), uint32(base))
   518  		for j := int32(0); j < SUBBUCKETS && i*SUBBUCKETS+j < n; j++ {
   519  			idx = indexes[i*SUBBUCKETS+j]
   520  			if idx == NOIDX {
   521  				Errorf(nil, "hole in findfunctab")
   522  			}
   523  			if idx-base >= 256 {
   524  				Errorf(nil, "too many functions in a findfunc bucket! %d/%d %d %d", i, nbuckets, j, idx-base)
   525  			}
   526  
   527  			t.SetUint8(ctxt.Arch, int64(i)*(4+SUBBUCKETS)+4+int64(j), uint8(idx-base))
   528  		}
   529  	}
   530  }