Commit 17a108ba authored by Alex Brainman's avatar Alex Brainman

runtime: handle all windows exception

Fixes #8006.

LGTM=rsc
R=golang-codereviews, rsc
CC=golang-codereviews
https://golang.org/cl/145150043
parent 9ca83684
...@@ -59,6 +59,9 @@ const ( ...@@ -59,6 +59,9 @@ const (
INFINITE = C.INFINITE INFINITE = C.INFINITE
WAIT_TIMEOUT = C.WAIT_TIMEOUT WAIT_TIMEOUT = C.WAIT_TIMEOUT
EXCEPTION_CONTINUE_EXECUTION = C.EXCEPTION_CONTINUE_EXECUTION
EXCEPTION_CONTINUE_SEARCH = C.EXCEPTION_CONTINUE_SEARCH
) )
type SystemInfo C.SYSTEM_INFO type SystemInfo C.SYSTEM_INFO
......
...@@ -32,6 +32,9 @@ enum { ...@@ -32,6 +32,9 @@ enum {
INFINITE = 0xffffffff, INFINITE = 0xffffffff,
WAIT_TIMEOUT = 0x102, WAIT_TIMEOUT = 0x102,
EXCEPTION_CONTINUE_EXECUTION = -0x1,
EXCEPTION_CONTINUE_SEARCH = 0x0,
}; };
typedef struct SystemInfo SystemInfo; typedef struct SystemInfo SystemInfo;
......
...@@ -32,6 +32,9 @@ enum { ...@@ -32,6 +32,9 @@ enum {
INFINITE = 0xffffffff, INFINITE = 0xffffffff,
WAIT_TIMEOUT = 0x102, WAIT_TIMEOUT = 0x102,
EXCEPTION_CONTINUE_EXECUTION = -0x1,
EXCEPTION_CONTINUE_SEARCH = 0x0,
}; };
typedef struct SystemInfo SystemInfo; typedef struct SystemInfo SystemInfo;
......
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
#pragma dynimport runtime·SetEvent SetEvent "kernel32.dll" #pragma dynimport runtime·SetEvent SetEvent "kernel32.dll"
#pragma dynimport runtime·SetProcessPriorityBoost SetProcessPriorityBoost "kernel32.dll" #pragma dynimport runtime·SetProcessPriorityBoost SetProcessPriorityBoost "kernel32.dll"
#pragma dynimport runtime·SetThreadPriority SetThreadPriority "kernel32.dll" #pragma dynimport runtime·SetThreadPriority SetThreadPriority "kernel32.dll"
#pragma dynimport runtime·SetUnhandledExceptionFilter SetUnhandledExceptionFilter "kernel32.dll"
#pragma dynimport runtime·SetWaitableTimer SetWaitableTimer "kernel32.dll" #pragma dynimport runtime·SetWaitableTimer SetWaitableTimer "kernel32.dll"
#pragma dynimport runtime·Sleep Sleep "kernel32.dll" #pragma dynimport runtime·Sleep Sleep "kernel32.dll"
#pragma dynimport runtime·SuspendThread SuspendThread "kernel32.dll" #pragma dynimport runtime·SuspendThread SuspendThread "kernel32.dll"
...@@ -65,6 +66,7 @@ extern void *runtime·SetConsoleCtrlHandler; ...@@ -65,6 +66,7 @@ extern void *runtime·SetConsoleCtrlHandler;
extern void *runtime·SetEvent; extern void *runtime·SetEvent;
extern void *runtime·SetProcessPriorityBoost; extern void *runtime·SetProcessPriorityBoost;
extern void *runtime·SetThreadPriority; extern void *runtime·SetThreadPriority;
extern void *runtime·SetUnhandledExceptionFilter;
extern void *runtime·SetWaitableTimer; extern void *runtime·SetWaitableTimer;
extern void *runtime·Sleep; extern void *runtime·Sleep;
extern void *runtime·SuspendThread; extern void *runtime·SuspendThread;
...@@ -77,7 +79,9 @@ void *runtime·GetQueuedCompletionStatusEx; ...@@ -77,7 +79,9 @@ void *runtime·GetQueuedCompletionStatusEx;
extern uintptr runtime·externalthreadhandlerp; extern uintptr runtime·externalthreadhandlerp;
void runtime·externalthreadhandler(void); void runtime·externalthreadhandler(void);
void runtime·sigtramp(void); void runtime·exceptiontramp(void);
void runtime·firstcontinuetramp(void);
void runtime·lastcontinuetramp(void);
#pragma textflag NOSPLIT #pragma textflag NOSPLIT
uintptr uintptr
...@@ -106,12 +110,28 @@ void ...@@ -106,12 +110,28 @@ void
runtime·osinit(void) runtime·osinit(void)
{ {
void *kernel32; void *kernel32;
void *addVectoredContinueHandler = nil;
kernel32 = runtime·stdcall1(runtime·LoadLibraryA, (uintptr)"kernel32.dll");
runtime·externalthreadhandlerp = (uintptr)runtime·externalthreadhandler; runtime·externalthreadhandlerp = (uintptr)runtime·externalthreadhandler;
runtime·stdcall2(runtime·AddVectoredExceptionHandler, 1, (uintptr)runtime·sigtramp); runtime·stdcall2(runtime·AddVectoredExceptionHandler, 1, (uintptr)runtime·exceptiontramp);
if(kernel32 != nil)
addVectoredContinueHandler = runtime·stdcall2(runtime·GetProcAddress, (uintptr)kernel32, (uintptr)"AddVectoredContinueHandler");
if(addVectoredContinueHandler == nil)
// use SetUnhandledExceptionFilter if VectoredContinueHandler is unavailable.
// note: SetUnhandledExceptionFilter handler won't be called, if debugging.
runtime·stdcall1(runtime·SetUnhandledExceptionFilter, (uintptr)runtime·lastcontinuetramp);
else {
runtime·stdcall2(addVectoredContinueHandler, 1, (uintptr)runtime·firstcontinuetramp);
runtime·stdcall2(addVectoredContinueHandler, 0, (uintptr)runtime·lastcontinuetramp);
}
runtime·stdcall2(runtime·SetConsoleCtrlHandler, (uintptr)runtime·ctrlhandler, 1); runtime·stdcall2(runtime·SetConsoleCtrlHandler, (uintptr)runtime·ctrlhandler, 1);
runtime·stdcall1(runtime·timeBeginPeriod, 1); runtime·stdcall1(runtime·timeBeginPeriod, 1);
runtime·ncpu = getproccount(); runtime·ncpu = getproccount();
// Windows dynamic priority boosting assumes that a process has different types // Windows dynamic priority boosting assumes that a process has different types
...@@ -120,7 +140,6 @@ runtime·osinit(void) ...@@ -120,7 +140,6 @@ runtime·osinit(void)
// In such context dynamic priority boosting does nothing but harm, so we turn it off. // In such context dynamic priority boosting does nothing but harm, so we turn it off.
runtime·stdcall2(runtime·SetProcessPriorityBoost, -1, 1); runtime·stdcall2(runtime·SetProcessPriorityBoost, -1, 1);
kernel32 = runtime·stdcall1(runtime·LoadLibraryA, (uintptr)"kernel32.dll");
if(kernel32 != nil) { if(kernel32 != nil) {
runtime·GetQueuedCompletionStatusEx = runtime·stdcall2(runtime·GetProcAddress, (uintptr)kernel32, (uintptr)"GetQueuedCompletionStatusEx"); runtime·GetQueuedCompletionStatusEx = runtime·stdcall2(runtime·GetProcAddress, (uintptr)kernel32, (uintptr)"GetQueuedCompletionStatusEx");
} }
...@@ -475,10 +494,14 @@ runtime·issigpanic(uint32 code) ...@@ -475,10 +494,14 @@ runtime·issigpanic(uint32 code)
void void
runtime·initsig(void) runtime·initsig(void)
{ {
// following line keeps sigtramp alive at link stage // following line keeps these functions alive at link stage
// if there's a better way please write it here // if there's a better way please write it here
void *p = runtime·sigtramp; void *e = runtime·exceptiontramp;
USED(p); void *f = runtime·firstcontinuetramp;
void *l = runtime·lastcontinuetramp;
USED(e);
USED(f);
USED(l);
} }
uint32 uint32
......
...@@ -24,45 +24,63 @@ runtime·dumpregs(Context *r) ...@@ -24,45 +24,63 @@ runtime·dumpregs(Context *r)
runtime·printf("gs %x\n", r->SegGs); runtime·printf("gs %x\n", r->SegGs);
} }
// Called by sigtramp from Windows VEH handler. bool
// Return value signals whether the exception has been handled (-1) runtime·isgoexception(ExceptionRecord *info, Context *r)
// or should be made available to other handlers in the chain (0).
uint32
runtime·sighandler(ExceptionRecord *info, Context *r, G *gp)
{ {
bool crash;
uintptr *sp;
extern byte runtime·text[], runtime·etext[]; extern byte runtime·text[], runtime·etext[];
// Only handle exception if executing instructions in Go binary // Only handle exception if executing instructions in Go binary
// (not Windows library code). // (not Windows library code).
if(r->Eip < (uint32)runtime·text || (uint32)runtime·etext < r->Eip) if(r->Eip < (uint32)runtime·text || (uint32)runtime·etext < r->Eip)
return 0; return false;
if(gp != nil && runtime·issigpanic(info->ExceptionCode)) { if(!runtime·issigpanic(info->ExceptionCode))
// Make it look like a call to the signal func. return false;
// Have to pass arguments out of band since
// augmenting the stack frame would break return true;
// the unwinding code. }
gp->sig = info->ExceptionCode;
gp->sigcode0 = info->ExceptionInformation[0]; // Called by sigtramp from Windows VEH handler.
gp->sigcode1 = info->ExceptionInformation[1]; // Return value signals whether the exception has been handled (EXCEPTION_CONTINUE_EXECUTION)
gp->sigpc = r->Eip; // or should be made available to other handlers in the chain (EXCEPTION_CONTINUE_SEARCH).
uint32
// Only push runtime·sigpanic if r->eip != 0. runtime·exceptionhandler(ExceptionRecord *info, Context *r, G *gp)
// If r->eip == 0, probably panicked because of a {
// call to a nil func. Not pushing that onto sp will uintptr *sp;
// make the trace look like a call to runtime·sigpanic instead.
// (Otherwise the trace will end at runtime·sigpanic and we if(!runtime·isgoexception(info, r))
// won't get to see who faulted.) return EXCEPTION_CONTINUE_SEARCH;
if(r->Eip != 0) {
sp = (uintptr*)r->Esp; // Make it look like a call to the signal func.
*--sp = r->Eip; // Have to pass arguments out of band since
r->Esp = (uintptr)sp; // augmenting the stack frame would break
} // the unwinding code.
r->Eip = (uintptr)runtime·sigpanic; gp->sig = info->ExceptionCode;
return -1; gp->sigcode0 = info->ExceptionInformation[0];
gp->sigcode1 = info->ExceptionInformation[1];
gp->sigpc = r->Eip;
// Only push runtime·sigpanic if r->eip != 0.
// If r->eip == 0, probably panicked because of a
// call to a nil func. Not pushing that onto sp will
// make the trace look like a call to runtime·sigpanic instead.
// (Otherwise the trace will end at runtime·sigpanic and we
// won't get to see who faulted.)
if(r->Eip != 0) {
sp = (uintptr*)r->Esp;
*--sp = r->Eip;
r->Esp = (uintptr)sp;
} }
r->Eip = (uintptr)runtime·sigpanic;
return EXCEPTION_CONTINUE_EXECUTION;
}
// lastcontinuehandler is reached, because runtime cannot handle
// current exception. lastcontinuehandler will print crash info and exit.
uint32
runtime·lastcontinuehandler(ExceptionRecord *info, Context *r, G *gp)
{
bool crash;
if(runtime·panicking) // traceback already printed if(runtime·panicking) // traceback already printed
runtime·exit(2); runtime·exit(2);
...@@ -88,7 +106,7 @@ runtime·sighandler(ExceptionRecord *info, Context *r, G *gp) ...@@ -88,7 +106,7 @@ runtime·sighandler(ExceptionRecord *info, Context *r, G *gp)
runtime·crash(); runtime·crash();
runtime·exit(2); runtime·exit(2);
return -1; // not reached return 0; // not reached
} }
void void
......
...@@ -32,45 +32,76 @@ runtime·dumpregs(Context *r) ...@@ -32,45 +32,76 @@ runtime·dumpregs(Context *r)
runtime·printf("gs %X\n", (uint64)r->SegGs); runtime·printf("gs %X\n", (uint64)r->SegGs);
} }
// Called by sigtramp from Windows VEH handler. bool
// Return value signals whether the exception has been handled (-1) runtime·isgoexception(ExceptionRecord *info, Context *r)
// or should be made available to other handlers in the chain (0).
uint32
runtime·sighandler(ExceptionRecord *info, Context *r, G *gp)
{ {
bool crash;
uintptr *sp;
extern byte runtime·text[], runtime·etext[]; extern byte runtime·text[], runtime·etext[];
// Only handle exception if executing instructions in Go binary // Only handle exception if executing instructions in Go binary
// (not Windows library code). // (not Windows library code).
if(r->Rip < (uint64)runtime·text || (uint64)runtime·etext < r->Rip) if(r->Rip < (uint64)runtime·text || (uint64)runtime·etext < r->Rip)
return 0; return false;
if(gp != nil && runtime·issigpanic(info->ExceptionCode)) { if(!runtime·issigpanic(info->ExceptionCode))
// Make it look like a call to the signal func. return false;
// Have to pass arguments out of band since
// augmenting the stack frame would break return true;
// the unwinding code. }
gp->sig = info->ExceptionCode;
gp->sigcode0 = info->ExceptionInformation[0]; // Called by sigtramp from Windows VEH handler.
gp->sigcode1 = info->ExceptionInformation[1]; // Return value signals whether the exception has been handled (EXCEPTION_CONTINUE_EXECUTION)
gp->sigpc = r->Rip; // or should be made available to other handlers in the chain (EXCEPTION_CONTINUE_SEARCH).
uint32
// Only push runtime·sigpanic if r->rip != 0. runtime·exceptionhandler(ExceptionRecord *info, Context *r, G *gp)
// If r->rip == 0, probably panicked because of a {
// call to a nil func. Not pushing that onto sp will uintptr *sp;
// make the trace look like a call to runtime·sigpanic instead.
// (Otherwise the trace will end at runtime·sigpanic and we if(!runtime·isgoexception(info, r))
// won't get to see who faulted.) return EXCEPTION_CONTINUE_SEARCH;
if(r->Rip != 0) {
sp = (uintptr*)r->Rsp; // Make it look like a call to the signal func.
*--sp = r->Rip; // Have to pass arguments out of band since
r->Rsp = (uintptr)sp; // augmenting the stack frame would break
} // the unwinding code.
r->Rip = (uintptr)runtime·sigpanic; gp->sig = info->ExceptionCode;
return -1; gp->sigcode0 = info->ExceptionInformation[0];
gp->sigcode1 = info->ExceptionInformation[1];
gp->sigpc = r->Rip;
// Only push runtime·sigpanic if r->rip != 0.
// If r->rip == 0, probably panicked because of a
// call to a nil func. Not pushing that onto sp will
// make the trace look like a call to runtime·sigpanic instead.
// (Otherwise the trace will end at runtime·sigpanic and we
// won't get to see who faulted.)
if(r->Rip != 0) {
sp = (uintptr*)r->Rsp;
*--sp = r->Rip;
r->Rsp = (uintptr)sp;
} }
r->Rip = (uintptr)runtime·sigpanic;
return EXCEPTION_CONTINUE_EXECUTION;
}
// It seems Windows searches ContinueHandler's list even
// if ExceptionHandler returns EXCEPTION_CONTINUE_EXECUTION.
// firstcontinuehandler will stop that search,
// if exceptionhandler did the same earlier.
uint32
runtime·firstcontinuehandler(ExceptionRecord *info, Context *r, G *gp)
{
USED(gp);
if(!runtime·isgoexception(info, r))
return EXCEPTION_CONTINUE_SEARCH;
return EXCEPTION_CONTINUE_EXECUTION;
}
// lastcontinuehandler is reached, because runtime cannot handle
// current exception. lastcontinuehandler will print crash info and exit.
uint32
runtime·lastcontinuehandler(ExceptionRecord *info, Context *r, G *gp)
{
bool crash;
if(runtime·panicking) // traceback already printed if(runtime·panicking) // traceback already printed
runtime·exit(2); runtime·exit(2);
...@@ -97,7 +128,7 @@ runtime·sighandler(ExceptionRecord *info, Context *r, G *gp) ...@@ -97,7 +128,7 @@ runtime·sighandler(ExceptionRecord *info, Context *r, G *gp)
runtime·crash(); runtime·crash();
runtime·exit(2); runtime·exit(2);
return -1; // not reached return 0; // not reached
} }
void void
......
...@@ -73,6 +73,7 @@ TEXT runtime·setlasterror(SB),NOSPLIT,$0 ...@@ -73,6 +73,7 @@ TEXT runtime·setlasterror(SB),NOSPLIT,$0
// Called by Windows as a Vectored Exception Handler (VEH). // Called by Windows as a Vectored Exception Handler (VEH).
// First argument is pointer to struct containing // First argument is pointer to struct containing
// exception record and context pointers. // exception record and context pointers.
// Handler function is stored in AX.
// Return 0 for 'not handled', -1 for handled. // Return 0 for 'not handled', -1 for handled.
TEXT runtime·sigtramp(SB),NOSPLIT,$0-0 TEXT runtime·sigtramp(SB),NOSPLIT,$0-0
MOVL ptrs+0(FP), CX MOVL ptrs+0(FP), CX
...@@ -84,6 +85,8 @@ TEXT runtime·sigtramp(SB),NOSPLIT,$0-0 ...@@ -84,6 +85,8 @@ TEXT runtime·sigtramp(SB),NOSPLIT,$0-0
MOVL SI, 20(SP) MOVL SI, 20(SP)
MOVL DI, 24(SP) MOVL DI, 24(SP)
MOVL AX, SI // save handler address
// find g // find g
get_tls(DX) get_tls(DX)
CMPL DX, $0 CMPL DX, $0
...@@ -123,11 +126,10 @@ TEXT runtime·sigtramp(SB),NOSPLIT,$0-0 ...@@ -123,11 +126,10 @@ TEXT runtime·sigtramp(SB),NOSPLIT,$0-0
sigtramp_g0: sigtramp_g0:
MOVL 0(CX), BX // ExceptionRecord* MOVL 0(CX), BX // ExceptionRecord*
MOVL 4(CX), CX // Context* MOVL 4(CX), CX // Context*
// call sighandler(ExceptionRecord*, Context*, G*)
MOVL BX, 0(SP) MOVL BX, 0(SP)
MOVL CX, 4(SP) MOVL CX, 4(SP)
MOVL DX, 8(SP) MOVL DX, 8(SP)
CALL runtime·sighandler(SB) CALL SI // call handler
// AX is set to report result back to Windows // AX is set to report result back to Windows
MOVL 12(SP), AX MOVL 12(SP), AX
...@@ -149,6 +151,18 @@ done: ...@@ -149,6 +151,18 @@ done:
// RET 4 (return and pop 4 bytes parameters) // RET 4 (return and pop 4 bytes parameters)
BYTE $0xC2; WORD $4 BYTE $0xC2; WORD $4
RET // unreached; make assembler happy RET // unreached; make assembler happy
TEXT runtime·exceptiontramp(SB),NOSPLIT,$0
MOVL $runtime·exceptionhandler(SB), AX
JMP runtime·sigtramp(SB)
TEXT runtime·firstcontinuetramp(SB),NOSPLIT,$0-0
// is never called
INT $3
TEXT runtime·lastcontinuetramp(SB),NOSPLIT,$0-0
MOVL $runtime·lastcontinuehandler(SB), AX
JMP runtime·sigtramp(SB)
TEXT runtime·ctrlhandler(SB),NOSPLIT,$0 TEXT runtime·ctrlhandler(SB),NOSPLIT,$0
PUSHL $runtime·ctrlhandler1(SB) PUSHL $runtime·ctrlhandler1(SB)
......
...@@ -99,6 +99,7 @@ TEXT runtime·setlasterror(SB),NOSPLIT,$0 ...@@ -99,6 +99,7 @@ TEXT runtime·setlasterror(SB),NOSPLIT,$0
// Called by Windows as a Vectored Exception Handler (VEH). // Called by Windows as a Vectored Exception Handler (VEH).
// First argument is pointer to struct containing // First argument is pointer to struct containing
// exception record and context pointers. // exception record and context pointers.
// Handler function is stored in AX.
// Return 0 for 'not handled', -1 for handled. // Return 0 for 'not handled', -1 for handled.
TEXT runtime·sigtramp(SB),NOSPLIT,$0-0 TEXT runtime·sigtramp(SB),NOSPLIT,$0-0
// CX: PEXCEPTION_POINTERS ExceptionInfo // CX: PEXCEPTION_POINTERS ExceptionInfo
...@@ -116,6 +117,8 @@ TEXT runtime·sigtramp(SB),NOSPLIT,$0-0 ...@@ -116,6 +117,8 @@ TEXT runtime·sigtramp(SB),NOSPLIT,$0-0
MOVQ R14, 32(SP) MOVQ R14, 32(SP)
MOVQ R15, 88(SP) MOVQ R15, 88(SP)
MOVQ AX, R15 // save handler address
// find g // find g
get_tls(DX) get_tls(DX)
CMPQ DX, $0 CMPQ DX, $0
...@@ -157,11 +160,10 @@ TEXT runtime·sigtramp(SB),NOSPLIT,$0-0 ...@@ -157,11 +160,10 @@ TEXT runtime·sigtramp(SB),NOSPLIT,$0-0
sigtramp_g0: sigtramp_g0:
MOVQ 0(CX), BX // ExceptionRecord* MOVQ 0(CX), BX // ExceptionRecord*
MOVQ 8(CX), CX // Context* MOVQ 8(CX), CX // Context*
// call sighandler(ExceptionRecord*, Context*, G*)
MOVQ BX, 0(SP) MOVQ BX, 0(SP)
MOVQ CX, 8(SP) MOVQ CX, 8(SP)
MOVQ DX, 16(SP) MOVQ DX, 16(SP)
CALL runtime·sighandler(SB) CALL R15 // call handler
// AX is set to report result back to Windows // AX is set to report result back to Windows
MOVL 24(SP), AX MOVL 24(SP), AX
...@@ -187,6 +189,18 @@ done: ...@@ -187,6 +189,18 @@ done:
RET RET
TEXT runtime·exceptiontramp(SB),NOSPLIT,$0
MOVQ $runtime·exceptionhandler(SB), AX
JMP runtime·sigtramp(SB)
TEXT runtime·firstcontinuetramp(SB),NOSPLIT,$0-0
MOVQ $runtime·firstcontinuehandler(SB), AX
JMP runtime·sigtramp(SB)
TEXT runtime·lastcontinuetramp(SB),NOSPLIT,$0-0
MOVQ $runtime·lastcontinuehandler(SB), AX
JMP runtime·sigtramp(SB)
TEXT runtime·ctrlhandler(SB),NOSPLIT,$8 TEXT runtime·ctrlhandler(SB),NOSPLIT,$8
MOVQ CX, 16(SP) // spill MOVQ CX, 16(SP) // spill
MOVQ $runtime·ctrlhandler1(SB), CX MOVQ $runtime·ctrlhandler1(SB), CX
......
...@@ -494,3 +494,42 @@ func TestOutputDebugString(t *testing.T) { ...@@ -494,3 +494,42 @@ func TestOutputDebugString(t *testing.T) {
p := syscall.StringToUTF16Ptr("testing OutputDebugString") p := syscall.StringToUTF16Ptr("testing OutputDebugString")
d.Proc("OutputDebugStringW").Call(uintptr(unsafe.Pointer(p))) d.Proc("OutputDebugStringW").Call(uintptr(unsafe.Pointer(p)))
} }
func TestRaiseException(t *testing.T) {
o := executeTest(t, raiseExceptionSource, nil)
if strings.Contains(o, "RaiseException should not return") {
t.Fatalf("RaiseException did not crash program: %v", o)
}
if !strings.Contains(o, "Exception 0xbad") {
t.Fatalf("No stack trace: %v", o)
}
}
const raiseExceptionSource = `
package main
import "syscall"
func main() {
const EXCEPTION_NONCONTINUABLE = 1
mod := syscall.MustLoadDLL("kernel32.dll")
proc := mod.MustFindProc("RaiseException")
proc.Call(0xbad, EXCEPTION_NONCONTINUABLE, 0, 0)
println("RaiseException should not return")
}
`
func TestZeroDivisionException(t *testing.T) {
o := executeTest(t, zeroDivisionExceptionSource, nil)
if !strings.Contains(o, "panic: runtime error: integer divide by zero") {
t.Fatalf("No stack trace: %v", o)
}
}
const zeroDivisionExceptionSource = `
package main
func main() {
x := 1
y := 0
z := x / y
println(z)
}
`
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment