authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2019-09-11 20:22:49-04:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2019-09-12 01:40:58-04:00
logcf4bccf76566ac112f9142863c3e4dbf81e71d08
tree8c0f3565b3a2f233b0b90ac79b752730de7c9774
parent68b49f74c45f9d46ceca0196ad5b2edeee30e26f
signaturelock-open Commit is signed but in an unrecognized format.

improvements targeted at improving async functions

* Reuse bytes of async function frames when non-async functions make `noasync` calls. This prevents explosive stack growth. * Zig now passes a stack size argument to the linker when linking ELF binaries. Linux ignores this value, but it is available as a program header called GNU_STACK. I prototyped some code that memory maps extra space to the stack using this program header, but there was still a problem when accessing stack memory very far down. Stack probing is needed or not working or something. I also prototyped using `@newStackCall` to call main and that does work around the issue but it also brings its own issues. That code is commented out for now in std/special/start.zig. I'm on a plane with no Internet, but I plan to consult with the musl community for advice when I get a chance. * Added `noasync` to a bunch of function calls in std.debug. It's very messy but it's a workaround that makes stack traces functional with evented I/O enabled. Eventually these will be cleaned up as the root bugs are found and fixed. Programs built in blocking mode are unaffected. * Lowered the default stack size of std.io.InStream (for the async version) to 1 MiB instead of 4. Until we figure out how to get choosing a stack size working (see 2nd bullet point above), 4 MiB tends to cause segfaults due to stack size running out, or usage of stack memory too far apart, or something like that. * Default thread stack size is bumped from 8 MiB to 16 to match the size we give for the main thread. It's planned to eventually remove this hard coded value and have Zig able to determine this value during semantic analysis, with call graph analysis and function pointer annotations and extern function annotations.

7 files changed, 90 insertions(+), 32 deletions(-)

src/codegen.cpp+12-2
......@@ -7184,6 +7184,9 @@ static void do_code_gen(CodeGen *g) {
71847184
71857185 if (!is_async) {
71867186 // allocate async frames for noasync calls & awaits to async functions
7187 ZigType *largest_call_frame_type = nullptr;
7188 IrInstruction *all_calls_alloca = ir_create_alloca(g, &fn_table_entry->fndef_scope->base,
7189 fn_table_entry->body_node, fn_table_entry, g->builtin_types.entry_void, "@async_call_frame");
71877190 for (size_t i = 0; i < fn_table_entry->call_list.length; i += 1) {
71887191 IrInstructionCallGen *call = fn_table_entry->call_list.at(i);
71897192 if (call->fn_entry == nullptr)
......@@ -7195,8 +7198,15 @@ static void do_code_gen(CodeGen *g) {
71957198 if (call->frame_result_loc != nullptr)
71967199 continue;
71977200 ZigType *callee_frame_type = get_fn_frame_type(g, call->fn_entry);
7198 call->frame_result_loc = ir_create_alloca(g, call->base.scope, call->base.source_node,
7199 fn_table_entry, callee_frame_type, "");
7201 if (largest_call_frame_type == nullptr ||
7202 callee_frame_type->abi_size > largest_call_frame_type->abi_size)
7203 {
7204 largest_call_frame_type = callee_frame_type;
7205 }
7206 call->frame_result_loc = all_calls_alloca;
7207 }
7208 if (largest_call_frame_type != nullptr) {
7209 all_calls_alloca->value.type = get_pointer_to_type(g, largest_call_frame_type, false);
72007210 }
72017211 // allocate temporary stack data
72027212 for (size_t alloca_i = 0; alloca_i < fn_table_entry->alloca_gen_list.length; alloca_i += 1) {
src/link.cpp+5
......@@ -1615,6 +1615,11 @@ static void construct_linker_job_elf(LinkJob *lj) {
16151615
16161616 lj->args.append("-error-limit=0");
16171617
1618 if (g->out_type == OutTypeExe) {
1619 lj->args.append("-z");
1620 lj->args.append("stack-size=16777216"); // default to 16 MiB
1621 }
1622
16181623 if (g->linker_script) {
16191624 lj->args.append("-T");
16201625 lj->args.append(g->linker_script);
std/debug.zig+34-15
......@@ -1478,10 +1478,11 @@ const LineNumberProgram = struct {
14781478 }
14791479};
14801480
1481// TODO the noasyncs here are workarounds
14811482fn readStringRaw(allocator: *mem.Allocator, in_stream: var) ![]u8 {
14821483 var buf = ArrayList(u8).init(allocator);
14831484 while (true) {
1484 const byte = try in_stream.readByte();
1485 const byte = try noasync in_stream.readByte();
14851486 if (byte == 0) break;
14861487 try buf.append(byte);
14871488 }
......@@ -1494,10 +1495,11 @@ fn getString(di: *DwarfInfo, offset: u64) ![]u8 {
14941495 return di.readString();
14951496}
14961497
1498// TODO the noasyncs here are workarounds
14971499fn readAllocBytes(allocator: *mem.Allocator, in_stream: var, size: usize) ![]u8 {
14981500 const buf = try allocator.alloc(u8, size);
14991501 errdefer allocator.free(buf);
1500 if ((try in_stream.read(buf)) < size) return error.EndOfFile;
1502 if ((try noasync in_stream.read(buf)) < size) return error.EndOfFile;
15011503 return buf;
15021504}
15031505
......@@ -1506,8 +1508,9 @@ fn parseFormValueBlockLen(allocator: *mem.Allocator, in_stream: var, size: usize
15061508 return FormValue{ .Block = buf };
15071509}
15081510
1511// TODO the noasyncs here are workarounds
15091512fn parseFormValueBlock(allocator: *mem.Allocator, in_stream: var, size: usize) !FormValue {
1510 const block_len = try in_stream.readVarInt(usize, builtin.Endian.Little, size);
1513 const block_len = try noasync in_stream.readVarInt(usize, builtin.Endian.Little, size);
15111514 return parseFormValueBlockLen(allocator, in_stream, block_len);
15121515}
15131516
......@@ -1537,27 +1540,37 @@ fn parseFormValueConstant(allocator: *mem.Allocator, in_stream: var, signed: boo
15371540 };
15381541}
15391542
1543// TODO the noasyncs here are workarounds
15401544fn parseFormValueDwarfOffsetSize(in_stream: var, is_64: bool) !u64 {
1541 return if (is_64) try in_stream.readIntLittle(u64) else u64(try in_stream.readIntLittle(u32));
1545 return if (is_64) try noasync in_stream.readIntLittle(u64) else u64(try noasync in_stream.readIntLittle(u32));
15421546}
15431547
1548// TODO the noasyncs here are workarounds
15441549fn parseFormValueTargetAddrSize(in_stream: var) !u64 {
1545 return if (@sizeOf(usize) == 4) u64(try in_stream.readIntLittle(u32)) else if (@sizeOf(usize) == 8) try in_stream.readIntLittle(u64) else unreachable;
1550 if (@sizeOf(usize) == 4) {
1551 return u64(try noasync in_stream.readIntLittle(u32));
1552 } else if (@sizeOf(usize) == 8) {
1553 return noasync in_stream.readIntLittle(u64);
1554 } else {
1555 unreachable;
1556 }
15461557}
15471558
1559// TODO the noasyncs here are workarounds
15481560fn parseFormValueRef(allocator: *mem.Allocator, in_stream: var, size: i32) !FormValue {
15491561 return FormValue{
15501562 .Ref = switch (size) {
1551 1 => try in_stream.readIntLittle(u8),
1552 2 => try in_stream.readIntLittle(u16),
1553 4 => try in_stream.readIntLittle(u32),
1554 8 => try in_stream.readIntLittle(u64),
1555 -1 => try leb.readULEB128(u64, in_stream),
1563 1 => try noasync in_stream.readIntLittle(u8),
1564 2 => try noasync in_stream.readIntLittle(u16),
1565 4 => try noasync in_stream.readIntLittle(u32),
1566 8 => try noasync in_stream.readIntLittle(u64),
1567 -1 => try noasync leb.readULEB128(u64, in_stream),
15561568 else => unreachable,
15571569 },
15581570 };
15591571}
15601572
1573// TODO the noasyncs here are workarounds
15611574fn parseFormValue(allocator: *mem.Allocator, in_stream: var, form_id: u64, is_64: bool) anyerror!FormValue {
15621575 return switch (form_id) {
15631576 DW.FORM_addr => FormValue{ .Address = try parseFormValueTargetAddrSize(in_stream) },
......@@ -1565,7 +1578,7 @@ fn parseFormValue(allocator: *mem.Allocator, in_stream: var, form_id: u64, is_64
15651578 DW.FORM_block2 => parseFormValueBlock(allocator, in_stream, 2),
15661579 DW.FORM_block4 => parseFormValueBlock(allocator, in_stream, 4),
15671580 DW.FORM_block => x: {
1568 const block_len = try leb.readULEB128(usize, in_stream);
1581 const block_len = try noasync leb.readULEB128(usize, in_stream);
15691582 return parseFormValueBlockLen(allocator, in_stream, block_len);
15701583 },
15711584 DW.FORM_data1 => parseFormValueConstant(allocator, in_stream, false, 1),
......@@ -1577,11 +1590,11 @@ fn parseFormValue(allocator: *mem.Allocator, in_stream: var, form_id: u64, is_64
15771590 return parseFormValueConstant(allocator, in_stream, signed, -1);
15781591 },
15791592 DW.FORM_exprloc => {
1580 const size = try leb.readULEB128(usize, in_stream);
1593 const size = try noasync leb.readULEB128(usize, in_stream);
15811594 const buf = try readAllocBytes(allocator, in_stream, size);
15821595 return FormValue{ .ExprLoc = buf };
15831596 },
1584 DW.FORM_flag => FormValue{ .Flag = (try in_stream.readByte()) != 0 },
1597 DW.FORM_flag => FormValue{ .Flag = (try noasync in_stream.readByte()) != 0 },
15851598 DW.FORM_flag_present => FormValue{ .Flag = true },
15861599 DW.FORM_sec_offset => FormValue{ .SecOffset = try parseFormValueDwarfOffsetSize(in_stream, is_64) },
15871600
......@@ -1592,12 +1605,12 @@ fn parseFormValue(allocator: *mem.Allocator, in_stream: var, form_id: u64, is_64
15921605 DW.FORM_ref_udata => parseFormValueRef(allocator, in_stream, -1),
15931606
15941607 DW.FORM_ref_addr => FormValue{ .RefAddr = try parseFormValueDwarfOffsetSize(in_stream, is_64) },
1595 DW.FORM_ref_sig8 => FormValue{ .Ref = try in_stream.readIntLittle(u64) },
1608 DW.FORM_ref_sig8 => FormValue{ .Ref = try noasync in_stream.readIntLittle(u64) },
15961609
15971610 DW.FORM_string => FormValue{ .String = try readStringRaw(allocator, in_stream) },
15981611 DW.FORM_strp => FormValue{ .StrPtr = try parseFormValueDwarfOffsetSize(in_stream, is_64) },
15991612 DW.FORM_indirect => {
1600 const child_form_id = try leb.readULEB128(u64, in_stream);
1613 const child_form_id = try noasync leb.readULEB128(u64, in_stream);
16011614 const F = @typeOf(async parseFormValue(allocator, in_stream, child_form_id, is_64));
16021615 var frame = try allocator.create(F);
16031616 defer allocator.destroy(frame);
......@@ -2400,3 +2413,9 @@ stdcallcc fn handleSegfaultWindows(info: *windows.EXCEPTION_POINTERS) c_long {
24002413 else => return windows.EXCEPTION_CONTINUE_SEARCH,
24012414 }
24022415}
2416
2417pub fn dumpStackPointerAddr(prefix: []const u8) void {
2418 const sp = asm ("" : [argc] "={rsp}" (-> usize));
2419 std.debug.warn("{} sp = 0x{x}\n", prefix, sp);
2420}
2421
std/io/in_stream.zig+1-1
......@@ -6,7 +6,7 @@ const assert = std.debug.assert;
66const mem = std.mem;
77const Buffer = std.Buffer;
88
9pub const default_stack_size = 4 * 1024 * 1024;
9pub const default_stack_size = 1 * 1024 * 1024;
1010pub const stack_size: usize = if (@hasDecl(root, "stack_size_std_io_InStream"))
1111 root.stack_size_std_io_InStream
1212else
std/os/linux/tls.zig+6-1
......@@ -125,7 +125,7 @@ pub fn setThreadPointer(addr: usize) void {
125125 }
126126}
127127
128pub fn initTLS() void {
128pub fn initTLS() ?*elf.Phdr {
129129 var tls_phdr: ?*elf.Phdr = null;
130130 var img_base: usize = 0;
131131
......@@ -152,10 +152,13 @@ pub fn initTLS() void {
152152 // Search the TLS section
153153 const phdrs = (@intToPtr([*]elf.Phdr, at_phdr))[0..at_phnum];
154154
155 var gnu_stack: ?*elf.Phdr = null;
156
155157 for (phdrs) |*phdr| {
156158 switch (phdr.p_type) {
157159 elf.PT_PHDR => img_base = at_phdr - phdr.p_vaddr,
158160 elf.PT_TLS => tls_phdr = phdr,
161 elf.PT_GNU_STACK => gnu_stack = phdr,
159162 else => continue,
160163 }
161164 }
......@@ -217,6 +220,8 @@ pub fn initTLS() void {
217220 .data_offset = data_offset,
218221 };
219222 }
223
224 return gnu_stack;
220225}
221226
222227pub fn copyTLS(addr: usize) usize {
std/special/start.zig+31-12
......@@ -5,7 +5,7 @@ const std = @import("std");
55const builtin = @import("builtin");
66const assert = std.debug.assert;
77
8var argc_ptr: [*]usize = undefined;
8var starting_stack_ptr: [*]usize = undefined;
99
1010const is_wasm = switch (builtin.arch) {
1111 .wasm32, .wasm64 => true,
......@@ -35,17 +35,17 @@ nakedcc fn _start() noreturn {
3535
3636 switch (builtin.arch) {
3737 .x86_64 => {
38 argc_ptr = asm (""
38 starting_stack_ptr = asm (""
3939 : [argc] "={rsp}" (-> [*]usize)
4040 );
4141 },
4242 .i386 => {
43 argc_ptr = asm (""
43 starting_stack_ptr = asm (""
4444 : [argc] "={esp}" (-> [*]usize)
4545 );
4646 },
4747 .aarch64, .aarch64_be, .arm => {
48 argc_ptr = asm ("mov %[argc], sp"
48 starting_stack_ptr = asm ("mov %[argc], sp"
4949 : [argc] "=r" (-> [*]usize)
5050 );
5151 },
......@@ -72,8 +72,8 @@ fn posixCallMainAndExit() noreturn {
7272 if (builtin.os == builtin.Os.freebsd) {
7373 @setAlignStack(16);
7474 }
75 const argc = argc_ptr[0];
76 const argv = @ptrCast([*][*]u8, argc_ptr + 1);
75 const argc = starting_stack_ptr[0];
76 const argv = @ptrCast([*][*]u8, starting_stack_ptr + 1);
7777
7878 const envp_optional = @ptrCast([*]?[*]u8, argv + argc + 1);
7979 var envp_count: usize = 0;
......@@ -85,21 +85,40 @@ fn posixCallMainAndExit() noreturn {
8585 const auxv = @ptrCast([*]std.elf.Auxv, envp.ptr + envp_count + 1);
8686 std.os.linux.elf_aux_maybe = auxv;
8787 // Initialize the TLS area
88 std.os.linux.tls.initTLS();
88 const gnu_stack_phdr = std.os.linux.tls.initTLS() orelse @panic("ELF missing stack size");
8989
9090 if (std.os.linux.tls.tls_image) |tls_img| {
9191 const tls_addr = std.os.linux.tls.allocateTLS(tls_img.alloc_size);
9292 const tp = std.os.linux.tls.copyTLS(tls_addr);
9393 std.os.linux.tls.setThreadPointer(tp);
9494 }
95
96 // TODO This is disabled because what should we do when linking libc and this code
97 // does not execute? And also it's causing a test failure in stack traces in release modes.
98
99 //// Linux ignores the stack size from the ELF file, and instead always does 8 MiB. A further
100 //// problem is that it uses PROT_GROWSDOWN which prevents stores to addresses too far down
101 //// the stack and requires "probing". So here we allocate our own stack.
102 //const wanted_stack_size = gnu_stack_phdr.p_memsz;
103 //assert(wanted_stack_size % std.mem.page_size == 0);
104 //// Allocate an extra page as the guard page.
105 //const total_size = wanted_stack_size + std.mem.page_size;
106 //const new_stack = std.os.mmap(
107 // null,
108 // total_size,
109 // std.os.PROT_READ | std.os.PROT_WRITE,
110 // std.os.MAP_PRIVATE | std.os.MAP_ANONYMOUS,
111 // -1,
112 // 0,
113 //) catch @panic("out of memory");
114 //std.os.mprotect(new_stack[0..std.mem.page_size], std.os.PROT_NONE) catch {};
115 //std.os.exit(@newStackCall(new_stack, callMainWithArgs, argc, argv, envp));
95116 }
96117
97 std.os.exit(callMainWithArgs(argc, argv, envp));
118 std.os.exit(@inlineCall(callMainWithArgs, argc, argv, envp));
98119}
99120
100// This is marked inline because for some reason LLVM in release mode fails to inline it,
101// and we want fewer call frames in stack traces.
102inline fn callMainWithArgs(argc: usize, argv: [*][*]u8, envp: [][*]u8) u8 {
121fn callMainWithArgs(argc: usize, argv: [*][*]u8, envp: [][*]u8) u8 {
103122 std.os.argv = argv[0..argc];
104123 std.os.environ = envp;
105124
......@@ -112,7 +131,7 @@ extern fn main(c_argc: i32, c_argv: [*][*]u8, c_envp: [*]?[*]u8) i32 {
112131 var env_count: usize = 0;
113132 while (c_envp[env_count] != null) : (env_count += 1) {}
114133 const envp = @ptrCast([*][*]u8, c_envp)[0..env_count];
115 return callMainWithArgs(@intCast(usize, c_argc), c_argv, envp);
134 return @inlineCall(callMainWithArgs, @intCast(usize, c_argc), c_argv, envp);
116135}
117136
118137// General error message for a malformed return type
std/thread.zig+1-1
......@@ -145,7 +145,7 @@ pub const Thread = struct {
145145 if (builtin.single_threaded) @compileError("cannot spawn thread when building in single-threaded mode");
146146 // TODO compile-time call graph analysis to determine stack upper bound
147147 // https://github.com/ziglang/zig/issues/157
148 const default_stack_size = 8 * 1024 * 1024;
148 const default_stack_size = 16 * 1024 * 1024;
149149
150150 const Context = @typeOf(context);
151151 comptime assert(@ArgType(@typeOf(startFn), 0) == Context);