]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/link: split large elf text sections on ppc64x
authorLynn Boger <laboger@linux.vnet.ibm.com>
Thu, 25 Aug 2016 16:07:33 +0000 (11:07 -0500)
committerIan Lance Taylor <iant@golang.org>
Wed, 21 Sep 2016 20:23:49 +0000 (20:23 +0000)
Some applications built with Go on ppc64x with external linking
can fail to link with relocation truncation errors if the elf
text section that is generated is larger than 2^26 bytes and that
section contains a call instruction (bl) which calls a function
beyond the limit addressable by the 24 bit field in the
instruction.

This solution consists of generating multiple text sections where
each is small enough to allow the GNU linker to resolve the calls
by generating long branch code where needed.  Other changes were added
to handle differences in processing when multiple text sections exist.

Some adjustments were required to the computation of a method's address
when using the method offset table when there are multiple text sections.

The number of possible section headers was increased to allow for up
to 128 text sections.  A test case was also added.

Fixes #15823.

Change-Id: If8117b0e0afb058cbc072258425a35aef2363c92
Reviewed-on: https://go-review.googlesource.com/27790
Run-TryBot: Ian Lance Taylor <iant@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
src/cmd/link/internal/ld/data.go
src/cmd/link/internal/ld/elf.go
src/cmd/link/internal/ld/lib.go
src/cmd/link/internal/ld/symtab.go
src/cmd/link/internal/ppc64/asm.go
src/cmd/link/linkbig_test.go [new file with mode: 0644]
src/runtime/symtab.go
src/runtime/type.go

index 454f7a5da2b58e5422dea1836a58418350b7b94f..a3d0b700483467e36f2dfeb78625546f965e55b3 100644 (file)
@@ -544,7 +544,14 @@ func relocsym(ctxt *Link, s *Symbol) {
                        o = Symaddr(r.Sym) + r.Add - int64(r.Sym.Sect.Vaddr)
 
                case obj.R_ADDROFF:
-                       o = Symaddr(r.Sym) - int64(r.Sym.Sect.Vaddr) + r.Add
+                       // The method offset tables using this relocation expect the offset to be relative
+                       // to the start of the first text section, even if there are multiple.
+
+                       if r.Sym.Sect.Name == ".text" {
+                               o = Symaddr(r.Sym) - int64(Segtext.Vaddr) + r.Add
+                       } else {
+                               o = Symaddr(r.Sym) - int64(r.Sym.Sect.Vaddr) + r.Add
+                       }
 
                        // r->sym can be null when CALL $(constant) is transformed from absolute PC to relative PC call.
                case obj.R_CALL, obj.R_GOTPCREL, obj.R_PCREL:
@@ -1881,11 +1888,11 @@ func (ctxt *Link) textaddress() {
 
        sect.Align = int32(Funcalign)
        ctxt.Syms.Lookup("runtime.text", 0).Sect = sect
-       ctxt.Syms.Lookup("runtime.etext", 0).Sect = sect
        if Headtype == obj.Hwindows || Headtype == obj.Hwindowsgui {
                ctxt.Syms.Lookup(".text", 0).Sect = sect
        }
        va := uint64(*FlagTextAddr)
+       n := 1
        sect.Vaddr = va
        for _, sym := range ctxt.Textp {
                sym.Sect = sect
@@ -1901,14 +1908,38 @@ func (ctxt *Link) textaddress() {
                for sub := sym; sub != nil; sub = sub.Sub {
                        sub.Value += int64(va)
                }
-               if sym.Size < MINFUNC {
-                       va += MINFUNC // spacing required for findfunctab
-               } else {
-                       va += uint64(sym.Size)
+               funcsize := uint64(MINFUNC) // spacing required for findfunctab
+               if sym.Size > MINFUNC {
+                       funcsize = uint64(sym.Size)
                }
+
+               // On ppc64x a text section should not be larger than 2^26 bytes due to the size of
+               // call target offset field in the bl instruction.  Splitting into smaller text
+               // sections smaller than this limit allows the GNU linker to modify the long calls
+               // appropriately.  The limit allows for the space needed for tables inserted by the linker.
+
+               // If this function doesn't fit in the current text section, then create a new one.
+
+               // Only break at outermost syms.
+
+               if SysArch.InFamily(sys.PPC64) && sym.Outer == nil && Iself && Linkmode == LinkExternal && va-sect.Vaddr+funcsize > 0x1c00000 {
+
+                       // Set the length for the previous text section
+                       sect.Length = va - sect.Vaddr
+
+                       // Create new section, set the starting Vaddr
+                       sect = addsection(&Segtext, ".text", 05)
+                       sect.Vaddr = va
+
+                       // Create a symbol for the start of the secondary text sections
+                       ctxt.Syms.Lookup(fmt.Sprintf("runtime.text.%d", n), 0).Sect = sect
+                       n++
+               }
+               va += funcsize
        }
 
        sect.Length = va - sect.Vaddr
+       ctxt.Syms.Lookup("runtime.etext", 0).Sect = sect
 }
 
 // assign addresses
@@ -2052,6 +2083,11 @@ func (ctxt *Link) address() {
                pclntab  = ctxt.Syms.Lookup("runtime.pclntab", 0).Sect
                types    = ctxt.Syms.Lookup("runtime.types", 0).Sect
        )
+       lasttext := text
+       // Could be multiple .text sections
+       for sect := text.Next; sect != nil && sect.Name == ".text"; sect = sect.Next {
+               lasttext = sect
+       }
 
        for _, s := range datap {
                if s.Sect != nil {
@@ -2079,10 +2115,20 @@ func (ctxt *Link) address() {
        }
 
        ctxt.xdefine("runtime.text", obj.STEXT, int64(text.Vaddr))
-       ctxt.xdefine("runtime.etext", obj.STEXT, int64(text.Vaddr+text.Length))
+       ctxt.xdefine("runtime.etext", obj.STEXT, int64(lasttext.Vaddr+lasttext.Length))
        if Headtype == obj.Hwindows || Headtype == obj.Hwindowsgui {
                ctxt.xdefine(".text", obj.STEXT, int64(text.Vaddr))
        }
+
+       // If there are multiple text sections, create runtime.text.n for
+       // their section Vaddr, using n for index
+       n := 1
+       for sect := Segtext.Sect.Next; sect != nil && sect.Name == ".text"; sect = sect.Next {
+               symname := fmt.Sprintf("runtime.text.%d", n)
+               ctxt.xdefine(symname, obj.STEXT, int64(sect.Vaddr))
+               n++
+       }
+
        ctxt.xdefine("runtime.rodata", obj.SRODATA, int64(rodata.Vaddr))
        ctxt.xdefine("runtime.erodata", obj.SRODATA, int64(rodata.Vaddr+rodata.Length))
        ctxt.xdefine("runtime.types", obj.SRODATA, int64(types.Vaddr))
index 3cdf390ef646502baf5f829a947b20a7da35a880..70681b32623d7fe701f6fe2ba28e75f78c0326bc 100644 (file)
@@ -879,7 +879,7 @@ const (
  * written in the 32-bit format on the 32-bit machines.
  */
 const (
-       NSECT = 48
+       NSECT = 400
 )
 
 var (
@@ -1634,6 +1634,25 @@ func elfshname(name string) *ElfShdr {
        return nil
 }
 
+// Create an ElfShdr for the section with name.
+// Create a duplicate if one already exists with that name
+func elfshnamedup(name string) *ElfShdr {
+       var off int
+       var sh *ElfShdr
+
+       for i := 0; i < nelfstr; i++ {
+               if name == elfstr[i].s {
+                       off = elfstr[i].off
+                       sh = newElfShdr(int64(off))
+                       return sh
+               }
+       }
+
+       Errorf(nil, "cannot find elf name %s", name)
+       errorexit()
+       return nil
+}
+
 func elfshalloc(sect *Section) *ElfShdr {
        sh := elfshname(sect.Name)
        sect.Elfsect = sh
@@ -1641,7 +1660,17 @@ func elfshalloc(sect *Section) *ElfShdr {
 }
 
 func elfshbits(sect *Section) *ElfShdr {
-       sh := elfshalloc(sect)
+       var sh *ElfShdr
+
+       if sect.Name == ".text" {
+               if sect.Elfsect == nil {
+                       sect.Elfsect = elfshnamedup(sect.Name)
+               }
+               sh = sect.Elfsect
+       } else {
+               sh = elfshalloc(sect)
+       }
+
        // If this section has already been set up as a note, we assume type_ and
        // flags are already correct, but the other fields still need filling in.
        if sh.type_ == SHT_NOTE {
@@ -1717,6 +1746,15 @@ func elfshreloc(sect *Section) *ElfShdr {
        }
 
        sh := elfshname(elfRelType + sect.Name)
+       // There could be multiple text sections but each needs
+       // its own .rela.text.
+
+       if sect.Name == ".text" {
+               if sh.info != 0 && sh.info != uint32(sect.Elfsect.shnum) {
+                       sh = elfshnamedup(elfRelType + sect.Name)
+               }
+       }
+
        sh.type_ = uint32(typ)
        sh.entsize = uint64(SysArch.RegSize) * 2
        if typ == SHT_RELA {
@@ -1788,10 +1826,14 @@ func Elfemitreloc(ctxt *Link) {
                Cput(0)
        }
 
-       elfrelocsect(ctxt, Segtext.Sect, ctxt.Textp)
-       for sect := Segtext.Sect.Next; sect != nil; sect = sect.Next {
-               elfrelocsect(ctxt, sect, datap)
+       for sect := Segtext.Sect; sect != nil; sect = sect.Next {
+               if sect.Name == ".text" {
+                       elfrelocsect(ctxt, sect, ctxt.Textp)
+               } else {
+                       elfrelocsect(ctxt, sect, datap)
+               }
        }
+
        for sect := Segrodata.Sect; sect != nil; sect = sect.Next {
                elfrelocsect(ctxt, sect, datap)
        }
@@ -2124,7 +2166,15 @@ func Asmbelfsetup() {
        elfshname("")
 
        for sect := Segtext.Sect; sect != nil; sect = sect.Next {
-               elfshalloc(sect)
+               // There could be multiple .text sections. Instead check the Elfsect
+               // field to determine if already has an ElfShdr and if not, create one.
+               if sect.Name == ".text" {
+                       if sect.Elfsect == nil {
+                               sect.Elfsect = elfshnamedup(sect.Name)
+                       }
+               } else {
+                       elfshalloc(sect)
+               }
        }
        for sect := Segrodata.Sect; sect != nil; sect = sect.Next {
                elfshalloc(sect)
@@ -2162,6 +2212,23 @@ func Asmbelf(ctxt *Link, symo int64) {
        }
 
        elfreserve := int64(ELFRESERVE)
+
+       numtext := int64(0)
+       for sect := Segtext.Sect; sect != nil; sect = sect.Next {
+               if sect.Name == ".text" {
+                       numtext++
+               }
+       }
+
+       // If there are multiple text sections, extra space is needed
+       // in the elfreserve for the additional .text and .rela.text
+       // section headers.  It can handle 4 extra now. Headers are
+       // 64 bytes.
+
+       if numtext > 4 {
+               elfreserve += elfreserve + numtext*64*2
+       }
+
        startva := *FlagTextAddr - int64(HEADR)
        resoff := elfreserve
 
@@ -2630,7 +2697,7 @@ elfobj:
        }
 
        if a > elfreserve {
-               Errorf(nil, "ELFRESERVE too small: %d > %d", a, elfreserve)
+               Errorf(nil, "ELFRESERVE too small: %d > %d with %d text sections", a, elfreserve, numtext)
        }
 }
 
index c288908a860e268021f84082734c0ef69200dd55..9b7d7a99c7e69cfc8002bbcfaa9dcae1eeca14dc 100644 (file)
@@ -1812,6 +1812,28 @@ func genasmsym(ctxt *Link, put func(*Link, *Symbol, string, SymbolType, int64, *
        if s.Type == obj.STEXT {
                put(ctxt, s, s.Name, TextSym, s.Value, nil)
        }
+
+       n := 0
+
+       // Generate base addresses for all text sections if there are multiple
+       for sect := Segtext.Sect; sect != nil; sect = sect.Next {
+               if n == 0 {
+                       n++
+                       continue
+               }
+               if sect.Name != ".text" {
+                       break
+               }
+               s = ctxt.Syms.ROLookup(fmt.Sprintf("runtime.text.%d", n), 0)
+               if s == nil {
+                       break
+               }
+               if s.Type == obj.STEXT {
+                       put(ctxt, s, s.Name, TextSym, s.Value, nil)
+               }
+               n++
+       }
+
        s = ctxt.Syms.Lookup("runtime.etext", 0)
        if s.Type == obj.STEXT {
                put(ctxt, s, s.Name, TextSym, s.Value, nil)
index d7eec06318b57bd2cd46e0591a16c391418f8163..e4280f0ccbcbee4ed92a18a8f56eabb2588ad1fb 100644 (file)
@@ -302,6 +302,62 @@ func (libs byPkg) Swap(a, b int) {
        libs[a], libs[b] = libs[b], libs[a]
 }
 
+// Create a table with information on the text sections.
+
+func textsectionmap(ctxt *Link) uint32 {
+
+       t := ctxt.Syms.Lookup("runtime.textsectionmap", 0)
+       t.Type = obj.SRODATA
+       t.Attr |= AttrReachable
+       nsections := int64(0)
+
+       for sect := Segtext.Sect; sect != nil; sect = sect.Next {
+               if sect.Name == ".text" {
+                       nsections++
+               } else {
+                       break
+               }
+       }
+       Symgrow(t, nsections*(2*int64(SysArch.IntSize)+int64(SysArch.PtrSize)))
+
+       off := int64(0)
+       n := 0
+
+       // The vaddr for each text section is the difference between the section's
+       // Vaddr and the Vaddr for the first text section as determined at compile
+       // time.
+
+       // The symbol for the first text section is named runtime.text as before.
+       // Additional text sections are named runtime.text.n where n is the
+       // order of creation starting with 1. These symbols provide the section's
+       // address after relocation by the linker.
+
+       textbase := Segtext.Sect.Vaddr
+       for sect := Segtext.Sect; sect != nil; sect = sect.Next {
+               if sect.Name != ".text" {
+                       break
+               }
+               off = setuintxx(ctxt, t, off, sect.Vaddr-textbase, int64(SysArch.IntSize))
+               off = setuintxx(ctxt, t, off, sect.Length, int64(SysArch.IntSize))
+               if n == 0 {
+                       s := ctxt.Syms.ROLookup("runtime.text", 0)
+                       if s == nil {
+                               Errorf(nil, "Unable to find symbol runtime.text\n")
+                       }
+                       off = setaddr(ctxt, t, off, s)
+
+               } else {
+                       s := ctxt.Syms.Lookup(fmt.Sprintf("runtime.text.%d", n), 0)
+                       if s == nil {
+                               Errorf(nil, "Unable to find symbol runtime.text.%d\n", n)
+                       }
+                       off = setaddr(ctxt, t, off, s)
+               }
+               n++
+       }
+       return uint32(n)
+}
+
 func (ctxt *Link) symtab() {
        dosymtype(ctxt)
 
@@ -492,6 +548,8 @@ func (ctxt *Link) symtab() {
                adduint(ctxt, abihashgostr, uint64(hashsym.Size))
        }
 
+       nsections := textsectionmap(ctxt)
+
        // Information about the layout of the executable image for the
        // runtime to use. Any changes here must be matched by changes to
        // the definition of moduledata in runtime/symtab.go.
@@ -530,6 +588,12 @@ func (ctxt *Link) symtab() {
        Addaddr(ctxt, moduledata, ctxt.Syms.Lookup("runtime.gcbss", 0))
        Addaddr(ctxt, moduledata, ctxt.Syms.Lookup("runtime.types", 0))
        Addaddr(ctxt, moduledata, ctxt.Syms.Lookup("runtime.etypes", 0))
+
+       // text section information
+       Addaddr(ctxt, moduledata, ctxt.Syms.Lookup("runtime.textsectionmap", 0))
+       adduint(ctxt, moduledata, uint64(nsections))
+       adduint(ctxt, moduledata, uint64(nsections))
+
        // The typelinks slice
        Addaddr(ctxt, moduledata, ctxt.Syms.Lookup("runtime.typelink", 0))
        adduint(ctxt, moduledata, uint64(ntypelinks))
index f0b8eadd0b23bbf24abdf36e1bba7780478cb203..7d9094ba470efe916573368147472a022548be8e 100644 (file)
@@ -811,12 +811,14 @@ func asmb(ctxt *ld.Link) {
                ld.Asmbelfsetup()
        }
 
-       sect := ld.Segtext.Sect
-       ld.Cseek(int64(sect.Vaddr - ld.Segtext.Vaddr + ld.Segtext.Fileoff))
-       ld.Codeblk(ctxt, int64(sect.Vaddr), int64(sect.Length))
-       for sect = sect.Next; sect != nil; sect = sect.Next {
+       for sect := ld.Segtext.Sect; sect != nil; sect = sect.Next {
                ld.Cseek(int64(sect.Vaddr - ld.Segtext.Vaddr + ld.Segtext.Fileoff))
-               ld.Datblk(ctxt, int64(sect.Vaddr), int64(sect.Length))
+               // Handle additional text sections with Codeblk
+               if sect.Name == ".text" {
+                       ld.Codeblk(ctxt, int64(sect.Vaddr), int64(sect.Length))
+               } else {
+                       ld.Datblk(ctxt, int64(sect.Vaddr), int64(sect.Length))
+               }
        }
 
        if ld.Segrodata.Filelen > 0 {
diff --git a/src/cmd/link/linkbig_test.go b/src/cmd/link/linkbig_test.go
new file mode 100644 (file)
index 0000000..b4fa5c7
--- /dev/null
@@ -0,0 +1,91 @@
+// Copyright 2016 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.
+
+// This program generates a test to verify that a program can be
+// successfully linked even when there are very large text
+// sections present.
+
+package main
+
+import (
+       "bytes"
+       "cmd/internal/obj"
+       "fmt"
+       "io/ioutil"
+       "os"
+       "os/exec"
+       "testing"
+)
+
+func TestLargeText(t *testing.T) {
+
+       var w bytes.Buffer
+
+       if testing.Short() || (obj.GOARCH != "ppc64le" && obj.GOARCH != "ppc64") {
+               t.Skip("Skipping large text section test in short mode or if not ppc64x")
+       }
+       const FN = 4
+       tmpdir, err := ioutil.TempDir("", "bigtext")
+
+       defer os.RemoveAll(tmpdir)
+
+       // Generate the scenario where the total amount of text exceeds the
+       // limit for the bl instruction, on RISC architectures like ppc64le,
+       // which is 2^26.  When that happens the call requires special trampolines or
+       // long branches inserted by the linker where supported.
+
+       // Multiple .s files are generated instead of one.
+
+       for j := 0; j < FN; j++ {
+               testname := fmt.Sprintf("bigfn%d", j)
+               fmt.Fprintf(&w, "TEXT ยท%s(SB),$0\n", testname)
+               for i := 0; i < 2200000; i++ {
+                       fmt.Fprintf(&w, "\tMOVD\tR0,R3\n")
+               }
+               fmt.Fprintf(&w, "\tRET\n")
+               err := ioutil.WriteFile(tmpdir+"/"+testname+".s", w.Bytes(), 0666)
+               if err != nil {
+                       t.Fatalf("can't write output: %v\n", err)
+               }
+               w.Reset()
+       }
+       fmt.Fprintf(&w, "package main\n")
+       fmt.Fprintf(&w, "\nimport (\n")
+       fmt.Fprintf(&w, "\t\"os\"\n")
+       fmt.Fprintf(&w, "\t\"fmt\"\n")
+       fmt.Fprintf(&w, ")\n\n")
+
+       for i := 0; i < FN; i++ {
+               fmt.Fprintf(&w, "func bigfn%d()\n", i)
+       }
+       fmt.Fprintf(&w, "\nfunc main() {\n")
+
+       // There are lots of dummy code generated in the .s files just to generate a lot
+       // of text. Link them in but guard their call so their code is not executed but
+       // the main part of the program can be run.
+
+       fmt.Fprintf(&w, "\tif os.Getenv(\"LINKTESTARG\") != \"\" {\n")
+       for i := 0; i < FN; i++ {
+               fmt.Fprintf(&w, "\t\tbigfn%d()\n", i)
+       }
+       fmt.Fprintf(&w, "\t}\n")
+       fmt.Fprintf(&w, "\tfmt.Printf(\"PASS\\n\")\n")
+       fmt.Fprintf(&w, "}")
+       err = ioutil.WriteFile(tmpdir+"/bigfn.go", w.Bytes(), 0666)
+       if err != nil {
+               t.Fatalf("can't write output: %v\n", err)
+       }
+
+       os.Chdir(tmpdir)
+       cmd := exec.Command("go", "build", "-o", "bigtext", "-ldflags", "'-linkmode=external'")
+       out, err := cmd.CombinedOutput()
+       if err != nil {
+               t.Fatalf("Build of big text program failed: %v, output: %s", err, out)
+       }
+       cmd = exec.Command(tmpdir + "/bigtext")
+       out, err = cmd.CombinedOutput()
+       if err != nil {
+               t.Fatalf("Program failed with err %v, output: %s", err, out)
+       }
+}
index 87b478a885fb3232480dc16ca67ce902cd9709fc..c1cca7037d7f6057f89d0d247d24a2bd2aec7390 100644 (file)
@@ -195,8 +195,9 @@ type moduledata struct {
        end, gcdata, gcbss    uintptr
        types, etypes         uintptr
 
-       typelinks []int32 // offsets from types
-       itablinks []*itab
+       textsectmap []textsect
+       typelinks   []int32 // offsets from types
+       itablinks   []*itab
 
        ptab []ptabEntry
 
@@ -228,6 +229,14 @@ type functab struct {
        funcoff uintptr
 }
 
+// Mapping information for secondary text sections
+
+type textsect struct {
+       vaddr    uintptr // prelinked section vaddr
+       length   uintptr // section length
+       baseaddr uintptr // relocated section address
+}
+
 const minfunc = 16                 // minimum function size
 const pcbucketsize = 256 * minfunc // size of bucket in the pc->func lookup table
 
@@ -370,12 +379,23 @@ func findfunc(pc uintptr) *_func {
        ffb := (*findfuncbucket)(add(unsafe.Pointer(datap.findfunctab), b*unsafe.Sizeof(findfuncbucket{})))
        idx := ffb.idx + uint32(ffb.subbuckets[i])
        if pc < datap.ftab[idx].entry {
-               throw("findfunc: bad findfunctab entry")
-       }
 
-       // linear search to find func with pc >= entry.
-       for datap.ftab[idx+1].entry <= pc {
-               idx++
+               // If there are multiple text sections then the buckets for the secondary
+               // text sections will be off because the addresses in those text sections
+               // were relocated to higher addresses.  Search back to find it.
+
+               for datap.ftab[idx].entry > pc && idx > 0 {
+                       idx--
+               }
+               if idx == 0 {
+                       throw("findfunc: bad findfunctab entry idx")
+               }
+       } else {
+
+               // linear search to find func with pc >= entry.
+               for datap.ftab[idx+1].entry <= pc {
+                       idx++
+               }
        }
        return (*_func)(unsafe.Pointer(&datap.pclntable[datap.ftab[idx].funcoff]))
 }
index 0467c774002d9476242122ebd56e0baf94473c60..7f7849d5a00d3d6a34564dac7d04c3e22af838a9 100644 (file)
@@ -257,7 +257,30 @@ func (t *_type) textOff(off textOff) unsafe.Pointer {
                }
                return res
        }
-       res := md.text + uintptr(off)
+       res := uintptr(0)
+
+       // The text, or instruction stream is generated as one large buffer.  The off (offset) for a method is
+       // its offset within this buffer.  If the total text size gets too large, there can be issues on platforms like ppc64 if
+       // the target of calls are too far for the call instruction.  To resolve the large text issue, the text is split
+       // into multiple text sections to allow the linker to generate long calls when necessary.  When this happens, the vaddr
+       // for each text section is set to its offset within the text.  Each method's offset is compared against the section
+       // vaddrs and sizes to determine the containing section.  Then the section relative offset is added to the section's
+       // relocated baseaddr to compute the method addess.
+
+       if len(md.textsectmap) > 1 {
+               for i := range md.textsectmap {
+                       sectaddr := md.textsectmap[i].vaddr
+                       sectlen := md.textsectmap[i].length
+                       if uintptr(off) >= sectaddr && uintptr(off) <= sectaddr+sectlen {
+                               res = md.textsectmap[i].baseaddr + uintptr(off) - uintptr(md.textsectmap[i].vaddr)
+                               break
+                       }
+               }
+       } else {
+               // single text section
+               res = md.text + uintptr(off)
+       }
+
        if res > md.etext {
                println("runtime: textOff", hex(off), "out of range", hex(md.text), "-", hex(md.etext))
                throw("runtime: text offset out of range")