diff --git a/build_test.go b/build_test.go index eb2197e719..f606b60693 100644 --- a/build_test.go +++ b/build_test.go @@ -337,6 +337,18 @@ func TestBuild(t *testing.T) { emuCheck(t, options) runTest("cgo/", options, t, nil, nil) }) + t.Run("glibc", func(t *testing.T) { + // Link against the system glibc, dynamically, instead of the + // musl that TinyGo builds. This needs the glibc development + // files (crt1.o, libc.so and the headers). + t.Parallel() + if _, err := compileopts.GlibcLibDir(runtime.GOARCH); err != nil { + t.Skip(err) + } + options := optionsFromTarget("", sema) + options.Libc = "glibc" + runTest("glibc/", options, t, nil, nil) + }) } else if runtime.GOOS == "windows" { if runtime.GOARCH != "386" { t.Run("Windows386", func(t *testing.T) { diff --git a/builder/build.go b/builder/build.go index 266179baaa..a821ed0f5f 100644 --- a/builder/build.go +++ b/builder/build.go @@ -171,6 +171,12 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe defer unlock() libcDependencies = append(libcDependencies, dummyCompileJob(filepath.Join(filepath.Dir(libcJob.result), "crt1.o"))) libcDependencies = append(libcDependencies, libcJob) + case "glibc": + libDir, err := compileopts.GlibcLibDir(config.GOARCH()) + if err != nil { + return BuildResult{}, err + } + libcDependencies = append(libcDependencies, glibcStartupJobs(libDir)...) case "picolibc": libcJob, unlock, err := libPicolibc.load(config, tmpdir) if err != nil { diff --git a/builder/glibc.go b/builder/glibc.go new file mode 100644 index 0000000000..9031919bce --- /dev/null +++ b/builder/glibc.go @@ -0,0 +1,19 @@ +package builder + +import ( + "path/filepath" +) + +// The system glibc, linked dynamically. + +// glibcStartupJobs returns the startup objects to link, in the order they must +// appear. crt1.o defines _start, which calls __libc_start_main with the main +// that TinyGo's runtime exports; crti.o and crtn.o bracket the .init and .fini +// sections, so crtn.o has to come last. +func glibcStartupJobs(libDir string) []*compileJob { + var jobs []*compileJob + for _, name := range []string{"crt1.o", "crti.o", "crtn.o"} { + jobs = append(jobs, dummyCompileJob(filepath.Join(libDir, name))) + } + return jobs +} diff --git a/compileopts/config.go b/compileopts/config.go index 944322eb69..63f4e7f7c1 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -483,6 +483,12 @@ func (c *Config) LibcCFlags() []string { "-isystem", filepath.Join(picolibcDir, "stdio"), "-D__PICOLIBC_ERRNO_FUNCTION=__errno_location", } + case "glibc": + // Use the system headers that belong to the installed glibc. The + // gnu triple makes clang look for a GCC install, and cgo treats the + // warning it prints with more than one GCC version as an error. + // Older clang does not know this warning, so silence that too. + return []string{"-Wno-unknown-warning-option", "-Wno-gcc-install-dir-libstdcxx"} case "musl": root := goenv.Get("TINYGOROOT") path := c.LibraryPath("musl") diff --git a/compileopts/options.go b/compileopts/options.go index 79b687db84..dca43a674d 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -10,6 +10,7 @@ import ( var ( validBuildModeOptions = []string{"default", "c-shared", "wasi-legacy"} + validLibcOptions = []string{"musl", "glibc"} validGCOptions = []string{"none", "leaking", "conservative", "custom", "precise", "boehm"} validSchedulerOptions = []string{"none", "tasks", "asyncify", "threads", "cores"} validSerialOptions = []string{"none", "uart", "usb", "rtt"} @@ -34,6 +35,7 @@ type Options struct { TrimPath bool // -trimpath flag Opt string GC string + Libc string // -libc flag PanicStrategy string PanicUnwind string Scheduler string @@ -89,6 +91,15 @@ func (o *Options) Verify() error { } } + if o.Libc != "" { + valid := slices.Contains(validLibcOptions, o.Libc) + if !valid { + return fmt.Errorf(`invalid libc option '%s': valid values are %s`, + o.Libc, + strings.Join(validLibcOptions, ", ")) + } + } + if o.Scheduler != "" { valid := slices.Contains(validSchedulerOptions, o.Scheduler) if !valid { diff --git a/compileopts/target.go b/compileopts/target.go index ab40c68721..e8f6f67e5c 100644 --- a/compileopts/target.go +++ b/compileopts/target.go @@ -193,6 +193,23 @@ func (spec *TargetSpec) resolveInherits() error { // Load a target specification. func LoadTarget(options *Options) (*TargetSpec, error) { + spec, err := loadTarget(options) + if err != nil { + return nil, err + } + // -libc only selects between musl and glibc on the default linux + // target. Reject it where it would otherwise be ignored. + if options.Libc != "" && spec.Libc != options.Libc { + target := options.Target + if target == "" { + target = options.GOOS + "/" + options.GOARCH + } + return nil, fmt.Errorf("-libc=%s is not supported for %s", options.Libc, target) + } + return spec, nil +} + +func loadTarget(options *Options) (*TargetSpec, error) { if options.Target == "" && options.GOARCH == "wasm" { // Set a specific target if we're building from a known GOOS/GOARCH // combination that is defined in a target JSON file. @@ -282,6 +299,50 @@ func GetTargetSpecs() (map[string]*TargetSpec, error) { return maps, nil } +// glibcMultiarchDirs are the Debian and Ubuntu multiarch directories that +// hold libc.so and the crt objects for each GOARCH. +var glibcMultiarchDirs = map[string]string{ + "amd64": "/usr/lib/x86_64-linux-gnu", + "arm64": "/usr/lib/aarch64-linux-gnu", +} + +// glibcLibDirs returns the directories to search for the glibc of goarch. +// Most other distributions use a flat lib64 or lib. Those hold the host +// libraries, so they are only searched for a native build. lib64 comes first +// because on Fedora lib holds the i686 libraries. +func glibcLibDirs(goarch, hostGOARCH string) []string { + dirs := []string{glibcMultiarchDirs[goarch]} + if goarch == hostGOARCH { + dirs = append(dirs, "/usr/lib64", "/usr/lib") + } + return dirs +} + +// GlibcLibDir returns the directory holding the glibc startup objects and +// libc.so for goarch, for -libc=glibc. crt1.o is what it looks for, because +// that is the object that must be linked; libc.so and the other crt files sit +// beside it. +func GlibcLibDir(goarch string) (string, error) { + dirs := glibcLibDirs(goarch, runtime.GOARCH) + for _, dir := range dirs { + if _, err := os.Stat(filepath.Join(dir, "crt1.o")); err == nil { + return dir, nil + } + } + return "", fmt.Errorf("could not find glibc crt1.o for %s in any of %v", goarch, dirs) +} + +// glibcDynamicLinkers is the path to ld.so per GOARCH, used by -libc=glibc. +// These are fixed by each architecture's ABI rather than by the distribution, +// which is why they can be a table rather than something to go looking for. +// Only 64-bit architectures are listed: on 32-bit glibc off_t is 32 bits +// unless _FILE_OFFSET_BITS=64 is set, but the runtime passes a 64-bit offset +// to mmap. +var glibcDynamicLinkers = map[string]string{ + "amd64": "/lib64/ld-linux-x86-64.so.2", + "arm64": "/lib/ld-linux-aarch64.so.1", +} + // Load a target from environment variables (which default to // runtime.GOOS/runtime.GOARCH). func defaultTarget(options *Options) (*TargetSpec, error) { @@ -436,6 +497,22 @@ func defaultTarget(options *Options) (*TargetSpec, error) { spec.Linker = "ld.lld" spec.RTLib = "compiler-rt" spec.Libc = "musl" + if options.Libc == "glibc" { + linker, ok := glibcDynamicLinkers[options.GOARCH] + if !ok { + return nil, fmt.Errorf("-libc=glibc is not supported on GOARCH=%s: supported values are amd64, arm64", options.GOARCH) + } + spec.Libc = "glibc" + libDir, err := GlibcLibDir(options.GOARCH) + if err != nil { + return nil, err + } + // -L because ld.lld does not search the system library + // directories by itself, the way a compiler driver would. + // Without --dynamic-linker lld emits no PT_INTERP, and the + // shared libraries are never resolved. + spec.LDFlags = append(spec.LDFlags, "-L"+libDir, "-lc", "--dynamic-linker="+linker) + } spec.LDFlags = append(spec.LDFlags, "--gc-sections") if options.GOARCH == "arm64" { // Disable outline atomics. For details, see: @@ -509,12 +586,19 @@ func defaultTarget(options *Options) (*TargetSpec, error) { if options.GOOS == "windows" { spec.Triple += "-gnu" } else if options.GOOS == "linux" { - // We use musl on Linux (not glibc) so we should use -musleabi* instead - // of -gnueabi*. + // We use musl on Linux by default (not glibc) so we should use + // -musleabi* instead of -gnueabi*. Under -libc=glibc it is the + // other way around: the triple has to say gnu, both so that clang + // lays out ABI-visible types the way glibc does and so that the + // cached compiler-rt and libc builds are keyed separately from the + // musl ones. // The *hf suffix selects between soft/hard floating point ABI. - if spec.SoftFloat { + switch { + case spec.Libc == "glibc": + spec.Triple += "-gnu" + case spec.SoftFloat: spec.Triple += "-musleabi" - } else { + default: spec.Triple += "-musleabihf" } } diff --git a/compileopts/target_test.go b/compileopts/target_test.go index 86dae1f708..371d12e506 100644 --- a/compileopts/target_test.go +++ b/compileopts/target_test.go @@ -258,3 +258,45 @@ func TestConfigPanicUnwind(t *testing.T) { }) } } + +func TestLoadTargetLibc(t *testing.T) { + tests := []struct { + name string + options Options + }{ + {"glibc with a target", Options{Target: "arduino", Libc: "glibc"}}, + {"musl with a target", Options{Target: "wasip1", Libc: "musl"}}, + {"glibc on windows", Options{GOOS: "windows", GOARCH: "amd64", Libc: "glibc"}}, + {"glibc on 386", Options{GOOS: "linux", GOARCH: "386", Libc: "glibc"}}, + {"glibc on arm", Options{GOOS: "linux", GOARCH: "arm", GOARM: "6", Libc: "glibc"}}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if _, err := LoadTarget(&tc.options); err == nil { + t.Errorf("LoadTarget should reject -libc=%s", tc.options.Libc) + } + }) + } + + if _, err := LoadTarget(&Options{GOOS: "linux", GOARCH: "amd64", Libc: "musl"}); err != nil { + t.Errorf("LoadTarget should accept -libc=musl on linux/amd64: %v", err) + } +} + +func TestGlibcLibDirs(t *testing.T) { + tests := []struct { + goarch, hostGOARCH string + want []string + }{ + {"amd64", "amd64", []string{"/usr/lib/x86_64-linux-gnu", "/usr/lib64", "/usr/lib"}}, + {"arm64", "arm64", []string{"/usr/lib/aarch64-linux-gnu", "/usr/lib64", "/usr/lib"}}, + {"arm64", "amd64", []string{"/usr/lib/aarch64-linux-gnu"}}, + {"amd64", "arm64", []string{"/usr/lib/x86_64-linux-gnu"}}, + } + for _, tc := range tests { + got := glibcLibDirs(tc.goarch, tc.hostGOARCH) + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("glibcLibDirs(%q, %q) = %v, want %v", tc.goarch, tc.hostGOARCH, got, tc.want) + } + } +} diff --git a/main.go b/main.go index 4c04b793a6..255ed9c702 100644 --- a/main.go +++ b/main.go @@ -1779,6 +1779,7 @@ func main() { opt := flag.String("opt", "z", "optimization level: 0, 1, 2, s, z") gc := flag.String("gc", "", "garbage collector to use (none, leaking, conservative, custom, precise, boehm)") + libc := flag.String("libc", "", "libc to use on linux (musl, glibc)") panicStrategy := flag.String("panic", "print", "panic strategy (print, trap)") panicUnwind := flag.String("panic-unwind", "", "panic unwind strategy (auto, explicit, none)") scheduler := flag.String("scheduler", "", "which scheduler to use (none, tasks, cores, threads, asyncify)") @@ -1961,6 +1962,7 @@ func main() { Monitor: *monitor, BaudRate: *baudrate, Timeout: *timeout, + Libc: *libc, WITPackage: witPackage, WITWorld: witWorld, GoCompatibility: *gocompatibility, diff --git a/testdata/glibc/main.go b/testdata/glibc/main.go new file mode 100644 index 0000000000..d5a97359ad --- /dev/null +++ b/testdata/glibc/main.go @@ -0,0 +1,57 @@ +package main + +// This program is built with -libc=glibc. It calls into the system glibc, which +// is linked dynamically, so it only runs if the binary names the dynamic linker +// and if glibc owns the thread pointer. + +/* +#include +#include +#include +#include + +// rangeErrno returns the errno that strtol sets for a value that does not fit +// in a long. errno is a thread-local variable in glibc, so this reads glibc's +// TLS through the thread pointer. +static int rangeErrno(void) { + errno = 0; + strtol("99999999999999999999999999", NULL, 10); + return errno; +} +*/ +import "C" + +import ( + "runtime" + "strings" + "unsafe" +) + +func main() { + // gnu_get_libc_version exists only in glibc, so this does not link with + // musl. + version := C.GoString(C.gnu_get_libc_version()) + println("glibc version 2.x:", strings.HasPrefix(version, "2.")) + + // malloc and free use the tcache, which glibc keeps in TLS. + cs := C.CString("hello from glibc") + dup := C.strdup(cs) + println("strdup:", C.GoString(dup)) + C.free(unsafe.Pointer(dup)) + C.free(unsafe.Pointer(cs)) + + println("errno is ERANGE:", C.rangeErrno() == C.ERANGE) + + // Do the same on a goroutine, which has its own thread under the threads + // scheduler, and thus its own glibc TLS block. + done := make(chan bool) + go func() { + p := C.malloc(64) + C.free(p) + done <- C.rangeErrno() == C.ERANGE + }() + println("errno is ERANGE on goroutine:", <-done) + + runtime.GC() + println("done") +} diff --git a/testdata/glibc/out.txt b/testdata/glibc/out.txt new file mode 100644 index 0000000000..37f14b28a8 --- /dev/null +++ b/testdata/glibc/out.txt @@ -0,0 +1,5 @@ +glibc version 2.x: true +strdup: hello from glibc +errno is ERANGE: true +errno is ERANGE on goroutine: true +done