Skip to content

Commit

Permalink
remove all unexported func names with -tiny via the linker
Browse files Browse the repository at this point in the history
The patch to the linker does this when generating the pclntab,
which is the binary section containing func names.
When `-tiny` is being used, we look for unexported funcs,
and set their names to the offset `0` - a shared empty string.

We also avoid including the original name in the binary,
which saves a significant amount of space.
The following stats were collected on GOOS=linux,
which show that `-tiny` is now about 4% smaller:

	go build                  1203067
	garble build               782336
	(old) garble -tiny build   688128
	(new) garble -tiny build   659456
  • Loading branch information
pagran committed Jan 9, 2023
1 parent 417bcf2 commit 22c177f
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 30 deletions.
44 changes: 21 additions & 23 deletions internal/linker/linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import (
"os/exec"
"path/filepath"
"runtime"
"strings"

"github.com/bluekeyes/go-gitdiff/gitdiff"
"github.com/rogpeppe/go-internal/lockedfile"
)

const (
MagicValueEnv = "GARBLE_LINKER_MAGIC"
MagicValueEnv = "GARBLE_LINK_MAGIC"
TinyEnv = "GARBLE_LINK_TINY"

cacheDirName = "garble"
versionExt = ".version"
Expand All @@ -36,10 +36,10 @@ var (
linkerPatchesFS embed.FS
)

func loadLinkerPatches() (string, map[string]string, error) {
func loadLinkerPatches() (version string, modFiles map[string]bool, patches [][]byte, err error) {
modFiles = make(map[string]bool)
versionHash := sha256.New()
patches := make(map[string]string)
err := fs.WalkDir(linkerPatchesFS, ".", func(path string, d fs.DirEntry, err error) error {
err = fs.WalkDir(linkerPatchesFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
Expand All @@ -64,23 +64,17 @@ func loadLinkerPatches() (string, map[string]string, error) {
if file.IsNew || file.IsDelete || file.IsCopy || file.IsRename {
panic("only modification patch is supported")
}
patches[file.OldName] = string(patchBytes)
modFiles[file.OldName] = true
}
patches = append(patches, patchBytes)
return nil
})

if err != nil {
return "", nil, err
return
}
return base64.RawStdEncoding.EncodeToString(versionHash.Sum(nil)), patches, nil
}

// TODO(pagran): Remove git dependency in future
// more information in README.md
func applyPatch(workingDir, patch string) error {
cmd := exec.Command("git", "-C", workingDir, "apply")
cmd.Stdin = strings.NewReader(patch)
return cmd.Run()
version = base64.RawStdEncoding.EncodeToString(versionHash.Sum(nil))
return
}

func copyFile(src, target string) error {
Expand Down Expand Up @@ -111,20 +105,24 @@ func fileExists(path string) bool {
return !stat.IsDir()
}

func applyPatches(srcDir, workingDir string, patches map[string]string) (map[string]string, error) {
// TODO(pagran): Remove git dependency in future
// more information in README.md
func applyPatches(srcDir, workingDir string, modFiles map[string]bool, patches [][]byte) (map[string]string, error) {
mod := make(map[string]string)
for fileName, patch := range patches {
for fileName := range modFiles {
oldPath := filepath.Join(srcDir, fileName)
newPath := filepath.Join(workingDir, fileName)
mod[oldPath] = newPath

if err := copyFile(oldPath, newPath); err != nil {
return nil, err
}
}

if err := applyPatch(workingDir, patch); err != nil {
return nil, fmt.Errorf("apply patch for %s failed: %v", fileName, err)
}
cmd := exec.Command("git", "-C", workingDir, "apply")
cmd.Stdin = bytes.NewReader(bytes.Join(patches, []byte("\n")))
if err := cmd.Run(); err != nil {
return nil, err
}
return mod, nil
}
Expand Down Expand Up @@ -199,7 +197,7 @@ func buildLinker(workingDir string, overlay map[string]string, outputLinkPath st
}

func PatchLinker(goRoot, goVersion, goExe, tempDir string) (string, func(), error) {
patchesVer, patches, err := loadLinkerPatches()
patchesVer, modFiles, patches, err := loadLinkerPatches()
if err != nil {
panic(fmt.Errorf("cannot retrieve linker patches: %v", err))
}
Expand Down Expand Up @@ -235,7 +233,7 @@ func PatchLinker(goRoot, goVersion, goExe, tempDir string) (string, func(), erro
srcDir := filepath.Join(goRoot, baseSrcSubdir)
workingDir := filepath.Join(tempDir, "linker-src")

overlay, err := applyPatches(srcDir, workingDir, patches)
overlay, err := applyPatches(srcDir, workingDir, modFiles, patches)
if err != nil {
return "", nil, err
}
Expand Down
10 changes: 5 additions & 5 deletions internal/linker/patches/0001-add-custom-magic-value.patch
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
From de93a968f1bb3500088b30cbdce439e6a0d95e58 Mon Sep 17 00:00:00 2001
From 444ff9310865ff5e4b367d28edfd386ec795524e Mon Sep 17 00:00:00 2001
From: pagran <pagran@protonmail.com>
Date: Sun, 8 Jan 2023 14:12:51 +0100
Subject: [PATCH 1/1] add custom magic value
Date: Mon, 9 Jan 2023 10:32:32 +0100
Subject: [PATCH] add custom magic value

---
cmd/link/internal/ld/pcln.go | 13 +++++++++++++
1 file changed, 13 insertions(+)

diff --git a/cmd/link/internal/ld/pcln.go b/cmd/link/internal/ld/pcln.go
index 34ab86cf12..1ec237ffc8 100644
index 34ab86cf12..b89a4d650c 100644
--- a/cmd/link/internal/ld/pcln.go
+++ b/cmd/link/internal/ld/pcln.go
@@ -249,6 +249,19 @@ func (state *pclntab) generatePCHeader(ctxt *Link) {
Expand All @@ -17,7 +17,7 @@ index 34ab86cf12..1ec237ffc8 100644
}
+
+ // Use garble prefix in variable names to minimize collision risk
+ garbleMagicStr := os.Getenv("GARBLE_LINKER_MAGIC")
+ garbleMagicStr := os.Getenv("GARBLE_LINK_MAGIC")
+ if garbleMagicStr == "" {
+ panic("[garble] magic value must be set")
+ }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
From aad38f7aa37d00c723c3540bd8a907b92353d97d Mon Sep 17 00:00:00 2001
From: pagran <pagran@protonmail.com>
Date: Mon, 9 Jan 2023 10:27:41 +0100
Subject: [PATCH] add unexported function name removing

---
cmd/link/internal/ld/pcln.go | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)

diff --git a/cmd/link/internal/ld/pcln.go b/cmd/link/internal/ld/pcln.go
index 1ec237ffc8..e1bea2032c 100644
--- a/cmd/link/internal/ld/pcln.go
+++ b/cmd/link/internal/ld/pcln.go
@@ -321,10 +321,19 @@ func (state *pclntab) generateFuncnametab(ctxt *Link, funcs []loader.Sym) map[lo
return name[:i], "[...]", name[j+1:]
}

+ garbleIsRemove := os.Getenv("GARBLE_LINK_TINY") == "true"
+
// Write the null terminated strings.
writeFuncNameTab := func(ctxt *Link, s loader.Sym) {
symtab := ctxt.loader.MakeSymbolUpdater(s)
+ if garbleIsRemove {
+ symtab.AddStringAt(0, "")
+ }
+
for s, off := range nameOffsets {
+ if garbleIsRemove && off == 0 {
+ continue
+ }
a, b, c := nameParts(ctxt.loader.SymName(s))
o := int64(off)
o = symtab.AddStringAt(o, a)
@@ -335,7 +344,25 @@ func (state *pclntab) generateFuncnametab(ctxt *Link, funcs []loader.Sym) map[lo

// Loop through the CUs, and calculate the size needed.
var size int64
+
+ if garbleIsRemove {
+ size = 1 // first byte is reserved for empty string used for all non-exportable method names
+ }
+ garbleIsUnexported := func(s loader.Sym) bool {
+ name, _, _ := nameParts(ctxt.loader.SymName(s))
+ if name[len(name)-1] == '.' {
+ return true
+ }
+ c := name[strings.LastIndexByte(name, '.')+1]
+ return 'a' <= c && c <= 'z'
+ }
+
walkFuncs(ctxt, funcs, func(s loader.Sym) {
+ if garbleIsRemove && garbleIsUnexported(s) {
+ nameOffsets[s] = 0 // redirect name to empty string
+ return
+ }
+
nameOffsets[s] = uint32(size)
a, b, c := nameParts(ctxt.loader.SymName(s))
size += int64(len(a) + len(b) + len(c) + 1) // NULL terminate
--
2.38.1.windows.1

3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,9 @@ func mainErr(args []string) error {

executablePath = modifiedLinkPath
os.Setenv(linker.MagicValueEnv, strconv.FormatUint(uint64(magicValue()), 10))
if flagTiny {
os.Setenv(linker.TinyEnv, "true")
}

log.Printf("replaced linker with: %s", executablePath)
}
Expand Down
30 changes: 28 additions & 2 deletions testdata/script/tiny.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ stderr '^caller: \?\? 1$' # position info is removed
stderr '^recovered: ya like jazz?'
! stderr '^init runtime' # GODEBUG prints are hidden, like inittrace=1
! stderr 'panic: oh noes' # panics are hidden

stderr 'funcExported false funcUnexported true'
stderr 'funcStructExported false funcStructUnexported true'

[short] stop # no need to verify this with -short

Expand All @@ -21,16 +22,41 @@ garble build
stderr '^caller: [0-9a-zA-Z_]+\.go [1-9]'
stderr '^recovered: ya like jazz?'
stderr 'panic: oh noes'
stderr 'funcExported false funcUnexported false'
stderr 'funcStructExported false funcStructUnexported false'
-- go.mod --
module test/main

go 1.19
-- garble_main.go --
package main

import "runtime"
import (
"reflect"
"runtime"
)

type testStruct struct {}

func (testStruct) unexportedFunc() { println("dummy") }

func (testStruct) ExportedFunc() { println("dummy") }

func ExportedFunc() { println("dummy") }

func unexportedFunc() { println("dummy") }

func isEmptyFuncName(i interface{}) bool {
name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
return len(name) == 0
}

func main() {
println("funcExported", isEmptyFuncName(ExportedFunc), "funcUnexported", isEmptyFuncName(unexportedFunc))

var s testStruct
println("funcStructExported", isEmptyFuncName(s.ExportedFunc), "funcStructUnexported", isEmptyFuncName(s.unexportedFunc))

var v any = "tada"
println(v)

Expand Down

0 comments on commit 22c177f

Please sign in to comment.