summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlejandro Soto <alejandro@34project.org>2022-12-06 13:04:15 -0600
committerAlejandro Soto <alejandro@34project.org>2022-12-06 13:04:15 -0600
commit064b72ae4eb22336438288a9664a37c0dd07f4bc (patch)
treebfbe072702b667299979d6ceb76a3ef444fb9c1a
parentdf69f7b7c73be01968ba767ab112b227533bbd70 (diff)
Implement gdbstub
Diffstat (limited to '')
-rw-r--r--.gitignore1
-rw-r--r--Makefile18
-rw-r--r--rtl/top/conspiracion.sv3
-rw-r--r--sim/gdbstub.py134
-rw-r--r--sim/link.ld (renamed from tb/sim/link.ld)0
-rwxr-xr-xsim/sim.py (renamed from tb/sim/sim.py)99
-rw-r--r--sim/start.S (renamed from tb/sim/start.S)0
-rw-r--r--tb/avalon.hpp3
-rw-r--r--tb/avalon.impl.hpp42
-rw-r--r--tb/top/conspiracion.cpp187
10 files changed, 409 insertions, 78 deletions
diff --git a/.gitignore b/.gitignore
index 5c5f2d1..4b52e55 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,4 @@ hps_isw_handoff/
*.vcd
qmegawiz_errors_log.txt
cr_ie_info.json
+u-boot
diff --git a/Makefile b/Makefile
index 66e9ff7..8f5399d 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,8 @@ VCD_DIR := vcd
OBJ_DIR := obj
RTL_DIR := rtl
TB_DIR := tb
-SIM_DIR := $(TB_DIR)/sim
+SIM_DIR := sim
+TB_SIM_DIR := $(TB_DIR)/sim
SIM_OBJ_DIR := $(OBJ_DIR)/$(TOP)/sim
VERILATOR := verilator
CROSS_CC := arm-none-eabi-gcc
@@ -28,10 +29,13 @@ trace/%: exe/% $(VCD_DIR)/%
$(VCD_DIR)/%:
mkdir -p $@
-sim: $(patsubst $(SIM_DIR)/%.py,sim/%,$(filter-out $(SIM_DIR)/sim.py,$(wildcard $(SIM_DIR)/*.py)))
+sim: $(patsubst $(TB_SIM_DIR)/%.py,sim/%,$(wildcard $(TB_SIM_DIR)/*.py))
-sim/%: $(SIM_DIR)/sim.py $(SIM_DIR)/%.py exe/$(TOP) $(SIM_OBJ_DIR)/%.bin
- @$< $(SIM_DIR)/$*.py $(OBJ_DIR)/$(TOP)/V$(TOP) $(SIM_OBJ_DIR)/$*.bin
+sim/%: $(SIM_DIR)/sim.py $(TB_SIM_DIR)/%.py exe/$(TOP) $(SIM_OBJ_DIR)/%.bin
+ @$< $(TB_SIM_DIR)/$*.py $(OBJ_DIR)/$(TOP)/V$(TOP) $(SIM_OBJ_DIR)/$*.bin
+
+vmlaunch: $(SIM_DIR)/sim.py $(SIM_DIR)/gdbstub.py exe/$(TOP)
+ @$< $(SIM_DIR)/gdbstub.py $(OBJ_DIR)/$(TOP)/V$(TOP) u-boot/build/u-boot-dtb.bin
$(SIM_OBJ_DIR)/%.bin: $(SIM_OBJ_DIR)/%
$(CROSS_OBJCOPY) -O binary --only-section=._img $< $@
@@ -39,10 +43,14 @@ $(SIM_OBJ_DIR)/%.bin: $(SIM_OBJ_DIR)/%
$(SIM_OBJ_DIR)/%: $(SIM_OBJ_DIR)/%.o $(SIM_OBJ_DIR)/start.o
$(CROSS_CC) $(CROSS_LDFLAGS) -o $@ -g -T $(SIM_DIR)/link.ld -nostartfiles -nostdlib $^
-$(SIM_OBJ_DIR)/%.o: $(SIM_DIR)/%.c
+$(SIM_OBJ_DIR)/%.o: $(TB_SIM_DIR)/%.c
@mkdir -p $(SIM_OBJ_DIR)
$(CROSS_CC) $(CROSS_CFLAGS) -o $@ -g -c $< -mcpu=arm810
+$(SIM_OBJ_DIR)/%.o: $(TB_SIM_DIR)/%.S
+ @mkdir -p $(SIM_OBJ_DIR)
+ $(CROSS_CC) $(CROSS_CFLAGS) -o $@ -g -c $<
+
$(SIM_OBJ_DIR)/%.o: $(SIM_DIR)/%.S
@mkdir -p $(SIM_OBJ_DIR)
$(CROSS_CC) $(CROSS_CFLAGS) -o $@ -g -c $<
diff --git a/rtl/top/conspiracion.sv b/rtl/top/conspiracion.sv
index c3ffb93..84b875e 100644
--- a/rtl/top/conspiracion.sv
+++ b/rtl/top/conspiracion.sv
@@ -3,6 +3,7 @@ module conspiracion
input wire clk_clk,
input wire rst_n,
input wire halt,
+ output wire cpu_halted,
output wire [12:0] memory_mem_a,
output wire [2:0] memory_mem_ba,
output wire memory_mem_ck,
@@ -46,7 +47,7 @@ module conspiracion
logic[3:0] data_be;
logic[29:0] addr;
logic[31:0] data_rd, data_wr;
- logic reset_reset_n, cpu_clk, cpu_rst_n, cpu_halt, cpu_halted,
+ logic reset_reset_n, cpu_clk, cpu_rst_n, cpu_halt,
ready, write, start, irq;
`ifdef VERILATOR
diff --git a/sim/gdbstub.py b/sim/gdbstub.py
new file mode 100644
index 0000000..b262971
--- /dev/null
+++ b/sim/gdbstub.py
@@ -0,0 +1,134 @@
+import sys, socket
+
+start_halted = True
+
+def init():
+ global client
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.bind(('127.0.0.1', 1234))
+ sock.listen()
+ print('Listening for gdb on', sock.getsockname(), file=sys.stderr)
+
+ client, peer = sock.accept()
+ sock.close()
+ print('Accepted connection from', peer, file=sys.stderr)
+
+buffer = b''
+haltFromStop = False
+
+def halt():
+ global buffer, haltFromStop
+
+ if haltFromStop:
+ reply(b'S05')
+ haltFromStop = False
+
+ while True:
+ data = client.recv(4096)
+ if not data:
+ break
+
+ buffer = buffer + data if buffer else data
+
+ try:
+ start = buffer.index(b'$')
+ marker = buffer.index(b'#', start + 1)
+ except ValueError:
+ continue
+
+ if marker + 2 >= len(buffer):
+ continue
+
+ data = buffer[start + 1:marker]
+ cksum = int(buffer[marker + 1:marker + 3], 16)
+
+ if cksum != (sum(data) & 0xff):
+ raise Exception(f'bad packet checksum: {buffer[start:marker + 3]}')
+
+ buffer = buffer[marker + 3:]
+
+ client.send(b'+')
+
+ if data[0] == b'?'[0]:
+ out = b'S05'
+ elif data[0] == b'c'[0]:
+ assert not data[1:] #TODO
+ break
+ elif data[0] == b'D'[0]:
+ out = b'OK'
+ elif data[0] == b'g'[0]:
+ out = hexout(read_reg(gdb_reg(r)) for r in range(16))
+ elif data[0] == b'm'[0]:
+ addr, length = (int(x, 16) for x in data[1:].split(b','))
+ out = hexout(read_mem(addr, length))
+ elif data[0] == b'M'[0]:
+ addrlen, data = data[1:].split(b':')
+ addr, length = (int(x, 16) for x in addrlen.split(b','))
+
+ data = bytes.fromhex(str(data, 'ascii'))
+ assert len(data) == length
+
+ write_mem(addr, data)
+ out = b'OK'
+ elif data[0] == b'p'[0]:
+ reg = gdb_reg(int(data[1:], 16))
+ out = hexout(read_reg(reg) if reg is not None else None)
+ else:
+ print('unhandled packet:', data)
+ out = b''
+
+ reply(out)
+
+ haltFromStop = True
+
+def reply(out):
+ client.send(b'$' + out + b'#' + hexout(sum(out) & 0xff, 1))
+
+def gdb_reg(n):
+ if 0 <= n < 8:
+ return (r0, r1, r2, r3, r4, r5, r6, r7)[n]
+
+ if n == 15:
+ return pc
+
+ if n == 0x19:
+ return cpsr
+
+ mode = read_reg(cpsr) & 0b11111
+ if 8 <= n < 13:
+ if mode == 0b10001:
+ regs = (r8_fiq, r9_fiq, r10_fiq, r11_fiq, r12_fiq)
+ else:
+ regs = (r8_fiq, r9_fiq, r10_fiq, r11_fiq, r12_fiq)
+
+ return regs[n - 8]
+
+ if 13 <= n < 15:
+ if mode == 0b10011:
+ regs = (r13_svc, r14_svc)
+ if mode == 0b10111:
+ regs = (r13_abt, r14_abt)
+ if mode == 0b11011:
+ regs = (r13_und, r14_und)
+ if mode == 0b10010:
+ regs = (r13_irq, r14_irq)
+ if mode == 0b10001:
+ regs = (r13_fiq, r14_fiq)
+ else:
+ regs = (r13_usr, r14_usr)
+
+ return regs[n - 13]
+
+ return None
+
+def hexout(data, size=4):
+ if data is None:
+ return b''
+ elif type(data) is bytes:
+ return data.hex().encode('ascii')
+ elif type(data) is int:
+ data = [data]
+
+ return b''.join(hex(d)[2:].zfill(2 * size)[:2 * size].encode('ascii') for d in data)
+
diff --git a/tb/sim/link.ld b/sim/link.ld
index d26cb2a..d26cb2a 100644
--- a/tb/sim/link.ld
+++ b/sim/link.ld
diff --git a/tb/sim/sim.py b/sim/sim.py
index 91fc348..16ceb54 100755
--- a/tb/sim/sim.py
+++ b/sim/sim.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
-import importlib.util, os, pathlib, random, subprocess, sys
+import importlib.util, os, pathlib, random, socket, subprocess, sys
module_path, verilated, image = sys.argv[1:]
test_name = pathlib.Path(module_path).stem
@@ -74,10 +74,28 @@ regs = {}
read_reg = lambda r: regs.setdefault(r, 0)
dumped = []
+halted = False
+
+def recv_mem_dump():
+ dumped.clear()
+ for line in sim_end:
+ line = line.strip()
+ if line == '=== dump-mem ===' or not line:
+ continue
+ elif line == '=== end-mem ===':
+ break
+
+ base, data = line.split()
+ dumped.append((int(base, 16) << 2, bytes.fromhex(data)))
+
def read_mem(base, length):
fragments = []
i = 0
+ if halted and length > 0:
+ print('dump-mem', base >> 2, (length + base - (base & ~0b11) + 0b11) >> 2, file=sim_end, flush=True)
+ recv_mem_dump()
+
while length > 0:
assert i < len(dumped), f'memory at 0x{base:08x} not dumped'
start, data = dumped[i]
@@ -96,6 +114,19 @@ def read_mem(base, length):
return b''.join(fragments)
+def write_mem(base, data):
+ assert halted
+
+ if not data:
+ return
+
+ prefix = read_mem(base & ~0b11, base & 0b11)
+ suffix = read_mem(base + len(data), (4 - ((base + len(data)) & 0b11)) & 0b11)
+ print('patch-mem ', base >> 2, ' ', prefix.hex(), data.hex(), suffix.hex(), sep='', file=sim_end, flush=True)
+
+ #TODO: Invalidate written addresses only
+ dumped.clear()
+
def hexdump(base, memory):
lines = []
offset = 0
@@ -233,6 +264,7 @@ module = importlib.util.module_from_spec(spec)
prelude = {
'read_reg': read_reg,
'read_mem': read_mem,
+ 'write_mem': write_mem,
'assert_reg': assert_reg,
'assert_mem': assert_mem,
'init_reg': init_reg,
@@ -267,6 +299,15 @@ for addr, const in module_get('consts', {}).items():
for addr, filename in module_get('loads', {}).items():
exec_args.extend(['--load', f'{addr},{filename}'])
+if module_get('start_halted', False):
+ exec_args.append('--start-halted')
+
+sim_end, target_end = socket.socketpair()
+sim_end = sim_end.makefile('rw')
+target_fd = target_end.fileno()
+
+exec_args.extend(['--control-fd', str(target_fd)])
+
init_regs = None
exec_args.append(image)
@@ -274,29 +315,45 @@ exec_args.append(f'+verilator+seed+{seed}')
if not os.getenv('SIM_PULLX', 0):
exec_args.append('+verilator+rand+reset+2')
-output = subprocess.run(exec_args, stdout=subprocess.PIPE, text=True)
-if output.returncode != 0:
- exit(success=False)
+process = subprocess.Popen(exec_args, pass_fds=(target_fd,))
+target_end.close()
in_regs = False
-in_mem = False
-
-for line in output.stdout.split('\n'):
- if line == '=== dump-regs ===':
- in_regs = True
- elif line == '=== dump-mem ===':
- in_mem = True
- elif not line:
- continue
- elif in_mem:
- base, data = line.split()
- dumped.append((int(base, 16) << 2, bytes.fromhex(data)))
- elif in_regs:
- value, reg = line.split()
- regs[reg] = int(value, 16)
+halt = module_get('halt')
+
+while True:
+ for line in sim_end:
+ line = line.strip()
+ if line == '=== halted ===':
+ break
+ if line == '=== dump-regs ===':
+ in_regs = True
+ elif line == '=== end-regs ===':
+ in_regs = False
+ elif line == '=== dump-mem ===':
+ recv_mem_dump()
+ elif not line:
+ continue
+ elif in_regs:
+ value, reg = line.split()
+ regs[reg] = int(value, 16)
+ else:
+ while_running()
+ print(f'{COLOR_BLUE}{line}{COLOR_RESET}')
else:
- while_running()
- print(f'{COLOR_BLUE}{line}{COLOR_RESET}')
+ break
+
+ halted = True
+ if halt:
+ halt()
+
+ print('continue', file=sim_end, flush=True)
+ if not halt:
+ break
+
+process.wait(timeout=1)
+if process.returncode != 0:
+ exit(success=False)
if final := module_get('final'):
final()
diff --git a/tb/sim/start.S b/sim/start.S
index 7639513..7639513 100644
--- a/tb/sim/start.S
+++ b/sim/start.S
diff --git a/tb/avalon.hpp b/tb/avalon.hpp
index 30eac2c..f37b306 100644
--- a/tb/avalon.hpp
+++ b/tb/avalon.hpp
@@ -81,6 +81,7 @@ namespace taller::avalon
void bail() noexcept;
std::uint32_t dump(std::uint32_t addr);
+ void patch(std::uint32_t addr, std::uint32_t readdata);
private:
struct binding
@@ -98,6 +99,8 @@ namespace taller::avalon
unsigned avl_byteenable = 0;
bool avl_read = false;
bool avl_write = false;
+
+ slave &resolve_external(std::uint32_t avl_address);
};
}
diff --git a/tb/avalon.impl.hpp b/tb/avalon.impl.hpp
index 5ba514f..3af60d0 100644
--- a/tb/avalon.impl.hpp
+++ b/tb/avalon.impl.hpp
@@ -128,25 +128,45 @@ namespace taller::avalon
std::uint32_t interconnect<Platform>::dump(std::uint32_t addr)
{
std::uint32_t avl_address = addr << 2;
+ auto &dev = resolve_external(avl_address);
+ auto pos = (avl_address & ~dev.address_mask()) >> dev.word_bits();
+
+ std::uint32_t readdata;
+ while(!dev.read(pos, readdata))
+ {
+ continue;
+ }
+
+ return readdata;
+ }
+
+ template<class Platform>
+ void interconnect<Platform>::patch(std::uint32_t addr, std::uint32_t writedata)
+ {
+ std::uint32_t avl_address = addr << 2;
+ auto &dev = resolve_external(avl_address);
+
+ auto pos = (avl_address & ~dev.address_mask()) >> dev.word_bits();
+
+ while(!dev.write(pos, writedata, 0b1111))
+ {
+ continue;
+ }
+ }
+
+ template<class Platform>
+ slave& interconnect<Platform>::resolve_external(std::uint32_t avl_address)
+ {
for(auto &binding : devices)
{
if((avl_address & binding.mask) == binding.base)
{
- auto &dev = binding.dev;
- auto pos = (avl_address & ~dev.address_mask()) >> 2;
-
- std::uint32_t readdata;
- while(!dev.read(pos, readdata))
- {
- continue;
- }
-
- return readdata;
+ return binding.dev;
}
}
- fprintf(stderr, "[avl] attempt to dump memory hole at 0x%08x\n", addr);
+ fprintf(stderr, "[avl] attempt to access hole at 0x%08x\n", avl_address);
assert(false);
}
}
diff --git a/tb/top/conspiracion.cpp b/tb/top/conspiracion.cpp
index 07b35a5..15da2ea 100644
--- a/tb/top/conspiracion.cpp
+++ b/tb/top/conspiracion.cpp
@@ -197,11 +197,21 @@ int main(int argc, char **argv)
parser, "no-tty", "Disable TTY takeoveer", {"no-tty"}
);
+ args::Flag start_halted
+ (
+ parser, "start-halted", "Halt before running the first instruction", {"start-halted"}
+ );
+
args::ValueFlag<unsigned> cycles
(
parser, "cycles", "Number of core cycles to run", {"cycles"}, 256
);
+ args::ValueFlag<int> control_fd
+ (
+ parser, "fd", "Control file descriptor", {"control-fd"}, -1
+ );
+
args::ValueFlagList<mem_region> dump_mem
(
parser, "addr,length", "Dump a memory region", {"dump-mem"}
@@ -209,7 +219,7 @@ int main(int argc, char **argv)
args::ValueFlagList<mem_init> const_
(
- parser, "addr,value", "Add a constant map", {"const"}
+ parser, "addr,value", "Add a constant mapping", {"const"}
);
args::ValueFlagList<file_load> loads
@@ -241,6 +251,13 @@ int main(int argc, char **argv)
return EXIT_FAILURE;
}
+ FILE *ctrl = stdout;
+ if(*control_fd != -1 && (ctrl = fdopen(*control_fd, "r+")) == nullptr)
+ {
+ std::perror("fdopen()");
+ return EXIT_FAILURE;
+ }
+
Vconspiracion top;
VerilatedVcdC trace;
@@ -369,17 +386,136 @@ int main(int argc, char **argv)
ttyJ0.takeover();
}
- top.halt = 0;
+ top.halt = start_halted;
top.rst_n = 0;
cycle();
top.rst_n = 1;
- for(unsigned i = 0; i < *cycles; ++i)
+ auto do_reg_dump = [&]()
+ {
+ std::fputs("=== dump-regs ===\n", ctrl);
+
+ const auto &core = *top.conspiracion->core;
+ const auto &regfile = core.regs->a->file;
+
+ int i = 0;
+ for(const auto *name : gp_regs)
+ {
+ std::fprintf(ctrl, "%08x %s\n", regfile[i++], name);
+ }
+
+ std::fprintf(ctrl, "%08x pc\n", core.control->pc << 2);
+ std::fprintf(ctrl, "%08x cpsr\n", core.psr->cpsr_word);
+ std::fprintf(ctrl, "%08x spsr_svc\n", core.psr->spsr_svc_word);
+ std::fprintf(ctrl, "%08x spsr_abt\n", core.psr->spsr_abt_word);
+ std::fprintf(ctrl, "%08x spsr_und\n", core.psr->spsr_und_word);
+ std::fprintf(ctrl, "%08x spsr_fiq\n", core.psr->spsr_fiq_word);
+ std::fprintf(ctrl, "%08x spsr_irq\n", core.psr->spsr_irq_word);
+ std::fputs("=== end-regs ===\n", ctrl);
+ };
+
+ auto do_mem_dump = [&](const mem_region *dumps, std::size_t count)
{
- cycle();
- if(failed)
+ std::fputs("=== dump-mem ===\n", ctrl);
+ for(std::size_t i = 0; i < count; ++i)
{
- break;
+ const auto &dump = dumps[i];
+
+ std::fprintf(ctrl, "%08x ", static_cast<std::uint32_t>(dump.start));
+ for(std::size_t i = 0; i < dump.length; ++i)
+ {
+ auto word = avl.dump(dump.start + i);
+ word = (word & 0xff) << 24
+ | ((word >> 8) & 0xff) << 16
+ | ((word >> 16) & 0xff) << 8
+ | ((word >> 24) & 0xff);
+
+ std::fprintf(ctrl, "%08x", word);
+ }
+
+ std::fputc('\n', ctrl);
+ }
+
+ std::fputs("=== end-mem ===\n", ctrl);
+ };
+
+ unsigned i = 0;
+ while(!failed && i < *cycles)
+ {
+ for(; i < *cycles; ++i)
+ {
+ cycle();
+ if(failed || top.cpu_halted) [[unlikely]]
+ {
+ break;
+ }
+ }
+
+ if(top.cpu_halted)
+ {
+ do_reg_dump();
+ std::fputs("=== halted ===\n", ctrl);
+
+ char *line = nullptr;
+ std::size_t buf_size = 0;
+
+ while(true)
+ {
+ ssize_t read = getline(&line, &buf_size, ctrl);
+ if(read == -1)
+ {
+ if(!std::feof(ctrl))
+ {
+ std::perror("getline()");
+ failed = true;
+ }
+
+ break;
+ }
+
+ if(read > 0 && line[read - 1] == '\n')
+ {
+ line[read - 1] = '\0';
+ }
+
+ const char *cmd = std::strtok(line, " ");
+ if(!std::strcmp(cmd, "continue"))
+ {
+ top.halt = false;
+ break;
+ } else if(!std::strcmp(cmd, "dump-mem"))
+ {
+ mem_region dump = {};
+ std::sscanf(std::strtok(nullptr, " "), "%zu", &dump.start);
+ std::sscanf(std::strtok(nullptr, " "), "%zu", &dump.length);
+ do_mem_dump(&dump, 1);
+ } else if(!std::strcmp(cmd, "patch-mem"))
+ {
+ std::uint32_t addr;
+ std::sscanf(std::strtok(nullptr, " "), "%u", &addr);
+
+ const char *data = std::strtok(nullptr, " ");
+ std::size_t length = std::strlen(data);
+
+ while(data && length >= 8)
+ {
+ std::uint32_t word;
+ std::sscanf(data, "%08x", &word);
+
+ data += 8;
+ length -= 8;
+
+ word = (word & 0xff) << 24
+ | ((word >> 8) & 0xff) << 16
+ | ((word >> 16) & 0xff) << 8
+ | ((word >> 24) & 0xff);
+
+ avl.patch(addr++, word);
+ }
+ }
+ }
+
+ std::free(line);
}
}
@@ -395,49 +531,20 @@ int main(int argc, char **argv)
if(dump_regs || failed)
{
- std::puts("=== dump-regs ===");
-
- const auto &core = *top.conspiracion->core;
- const auto &regfile = core.regs->a->file;
-
- int i = 0;
- for(const auto *name : gp_regs)
- {
- std::printf("%08x %s\n", regfile[i++], name);
- }
-
- std::printf("%08x pc\n", core.control->pc << 2);
- std::printf("%08x cpsr\n", core.psr->cpsr_word);
- std::printf("%08x spsr_svc\n", core.psr->spsr_svc_word);
- std::printf("%08x spsr_abt\n", core.psr->spsr_abt_word);
- std::printf("%08x spsr_und\n", core.psr->spsr_und_word);
- std::printf("%08x spsr_fiq\n", core.psr->spsr_fiq_word);
- std::printf("%08x spsr_irq\n", core.psr->spsr_irq_word);
+ do_reg_dump();
}
const auto &dumps = *dump_mem;
if(!dumps.empty())
{
- std::puts("=== dump-mem ===");
+ do_mem_dump(dumps.data(), dumps.size());
}
- for(const auto &dump : dumps)
+ top.final();
+ if(ctrl != stdout)
{
- std::printf("%08x ", static_cast<std::uint32_t>(dump.start));
- for(std::size_t i = 0; i < dump.length; ++i)
- {
- auto word = avl.dump(dump.start + i);
- word = (word & 0xff) << 24
- | ((word >> 8) & 0xff) << 16
- | ((word >> 16) & 0xff) << 8
- | ((word >> 24) & 0xff);
-
- std::printf("%08x", word);
- }
-
- std::putchar('\n');
+ std::fclose(ctrl);
}
- top.final();
return failed ? EXIT_FAILURE : EXIT_SUCCESS;
}