runtime: query thread stack size from OS on Windows

Currently, on Windows, the thread stack size is set or assumed in many
different places. In non-cgo binaries, both the Go linker and the
runtime have a copy of the stack size, the Go linker sets the size of
the main thread stack, and the runtime sets the size of other thread
stacks. In cgo binaries, the external linker sets the main thread
stack size, the runtime assumes the size of the main thread stack will
be the same as used by the Go linker, and the cgo entry code assumes
the same.

Furthermore, users can change the main thread stack size using
editbin, so the runtime doesn't even really know what size it is, and
user C code can create threads with unknown thread stack sizes, which
we also assume have the same default stack size.

This is all a mess.

Fix the corner cases of this and the duplication of knowledge between
the linker and the runtime by querying the OS for the stack bounds
during thread setup. Furthermore, we unify all of this into just
runtime.minit for both cgo and non-cgo binaries and for the main
thread, other runtime-created threads, and C-created threads.

Updates #20975.

Change-Id: I45dbee2b5ea2ae721a85a27680737ff046f9d464
Reviewed-on: https://go-review.googlesource.com/120336
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Alex Brainman <alex.brainman@gmail.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
This commit is contained in:
Austin Clements 2018-06-21 12:08:36 -04:00
parent 52e782a2d6
commit 99e9be8043
18 changed files with 208 additions and 50 deletions

View file

@ -845,15 +845,18 @@ func (f *peFile) writeOptionalHeader(ctxt *Link) {
// and system calls even in "pure" Go code are actually C
// calls that may need more stack than we think.
//
// The default stack reserve size affects only the main
// The default stack reserve size directly affects only the main
// thread, ctrlhandler thread, and profileloop thread. For
// these, it must be greater than the stack size assumed by
// externalthreadhandler.
//
// For other threads we specify stack size in runtime explicitly.
// For these, the reserve must match STACKSIZE in
// runtime/cgo/gcc_windows_{386,amd64}.c and osStackSize in
// runtime/os_windows.go.
// For other threads, the runtime explicitly asks the kernel
// to use the default stack size so that all stacks are
// consistent.
//
// At thread start, in minit, the runtime queries the OS for
// the actual stack bounds so that the stack size doesn't need
// to be hard-coded into the runtime.
oh64.SizeOfStackReserve = 0x00200000
if !iscgo {
oh64.SizeOfStackCommit = 0x00001000

View file

@ -22,5 +22,3 @@ runtime/duff_386.s: [386] duffcopy: function duffcopy missing Go declaration
runtime/asm_386.s: [386] uint32tofloat64: function uint32tofloat64 missing Go declaration
runtime/asm_386.s: [386] float64touint32: function float64touint32 missing Go declaration
runtime/asm_386.s: [386] stackcheck: function stackcheck missing Go declaration

View file

@ -20,4 +20,3 @@ runtime/asm_amd64.s: [amd64] aeshashbody: function aeshashbody missing Go declar
runtime/asm_amd64.s: [amd64] addmoduledata: function addmoduledata missing Go declaration
runtime/duff_amd64.s: [amd64] duffzero: function duffzero missing Go declaration
runtime/duff_amd64.s: [amd64] duffcopy: function duffcopy missing Go declaration
runtime/asm_amd64.s: [amd64] stackcheck: function stackcheck missing Go declaration

View file

@ -24,5 +24,3 @@ runtime/asm_amd64p32.s: [amd64p32] rt0_go: unknown variable argc
runtime/asm_amd64p32.s: [amd64p32] rt0_go: unknown variable argv
runtime/asm_amd64p32.s: [amd64p32] asmcgocall: RET without writing to 4-byte ret+8(FP)
runtime/asm_amd64p32.s: [amd64p32] stackcheck: function stackcheck missing Go declaration

View file

@ -11,16 +11,9 @@
static void threadentry(void*);
/* 1MB is default stack size for 32-bit Windows.
Allocation granularity on Windows is typically 64 KB.
This constant must match SizeOfStackReserve in ../cmd/link/internal/ld/pe.go. */
#define STACKSIZE (1*1024*1024)
void
x_cgo_init(G *g)
{
int tmp;
g->stacklo = (uintptr)&tmp - STACKSIZE + 8*1024;
}
@ -44,8 +37,7 @@ threadentry(void *v)
ts = *(ThreadStart*)v;
free(v);
ts.g->stackhi = (uintptr)&ts;
ts.g->stacklo = (uintptr)&ts - STACKSIZE + 8*1024;
// minit queries stack bounds from the OS.
/*
* Set specific keys in thread local storage.

View file

@ -11,16 +11,9 @@
static void threadentry(void*);
/* 2MB is default stack size for 64-bit Windows.
Allocation granularity on Windows is typically 64 KB.
This constant must match SizeOfStackReserve in ../cmd/link/internal/ld/pe.go. */
#define STACKSIZE (2*1024*1024)
void
x_cgo_init(G *g)
{
int tmp;
g->stacklo = (uintptr)&tmp - STACKSIZE + 8*1024;
}
@ -44,8 +37,7 @@ threadentry(void *v)
ts = *(ThreadStart*)v;
free(v);
ts.g->stackhi = (uintptr)&ts;
ts.g->stacklo = (uintptr)&ts - STACKSIZE + 8*1024;
// minit queries stack bounds from the OS.
/*
* Set specific keys in thread local storage.

View file

@ -489,3 +489,19 @@ func TestCgoTracebackSigpanic(t *testing.T) {
t.Fatalf("failure incorrectly contains %q. output:\n%s\n", nowant, got)
}
}
// Test that C code called via cgo can use large Windows thread stacks
// and call back in to Go without crashing. See issue #20975.
//
// See also TestBigStackCallbackSyscall.
func TestBigStackCallbackCgo(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("skipping windows specific test")
}
t.Parallel()
got := runTestProg(t, "testprogcgo", "BigStack")
want := "OK\n"
if got != want {
t.Errorf("expected %q got %v", want, got)
}
}

View file

@ -71,3 +71,4 @@ type FloatingSaveArea C.FLOATING_SAVE_AREA
type M128a C.M128A
type Context C.CONTEXT
type Overlapped C.OVERLAPPED
type MemoryBasicInformation C.MEMORY_BASIC_INFORMATION

View file

@ -129,3 +129,13 @@ type overlapped struct {
anon0 [8]byte
hevent *byte
}
type memoryBasicInformation struct {
baseAddress uintptr
allocationBase uintptr
allocationProtect uint32
regionSize uintptr
state uint32
protect uint32
type_ uint32
}

View file

@ -151,3 +151,13 @@ type overlapped struct {
anon0 [8]byte
hevent *byte
}
type memoryBasicInformation struct {
baseAddress uintptr
allocationBase uintptr
allocationProtect uint32
regionSize uintptr
state uint32
protect uint32
type_ uint32
}

View file

@ -45,6 +45,7 @@ const (
//go:cgo_import_dynamic runtime._SwitchToThread SwitchToThread%0 "kernel32.dll"
//go:cgo_import_dynamic runtime._VirtualAlloc VirtualAlloc%4 "kernel32.dll"
//go:cgo_import_dynamic runtime._VirtualFree VirtualFree%3 "kernel32.dll"
//go:cgo_import_dynamic runtime._VirtualQuery VirtualQuery%3 "kernel32.dll"
//go:cgo_import_dynamic runtime._WSAGetOverlappedResult WSAGetOverlappedResult%5 "ws2_32.dll"
//go:cgo_import_dynamic runtime._WaitForSingleObject WaitForSingleObject%2 "kernel32.dll"
//go:cgo_import_dynamic runtime._WriteConsoleW WriteConsoleW%5 "kernel32.dll"
@ -92,6 +93,7 @@ var (
_SwitchToThread,
_VirtualAlloc,
_VirtualFree,
_VirtualQuery,
_WSAGetOverlappedResult,
_WaitForSingleObject,
_WriteConsoleW,
@ -291,9 +293,6 @@ func osRelax(relax bool) uint32 {
}
}
// osStackSize must match SizeOfStackReserve in ../cmd/link/internal/ld/pe.go.
var osStackSize uintptr = 0x00200000*_64bit + 0x00100000*(1-_64bit)
func osinit() {
asmstdcallAddr = unsafe.Pointer(funcPC(asmstdcall))
usleep2Addr = unsafe.Pointer(funcPC(usleep2))
@ -322,18 +321,6 @@ func osinit() {
// equivalent threads that all do a mix of GUI, IO, computations, etc.
// In such context dynamic priority boosting does nothing but harm, so we turn it off.
stdcall2(_SetProcessPriorityBoost, currentProcess, 1)
// Fix the entry thread's stack bounds, since runtime entry
// assumed we were on a tiny stack. If this is a cgo binary,
// x_cgo_init already fixed these.
if !iscgo {
// Leave 8K of slop for calling C functions that don't
// have stack checks. We shouldn't be anywhere near
// this bound anyway.
g0.stack.lo = g0.stack.hi - osStackSize + 8*1024
g0.stackguard0 = g0.stack.lo + _StackGuard
g0.stackguard1 = g0.stackguard0
}
}
func nanotime() int64
@ -634,10 +621,10 @@ func semacreate(mp *m) {
//go:nowritebarrierrec
//go:nosplit
func newosproc(mp *m) {
const _STACK_SIZE_PARAM_IS_A_RESERVATION = 0x00010000
thandle := stdcall6(_CreateThread, 0, osStackSize,
// We pass 0 for the stack size to use the default for this binary.
thandle := stdcall6(_CreateThread, 0, 0,
funcPC(tstart_stdcall), uintptr(unsafe.Pointer(mp)),
_STACK_SIZE_PARAM_IS_A_RESERVATION, 0)
0, 0)
if thandle == 0 {
if atomic.Load(&exiting) != 0 {
@ -702,6 +689,30 @@ func minit() {
var thandle uintptr
stdcall7(_DuplicateHandle, currentProcess, currentThread, currentProcess, uintptr(unsafe.Pointer(&thandle)), 0, 0, _DUPLICATE_SAME_ACCESS)
atomic.Storeuintptr(&getg().m.thread, thandle)
// Query the true stack base from the OS. Currently we're
// running on a small assumed stack.
var mbi memoryBasicInformation
res := stdcall3(_VirtualQuery, uintptr(unsafe.Pointer(&mbi)), uintptr(unsafe.Pointer(&mbi)), unsafe.Sizeof(mbi))
if res == 0 {
print("runtime: VirtualQuery failed; errno=", getlasterror(), "\n")
throw("VirtualQuery for stack base failed")
}
// Add 8K of slop for calling C functions that don't have
// stack checks. We shouldn't be anywhere near this bound
// anyway.
base := mbi.allocationBase + 8*1024
// Sanity check the stack bounds.
g0 := getg()
if base > g0.stack.hi || g0.stack.hi-base > 64<<20 {
print("runtime: g0 stack [", hex(base), ",", hex(g0.stack.hi), ")\n")
throw("bad g0 stack")
}
g0.stack.lo = base
g0.stackguard0 = g0.stack.lo + _StackGuard
g0.stackguard1 = g0.stackguard0
// Sanity check the SP.
stackcheck()
}
// Called from dropm to undo the effect of an minit.

View file

@ -1233,6 +1233,7 @@ func mstart() {
if osStack {
// Initialize stack bounds from system stack.
// Cgo may have left stack size in stack.hi.
// minit may update the stack bounds.
size := _g_.stack.hi
if size == 0 {
size = 8192 * sys.StackGuardMultiplier

10
src/runtime/stubs_x86.go Normal file
View file

@ -0,0 +1,10 @@
// Copyright 2018 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.
// +build amd64 amd64p32 386
package runtime
// stackcheck checks that SP is in range [g->stack.lo, g->stack.hi).
func stackcheck()

View file

@ -315,8 +315,7 @@ TEXT runtime·tstart(SB),NOSPLIT,$0
// Layout new m scheduler stack on os stack.
MOVL SP, AX
MOVL AX, (g_stack+stack_hi)(DX)
SUBL runtime·osStackSize(SB), AX // stack size
ADDL $(8*1024), AX // slop for calling C
SUBL $(64*1024), AX // initial stack size (adjusted later)
MOVL AX, (g_stack+stack_lo)(DX)
ADDL $const__StackGuard, AX
MOVL AX, g_stackguard0(DX)

View file

@ -363,8 +363,7 @@ TEXT runtime·tstart_stdcall(SB),NOSPLIT,$0
// Layout new m scheduler stack on os stack.
MOVQ SP, AX
MOVQ AX, (g_stack+stack_hi)(DX)
SUBQ runtime·osStackSize(SB), AX // stack size
ADDQ $(8*1024), AX // slop for calling C
SUBQ $(64*1024), AX // inital stack size (adjusted later)
MOVQ AX, (g_stack+stack_lo)(DX)
ADDQ $const__StackGuard, AX
MOVQ AX, g_stackguard0(DX)

View file

@ -957,6 +957,52 @@ uintptr_t cfunc() {
}
}
// Test that C code called via a DLL can use large Windows thread
// stacks and call back in to Go without crashing. See issue #20975.
//
// See also TestBigStackCallbackCgo.
func TestBigStackCallbackSyscall(t *testing.T) {
if _, err := exec.LookPath("gcc"); err != nil {
t.Skip("skipping test: gcc is missing")
}
srcname, err := filepath.Abs("testdata/testprogcgo/bigstack_windows.c")
if err != nil {
t.Fatal("Abs failed: ", err)
}
tmpdir, err := ioutil.TempDir("", "TestBigStackCallback")
if err != nil {
t.Fatal("TempDir failed: ", err)
}
defer os.RemoveAll(tmpdir)
outname := "mydll.dll"
cmd := exec.Command("gcc", "-shared", "-s", "-Werror", "-o", outname, srcname)
cmd.Dir = tmpdir
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("failed to build dll: %v - %v", err, string(out))
}
dllpath := filepath.Join(tmpdir, outname)
dll := syscall.MustLoadDLL(dllpath)
defer dll.Release()
var ok bool
proc := dll.MustFindProc("bigStack")
cb := syscall.NewCallback(func() uintptr {
// Do something interesting to force stack checks.
forceStackCopy()
ok = true
return 0
})
proc.Call(cb)
if !ok {
t.Fatalf("callback not called")
}
}
// wantLoadLibraryEx reports whether we expect LoadLibraryEx to work for tests.
func wantLoadLibraryEx() bool {
return testenv.Builder() == "windows-amd64-gce" || testenv.Builder() == "windows-386-gce"

View file

@ -0,0 +1,46 @@
// Copyright 2018 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 test source is used by both TestBigStackCallbackCgo (linked
// directly into the Go binary) and TestBigStackCallbackSyscall
// (compiled into a DLL).
#include <windows.h>
#include <stdio.h>
#ifndef STACK_SIZE_PARAM_IS_A_RESERVATION
#define STACK_SIZE_PARAM_IS_A_RESERVATION 0x00010000
#endif
typedef void callback(char*);
// Allocate a stack that's much larger than the default.
static const int STACK_SIZE = 16<<20;
static callback *bigStackCallback;
static void useStack(int bytes) {
// Windows doesn't like huge frames, so we grow the stack 64k at a time.
char x[64<<10];
if (bytes < sizeof x) {
bigStackCallback(x);
} else {
useStack(bytes - sizeof x);
}
}
static DWORD WINAPI threadEntry(LPVOID lpParam) {
useStack(STACK_SIZE - (128<<10));
return 0;
}
void bigStack(callback *cb) {
bigStackCallback = cb;
HANDLE hThread = CreateThread(NULL, STACK_SIZE, threadEntry, NULL, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL);
if (hThread == NULL) {
fprintf(stderr, "CreateThread failed\n");
exit(1);
}
WaitForSingleObject(hThread, INFINITE);
}

View file

@ -0,0 +1,27 @@
// Copyright 2018 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.
package main
/*
typedef void callback(char*);
extern void goBigStack1(char*);
extern void bigStack(callback*);
*/
import "C"
func init() {
register("BigStack", BigStack)
}
func BigStack() {
// Create a large thread stack and call back into Go to test
// if Go correctly determines the stack bounds.
C.bigStack((*C.callback)(C.goBigStack1))
}
//export goBigStack1
func goBigStack1(x *C.char) {
println("OK")
}