From 82ebfdf8ce1a93a6c1716305c7124989527861a2 Mon Sep 17 00:00:00 2001 From: Stanislav Kramar Date: Tue, 6 Dec 2022 01:24:33 +0100 Subject: [PATCH 01/12] add grading and tests --- Makefile | 12 +- grade-lab-util.py | 80 +++++++ gradelib.py | 582 ++++++++++++++++++++++++++++++++++++++++++++++ user/xargstest.sh | 6 + 4 files changed, 678 insertions(+), 2 deletions(-) create mode 100644 grade-lab-util.py create mode 100644 gradelib.py create mode 100644 user/xargstest.sh diff --git a/Makefile b/Makefile index 39a99d7a8f..51b23ab8f0 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ OBJS = \ # riscv64-unknown-elf- or riscv64-linux-gnu- # perhaps in /opt/riscv/bin -#TOOLPREFIX = +TOOLPREFIX = riscv64-unknown-elf- # Try to infer the correct TOOLPREFIX if not set ifndef TOOLPREFIX @@ -134,7 +134,7 @@ UPROGS=\ $U/_zombie\ fs.img: mkfs/mkfs README $(UPROGS) - mkfs/mkfs fs.img README $(UPROGS) + mkfs/mkfs fs.img README user/xargstest.sh $(UPROGS) -include kernel/*.d user/*.d @@ -171,3 +171,11 @@ qemu-gdb: $K/kernel .gdbinit fs.img @echo "*** Now run 'gdb' in another window." 1>&2 $(QEMU) $(QEMUOPTS) -S $(QEMUGDB) +print-gdbport: + @echo $(GDBPORT) + +grade: + @echo $(MAKE) clean + @$(MAKE) clean || \ + (echo "'make clean' failed. HINT: Do you have another running instance of xv6?" && exit 1) + python3 grade-lab-util.py $(GRADEFLAGS) diff --git a/grade-lab-util.py b/grade-lab-util.py new file mode 100644 index 0000000000..6afce5a893 --- /dev/null +++ b/grade-lab-util.py @@ -0,0 +1,80 @@ +import re +from gradelib import * + +r = Runner(save("xv6.out")) + +@test(5, "sleep, no arguments") +def test_sleep_no_args(): + r.run_qemu(shell_script([ + 'sleep' + ])) + r.match(no=["exec .* failed", "$ sleep\n$"]) + +@test(5, "sleep, returns") +def test_sleep_no_args(): + r.run_qemu(shell_script([ + 'sleep', + 'echo OK' + ])) + r.match('^OK$', no=["exec .* failed", "$ sleep\n$"]) + +@test(10, "sleep, makes syscall") +def test_sleep(): + r.run_qemu(shell_script([ + 'sleep 10', + 'echo FAIL' + ]), stop_breakpoint('sys_sleep')) + r.match('\\$ sleep 10', no=['FAIL']) + +@test(20, "pingpong") +def test_pingpong(): + r.run_qemu(shell_script([ + 'pingpong', 'echo OK' + ])) + r.match('^\\d+: received ping$', '^\\d+: received pong$', '^OK$') + +@test(20, "primes") +def test_primes(): + r.run_qemu(shell_script([ + 'primes', 'echo OK' + ])) + args = ['prime %d' % i for i in [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]] + args.append('^OK$') + r.match(*args) + +@test(10, "find, in current directory") +def test_find_curdir(): + fn = random_str() + r.run_qemu(shell_script([ + 'echo > %s' % fn, + 'find . %s' % fn + ])) + r.match('./%s' % fn) + +@test(10, "find, recursive") +def test_find_recursive(): + needle = random_str() + dirs = [random_str() for _ in range(3)] + r.run_qemu(shell_script([ + 'mkdir %s' % dirs[0], + 'echo > %s/%s' % (dirs[0], needle), + 'mkdir %s/%s' % (dirs[0], dirs[1]), + 'echo > %s/%s/%s' % (dirs[0], dirs[1], needle), + 'mkdir %s' % dirs[2], + 'echo > %s/%s' % (dirs[2], needle), + 'find . %s' % needle + ])) + r.match('./%s/%s' % (dirs[0], needle), + './%s/%s/%s' % (dirs[0], dirs[1], needle), + './%s/%s' % (dirs[2], needle)) + +@test(20, "xargs") +def test_xargs(): + r.run_qemu(shell_script([ + 'sh < xargstest.sh', + 'echo DONE', + ], 'DONE')) + matches = re.findall("hello", r.qemu.output) + assert_equal(len(matches), 3, "Number of appearances of 'hello'") + +run_tests() diff --git a/gradelib.py b/gradelib.py new file mode 100644 index 0000000000..2201de7a47 --- /dev/null +++ b/gradelib.py @@ -0,0 +1,582 @@ +from __future__ import print_function + +import sys, os, re, time, socket, select, subprocess, errno, shutil, random, string +from subprocess import check_call, Popen +from optparse import OptionParser + +__all__ = [] + +################################################################## +# Test structure +# + +__all__ += ["test", "end_part", "run_tests", "get_current_test"] + +TESTS = [] +TOTAL = POSSIBLE = 0 +PART_TOTAL = PART_POSSIBLE = 0 +CURRENT_TEST = None + +def test(points, title=None, parent=None): + """Decorator for declaring test functions. If title is None, the + title of the test will be derived from the function name by + stripping the leading "test_" and replacing underscores with + spaces.""" + + def register_test(fn, title=title): + if not title: + assert fn.__name__.startswith("test_") + title = fn.__name__[5:].replace("_", " ") + if parent: + title = " " + title + + def run_test(): + global TOTAL, POSSIBLE, CURRENT_TEST + + # Handle test dependencies + if run_test.complete: + return + run_test.complete = True + if parent: + parent() + + # Run the test + fail = None + start = time.time() + CURRENT_TEST = run_test + sys.stdout.write("%s: " % title) + sys.stdout.flush() + try: + fn() + except AssertionError as e: + fail = str(e) + + # Display and handle test result + POSSIBLE += points + if points: + print("%s" % \ + (color("red", "FAIL") if fail else color("green", "OK")), end=' ') + if time.time() - start > 0.1: + print("(%.1fs)" % (time.time() - start), end=' ') + print() + if fail: + print(" %s" % fail.replace("\n", "\n ")) + else: + TOTAL += points + for callback in run_test.on_finish: + callback(fail) + CURRENT_TEST = None + + # Record test metadata on the test wrapper function + run_test.__name__ = fn.__name__ + run_test.title = title + run_test.complete = False + run_test.on_finish = [] + TESTS.append(run_test) + return run_test + return register_test + +def end_part(name): + def show_part(): + global PART_TOTAL, PART_POSSIBLE + print("Part %s score: %d/%d" % \ + (name, TOTAL - PART_TOTAL, POSSIBLE - PART_POSSIBLE)) + print() + PART_TOTAL, PART_POSSIBLE = TOTAL, POSSIBLE + show_part.title = "" + TESTS.append(show_part) + +def run_tests(): + """Set up for testing and run the registered test functions.""" + + # Handle command line + global options + parser = OptionParser(usage="usage: %prog [-v] [filters...]") + parser.add_option("-v", "--verbose", action="store_true", + help="print commands") + parser.add_option("--color", choices=["never", "always", "auto"], + default="auto", help="never, always, or auto") + (options, args) = parser.parse_args() + + # Start with a full build to catch build errors + make() + + # Clean the file system if there is one + reset_fs() + + # Run tests + limit = list(map(str.lower, args)) + try: + for test in TESTS: + if not limit or any(l in test.title.lower() for l in limit): + test() + if not limit: + print("Score: %d/%d" % (TOTAL, POSSIBLE)) + except KeyboardInterrupt: + pass + if TOTAL < POSSIBLE: + sys.exit(1) + +def get_current_test(): + if not CURRENT_TEST: + raise RuntimeError("No test is running") + return CURRENT_TEST + +################################################################## +# Assertions +# + +__all__ += ["assert_equal", "assert_lines_match"] + +def assert_equal(got, expect, msg=""): + if got == expect: + return + if msg: + msg += "\n" + raise AssertionError("%sgot:\n %s\nexpected:\n %s" % + (msg, str(got).replace("\n", "\n "), + str(expect).replace("\n", "\n "))) + +def assert_lines_match(text, *regexps, **kw): + """Assert that all of regexps match some line in text. If a 'no' + keyword argument is given, it must be a list of regexps that must + *not* match any line in text.""" + + def assert_lines_match_kw(no=[]): + return no + no = assert_lines_match_kw(**kw) + + # Check text against regexps + lines = text.splitlines() + good = set() + bad = set() + for i, line in enumerate(lines): + if any(re.match(r, line) for r in regexps): + good.add(i) + regexps = [r for r in regexps if not re.match(r, line)] + if any(re.match(r, line) for r in no): + bad.add(i) + + if not regexps and not bad: + return + + # We failed; construct an informative failure message + show = set() + for lineno in good.union(bad): + for offset in range(-2, 3): + show.add(lineno + offset) + if regexps: + show.update(n for n in range(len(lines) - 5, len(lines))) + + msg = [] + last = -1 + for lineno in sorted(show): + if 0 <= lineno < len(lines): + if lineno != last + 1: + msg.append("...") + last = lineno + msg.append("%s %s" % (color("red", "BAD ") if lineno in bad else + color("green", "GOOD") if lineno in good + else " ", + lines[lineno])) + if last != len(lines) - 1: + msg.append("...") + if bad: + msg.append("unexpected lines in output") + for r in regexps: + msg.append(color("red", "MISSING") + " '%s'" % r) + raise AssertionError("\n".join(msg)) + +################################################################## +# Utilities +# + +__all__ += ["make", "maybe_unlink", "reset_fs", "color", "random_str"] + +MAKE_TIMESTAMP = 0 + +def pre_make(): + """Delay prior to running make to ensure file mtimes change.""" + while int(time.time()) == MAKE_TIMESTAMP: + time.sleep(0.1) + +def post_make(): + """Record the time after make completes so that the next run of + make can be delayed if needed.""" + global MAKE_TIMESTAMP + MAKE_TIMESTAMP = int(time.time()) + +def make(*target): + pre_make() + if Popen(("make",) + target).wait(): + sys.exit(1) + post_make() + +def show_command(cmd): + from pipes import quote + print("\n$", " ".join(map(quote, cmd))) + +def maybe_unlink(*paths): + for path in paths: + try: + os.unlink(path) + except EnvironmentError as e: + if e.errno != errno.ENOENT: + raise + +COLORS = {"default": "\033[0m", "red": "\033[31m", "green": "\033[32m"} + +def color(name, text): + if options.color == "always" or (options.color == "auto" and os.isatty(1)): + return COLORS[name] + text + COLORS["default"] + return text + +def reset_fs(): + if os.path.exists("obj/fs/clean-fs.img"): + shutil.copyfile("obj/fs/clean-fs.img", "obj/fs/fs.img") + +def random_str(n=8): + letters = string.ascii_letters + string.digits + return ''.join(random.choice(letters) for _ in range(n)) + +################################################################## +# Controllers +# + +__all__ += ["QEMU", "GDBClient"] + +class QEMU(object): + _GDBPORT = None + + def __init__(self, *make_args): + # Check that QEMU is not currently running + try: + GDBClient(self.get_gdb_port(), timeout=0).close() + except socket.error: + pass + else: + print("""\ +GDB stub found on port %d. +QEMU appears to already be running. Please exit it if possible or use +'killall qemu' or 'killall qemu.real'.""" % self.get_gdb_port(), file=sys.stderr) + sys.exit(1) + + if options.verbose: + show_command(("make",) + make_args) + cmd = ("make", "-s", "--no-print-directory") + make_args + self.proc = Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE) + # Accumulated output as a string + self.output = "" + # Accumulated output as a bytearray + self.outbytes = bytearray() + self.on_output = [] + + @staticmethod + def get_gdb_port(): + if QEMU._GDBPORT is None: + p = Popen(["make", "-s", "--no-print-directory", "print-gdbport"], + stdout=subprocess.PIPE) + (out, _) = p.communicate() + if p.returncode: + raise RuntimeError( + "Failed to get gdbport: make exited with %d" % + p.returncode) + QEMU._GDBPORT = int(out) + return QEMU._GDBPORT + + def fileno(self): + if self.proc: + return self.proc.stdout.fileno() + + def handle_read(self): + buf = os.read(self.proc.stdout.fileno(), 4096) + self.outbytes.extend(buf) + self.output = self.outbytes.decode("utf-8", "replace") + for callback in self.on_output: + callback(buf) + if buf == b"": + self.wait() + return + + def write(self, buf): + if isinstance(buf, str): + buf = buf.encode('utf-8') + self.proc.stdin.write(buf) + self.proc.stdin.flush() + + def wait(self): + if self.proc: + self.proc.wait() + self.proc = None + + def kill(self): + if self.proc: + self.proc.terminate() + +class GDBClient(object): + def __init__(self, port, timeout=15): + start = time.time() + while True: + self.sock = socket.socket() + try: + self.sock.settimeout(1) + self.sock.connect(("localhost", port)) + break + except socket.error: + if time.time() >= start + timeout: + raise + self.__buf = "" + + def fileno(self): + if self.sock: + return self.sock.fileno() + + def handle_read(self): + try: + data = self.sock.recv(4096).decode("ascii", "replace") + except socket.error: + data = "" + if data == "": + self.sock.close() + self.sock = None + return + self.__buf += data + + while True: + m = re.search(r"\$([^#]*)#[0-9a-zA-Z]{2}", self.__buf) + if not m: + break + pkt = m.group(1) + self.__buf = self.__buf[m.end():] + + if pkt.startswith("T05"): + # Breakpoint + raise TerminateTest + + def __send(self, cmd): + packet = "$%s#%02x" % (cmd, sum(map(ord, cmd)) % 256) + self.sock.sendall(packet.encode("ascii")) + + def __send_break(self): + self.sock.sendall(b"\x03") + + def close(self): + if self.sock: + self.sock.close() + self.sock = None + + def cont(self): + self.__send("c") + + def breakpoint(self, addr): + self.__send("Z1,%x,1" % addr) + + +################################################################## +# QEMU test runner +# + +__all__ += ["TerminateTest", "Runner"] + +class TerminateTest(Exception): + pass + +class Runner(): + def __init__(self, *default_monitors): + self.__default_monitors = default_monitors + + def run_qemu(self, *monitors, **kw): + """Run a QEMU-based test. monitors should functions that will + be called with this Runner instance once QEMU and GDB are + started. Typically, they should register callbacks that throw + TerminateTest when stop events occur. The target_base + argument gives the make target to run. The make_args argument + should be a list of additional arguments to pass to make. The + timeout argument bounds how long to run before returning.""" + + def run_qemu_kw(target_base="qemu", make_args=[], timeout=30): + return target_base, make_args, timeout + target_base, make_args, timeout = run_qemu_kw(**kw) + + # Start QEMU + pre_make() + self.qemu = QEMU(target_base + "-gdb", *make_args) + self.gdb = None + + try: + # Wait for QEMU to start or make to fail. This will set + # self.gdb if QEMU starts. + self.qemu.on_output = [self.__monitor_start] + self.__react([self.qemu], timeout=30) + self.qemu.on_output = [] + if self.gdb is None: + print("Failed to connect to QEMU; output:") + print(self.qemu.output) + sys.exit(1) + post_make() + + # QEMU and GDB are up + self.reactors = [self.qemu, self.gdb] + + # Start monitoring + for m in self.__default_monitors + monitors: + m(self) + + # Run and react + self.gdb.cont() + self.__react(self.reactors, timeout) + finally: + # Shutdown QEMU + try: + if self.gdb is None: + sys.exit(1) + self.qemu.kill() + self.__react(self.reactors, 5) + self.gdb.close() + self.qemu.wait() + except: + print("""\ +Failed to shutdown QEMU. You might need to 'killall qemu' or +'killall qemu.real'. +""") + raise + + def __monitor_start(self, output): + if b"\n" in output: + try: + self.gdb = GDBClient(self.qemu.get_gdb_port(), timeout=2) + raise TerminateTest + except socket.error: + pass + if not len(output): + raise TerminateTest + + def __react(self, reactors, timeout): + deadline = time.time() + timeout + try: + while True: + timeleft = deadline - time.time() + if timeleft < 0: + sys.stdout.write("Timeout! ") + sys.stdout.flush() + return + + rset = [r for r in reactors if r.fileno() is not None] + if not rset: + return + + rset, _, _ = select.select(rset, [], [], timeleft) + for reactor in rset: + reactor.handle_read() + except TerminateTest: + pass + + def user_test(self, binary, *monitors, **kw): + """Run a user test using the specified binary. Monitors and + keyword arguments are as for run_qemu. This runs on a disk + snapshot unless the keyword argument 'snapshot' is False.""" + + maybe_unlink("obj/kern/init.o", "obj/kern/kernel") + if kw.pop("snapshot", True): + kw.setdefault("make_args", []).append("QEMUEXTRA+=-snapshot") + self.run_qemu(target_base="run-%s" % binary, *monitors, **kw) + + def match(self, *args, **kwargs): + """Shortcut to call assert_lines_match on the most recent QEMU + output.""" + + assert_lines_match(self.qemu.output, *args, **kwargs) + +################################################################## +# Monitors +# + +__all__ += ["save", "stop_breakpoint", "call_on_line", "stop_on_line", "shell_script"] + +def save(path): + """Return a monitor that writes QEMU's output to path. If the + test fails, copy the output to path.test-name.""" + + def setup_save(runner): + f.seek(0) + f.truncate() + runner.qemu.on_output.append(f.write) + get_current_test().on_finish.append(save_on_finish) + + def save_on_finish(fail): + f.flush() + save_path = path + "." + get_current_test().__name__[5:] + if fail: + shutil.copyfile(path, save_path) + print(" QEMU output saved to %s" % save_path) + elif os.path.exists(save_path): + os.unlink(save_path) + print(" (Old %s failure log removed)" % save_path) + + f = open(path, "wb") + return setup_save + +def stop_breakpoint(addr): + """Returns a monitor that stops when addr is reached. addr may be + a number or the name of a symbol.""" + + def setup_breakpoint(runner): + if isinstance(addr, str): + addrs = [int(sym[:16], 16) for sym in open("kernel/kernel.sym") + if sym[17:].strip() == addr] + assert len(addrs), "Symbol %s not found" % addr + runner.gdb.breakpoint(addrs[0]) + else: + runner.gdb.breakpoint(addr) + return setup_breakpoint + +def call_on_line(regexp, callback): + """Returns a monitor that calls 'callback' when QEMU prints a line + matching 'regexp'.""" + + def setup_call_on_line(runner): + buf = bytearray() + def handle_output(output): + buf.extend(output) + while b"\n" in buf: + line, buf[:] = buf.split(b"\n", 1) + line = line.decode("utf-8", "replace") + if re.match(regexp, line): + callback(line) + runner.qemu.on_output.append(handle_output) + return setup_call_on_line + +def stop_on_line(regexp): + """Returns a monitor that stops when QEMU prints a line matching + 'regexp'.""" + + def stop(line): + raise TerminateTest + return call_on_line(regexp, stop) + +def shell_script(script, terminate_match=None): + """Returns a monitor that plays the script, and stops when the script is + done executing.""" + + def setup_call_on_line(runner): + class context: + n = 0 + buf = bytearray() + def handle_output(output): + context.buf.extend(output) + if terminate_match is not None: + if re.match(terminate_match, context.buf.decode('utf-8')): + raise TerminateTest + if b'$ ' in context.buf: + context.buf = bytearray() + if context.n < len(script): + runner.qemu.write(script[context.n]) + runner.qemu.write('\n') + context.n += 1 + else: + if terminate_match is None: + raise TerminateTest + runner.qemu.on_output.append(handle_output) + return setup_call_on_line diff --git a/user/xargstest.sh b/user/xargstest.sh new file mode 100644 index 0000000000..4362589c83 --- /dev/null +++ b/user/xargstest.sh @@ -0,0 +1,6 @@ +mkdir a +echo hello > a/b +mkdir c +echo hello > c/b +echo hello > b +find . b | xargs grep hello From 3f78d72f471d0d56f0dfb64187942dbf5201b573 Mon Sep 17 00:00:00 2001 From: Stanislav Kramar Date: Tue, 6 Dec 2022 01:25:45 +0100 Subject: [PATCH 02/12] exclude vscode, jetbrains files, and grading logs from git --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index 07216f3ad0..2e8cf40622 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +# Build ignores *~ _* *.o @@ -5,6 +6,7 @@ _* *.asm *.sym *.img +*.out vectors.S bootblock entryother @@ -15,3 +17,7 @@ mkfs kernel/kernel user/usys.S .gdbinit + +# IDE ignores +.vscode/ +.idea/ From a063810e3462a36e3835db7a62e2ea1730e1d767 Mon Sep 17 00:00:00 2001 From: Stanislav Kramar Date: Tue, 6 Dec 2022 01:27:35 +0100 Subject: [PATCH 03/12] add user-defined constants --- user/defs.h | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 user/defs.h diff --git a/user/defs.h b/user/defs.h new file mode 100644 index 0000000000..f139f14c60 --- /dev/null +++ b/user/defs.h @@ -0,0 +1,14 @@ +#ifndef _XV6_USER_DEFS_H_ +#define _XV6_USER_DEFS_H_ + +#define EXIT_SUCCESS 0 +#define EXIT_FAILURE 1 + +#define STDIN 0 +#define STDOUT 1 +#define STDERR 2 + +#define INT_MAX 1 << (sizeof(int) * 8 - 1) + +#endif // _XV6_USER_DEFS_H_ + From 014921ae58156843b8e1281b0684e829453a8650 Mon Sep 17 00:00:00 2001 From: Stanislav Kramar Date: Tue, 6 Dec 2022 01:28:40 +0100 Subject: [PATCH 04/12] implement sleep utility --- Makefile | 1 + user/sleep.c | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 user/sleep.c diff --git a/Makefile b/Makefile index 51b23ab8f0..00ab90541c 100644 --- a/Makefile +++ b/Makefile @@ -132,6 +132,7 @@ UPROGS=\ $U/_grind\ $U/_wc\ $U/_zombie\ + $U/_sleep\ fs.img: mkfs/mkfs README $(UPROGS) mkfs/mkfs fs.img README user/xargstest.sh $(UPROGS) diff --git a/user/sleep.c b/user/sleep.c new file mode 100644 index 0000000000..28f73c2cc3 --- /dev/null +++ b/user/sleep.c @@ -0,0 +1,29 @@ +#include "kernel/types.h" +#include "kernel/stat.h" +#include "user/defs.h" +#include "user/user.h" + +int main(int argc, char** argv) { + int exit_code; + if (argc < 2) { + fprintf(STDERR, "usage: sleep ticks\n"); + exit_code = EXIT_FAILURE; + } else { + const char* input = argv[1]; + int ticks; + if (strcmp(input, "0")) { + ticks = atoi(input); + if (ticks <= 0) { + fprintf(STDERR, "sleep: ticks should be an integer value less than %d\n", INT_MAX); + exit(EXIT_FAILURE); + } + } else { + ticks = 0; + } + + sleep(ticks); + } + + exit(exit_code); +} + From f470f12d5a228e6197134f6c540004a5232542ce Mon Sep 17 00:00:00 2001 From: Stanislav Kramar Date: Tue, 6 Dec 2022 01:29:38 +0100 Subject: [PATCH 05/12] implement pingpong utility --- Makefile | 1 + user/pingpong.c | 68 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 user/pingpong.c diff --git a/Makefile b/Makefile index 00ab90541c..0f22864385 100644 --- a/Makefile +++ b/Makefile @@ -133,6 +133,7 @@ UPROGS=\ $U/_wc\ $U/_zombie\ $U/_sleep\ + $U/_pingpong\ fs.img: mkfs/mkfs README $(UPROGS) mkfs/mkfs fs.img README user/xargstest.sh $(UPROGS) diff --git a/user/pingpong.c b/user/pingpong.c new file mode 100644 index 0000000000..076e7a52e1 --- /dev/null +++ b/user/pingpong.c @@ -0,0 +1,68 @@ +#include "kernel/types.h" +#include "kernel/stat.h" +#include "user/defs.h" +#include "user/user.h" + +#define PING 7 // random bytes +#define PONG 8 + +int main(int argc, char **argv) +{ + int pingfd[2]; + int pongfd[2]; + if (pipe(pingfd) < 0 || pipe(pongfd) < 0) + { + fprintf(STDERR, "error: pipe failed\n"); + exit(EXIT_FAILURE); + } + + int child = fork(); + + if (child == 0) + { + uint8 ping; + read(pingfd[0], &ping, sizeof(uint8)); + + if (ping == PING) + { + uint8 pong = PONG; + fprintf(STDOUT, "%d: received ping\n", getpid()); + write(pongfd[1], &pong, sizeof(uint8)); + close(pongfd[1]); + } + else + { + fprintf(STDERR, "error: failed to read ping\n"); + exit(EXIT_FAILURE); + } + + close(pingfd[0]); + } + else if (child > 0) + { + uint8 ping = PING; + write(pingfd[1], &ping, sizeof(uint8)); + close(pingfd[1]); + + uint8 pong; + read(pongfd[0], &pong, sizeof(uint8)); + close(pongfd[0]); + + if (pong == PONG) + { + fprintf(STDOUT, "%d: received pong\n", getpid()); + } + else + { + fprintf(STDERR, "error: failed to read pong\n"); + exit(EXIT_FAILURE); + } + } + else + { + fprintf(STDERR, "error: fork failed\n"); + exit(EXIT_FAILURE); + } + + exit(EXIT_SUCCESS); +} From 1bbca2285eb259ff44b4eac5e56403bf597184e1 Mon Sep 17 00:00:00 2001 From: Stanislav Kramar Date: Tue, 6 Dec 2022 01:30:13 +0100 Subject: [PATCH 06/12] implement primes utility --- Makefile | 1 + user/primes.c | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 user/primes.c diff --git a/Makefile b/Makefile index 0f22864385..e74f729a0a 100644 --- a/Makefile +++ b/Makefile @@ -134,6 +134,7 @@ UPROGS=\ $U/_zombie\ $U/_sleep\ $U/_pingpong\ + $U/_primes\ fs.img: mkfs/mkfs README $(UPROGS) mkfs/mkfs fs.img README user/xargstest.sh $(UPROGS) diff --git a/user/primes.c b/user/primes.c new file mode 100644 index 0000000000..162f7dca97 --- /dev/null +++ b/user/primes.c @@ -0,0 +1,65 @@ +#include "kernel/types.h" +#include "kernel/stat.h" +#include "user/defs.h" +#include "user/user.h" + +#define SIEVE_LIMIT 35 + + +void sieve(int pipe_in); + +int main(int argc, char **argv) { + int sieve_pipe[2]; + + if (pipe(sieve_pipe) < 0) { + fprintf(STDERR, "error: pipe failed\n"); + exit(EXIT_FAILURE); + } + + for (int i = 2; i < SIEVE_LIMIT; i += 1) { + write(sieve_pipe[1], &i, sizeof(int)); + } + close(sieve_pipe[1]); + + sieve(sieve_pipe[0]); + + exit(EXIT_SUCCESS); +} + + +void sieve(int pipe_in) { + int child = fork(); + + if (child == 0) { + int next_pipe[2]; + if (pipe(next_pipe) < 0) { + fprintf(STDERR, "error: pipe failed\n"); + exit(EXIT_FAILURE); + } + + int prime; + read(pipe_in, &prime, sizeof(int)); + fprintf(STDOUT, "prime %d\n", prime); + + int next = -1; + while (read(pipe_in, &next, sizeof(int)) > 0) { + if (next % prime != 0) { + write(next_pipe[1], &next, sizeof(int)); + } + } + close(next_pipe[1]); + close(pipe_in); + + if (next != -1) { + sieve(next_pipe[0]); + } else { + close(next_pipe[0]); + } + } else if (child > 0) { + close(pipe_in); + wait(0); + } else { + fprintf(STDERR, "error: fork failed\n"); + exit(EXIT_FAILURE); + } +} From 88be3d0289dd315cd935ff1d476ada2238f40bbc Mon Sep 17 00:00:00 2001 From: Stanislav Kramar Date: Tue, 6 Dec 2022 01:30:39 +0100 Subject: [PATCH 07/12] implement find utility --- Makefile | 1 + user/find.c | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 user/find.c diff --git a/Makefile b/Makefile index e74f729a0a..da29d23481 100644 --- a/Makefile +++ b/Makefile @@ -135,6 +135,7 @@ UPROGS=\ $U/_sleep\ $U/_pingpong\ $U/_primes\ + $U/_find\ fs.img: mkfs/mkfs README $(UPROGS) mkfs/mkfs fs.img README user/xargstest.sh $(UPROGS) diff --git a/user/find.c b/user/find.c new file mode 100644 index 0000000000..eb4ce2842f --- /dev/null +++ b/user/find.c @@ -0,0 +1,111 @@ +#include "kernel/types.h" +#include "kernel/stat.h" +#include "kernel/fs.h" +#include "kernel/param.h" +#include "user/defs.h" +#include "user/user.h" + +void find(char *search_dir, char *search_file); + +int main(int argc, char **argv) +{ + if (argc < 3) + { + fprintf(STDERR, "usage: find directory expression\n"); + exit(EXIT_FAILURE); + } + + char *search_dir = argv[1]; + char *search_file = argv[2]; + + int search_dir_fd; + if ((search_dir_fd = open(search_dir, 0)) < 0) + { + fprintf(STDERR, "find: cannot open %s\n", search_dir); + exit(EXIT_FAILURE); + } + + struct stat search_dir_stat; + if (fstat(search_dir_fd, &search_dir_stat) < 0) + { + fprintf(STDERR, "find: cannot stat %s\n", search_dir); + close(search_dir_fd); + exit(EXIT_FAILURE); + } + close(search_dir_fd); + + switch (search_dir_stat.type) + { + case T_DIR: + find(search_dir, search_file); + break; + case T_DEVICE: + case T_FILE: + printf("find : `%s`: not a directory\n", search_dir); + break; + } + + exit(EXIT_SUCCESS); +} + +void find(char *search_dir, char *search_file) +{ + int dir_fd; + if ((dir_fd = open(search_dir, 0)) < 0) + { + fprintf(STDERR, "find: cannot open %s\n", search_dir); + exit(EXIT_FAILURE); + } + + char path[MAXPATH]; + char *path_end = path + strlen(search_dir); + strcpy(path, search_dir); + + struct dirent de; + while (read(dir_fd, &de, sizeof(de)) == sizeof(de)) + { + char* filename = de.name; + if (de.inum != 0 && strcmp(filename, ".") && strcmp(filename, "..")) + { + if (*(path_end - 1) != '/') + { + strcpy(path_end, "/"); + path_end += 1; + } + strcpy(path_end, filename); + + int fd; + if ((fd = open(path, 0)) < 0) + { + fprintf(STDERR, "find: cannot open %s\n", path); + continue; + } + + struct stat file_stat; + if (fstat(fd, &file_stat) < 0) + { + fprintf(STDERR, "find: cannot stat %s\n", path); + close(fd); + continue; + } + + switch (file_stat.type) + { + case T_DIR: + find(path, search_file); + break; + case T_DEVICE: + break; + case T_FILE: + if (!strcmp(search_file, filename)) + { + printf("%s\n", path); + } + break; + } + close(fd); + } + } + + close(dir_fd); +} From 770543a01af15b2c22f39ba1a6ff023afd954d9a Mon Sep 17 00:00:00 2001 From: Stanislav Kramar Date: Tue, 6 Dec 2022 01:31:10 +0100 Subject: [PATCH 08/12] implement xargs utility --- Makefile | 1 + user/xargs.c | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 user/xargs.c diff --git a/Makefile b/Makefile index da29d23481..4bc7ec3bd3 100644 --- a/Makefile +++ b/Makefile @@ -136,6 +136,7 @@ UPROGS=\ $U/_pingpong\ $U/_primes\ $U/_find\ + $U/_xargs\ fs.img: mkfs/mkfs README $(UPROGS) mkfs/mkfs fs.img README user/xargstest.sh $(UPROGS) diff --git a/user/xargs.c b/user/xargs.c new file mode 100644 index 0000000000..e6a919edf5 --- /dev/null +++ b/user/xargs.c @@ -0,0 +1,108 @@ +#include "kernel/param.h" +#include "kernel/types.h" +#include "kernel/stat.h" +#include "user/defs.h" +#include "user/user.h" + +#define MAX_LINE_LENGTH 100 + +int get_line(char *buffer); +void parse_line(char *line, char **xargv, int *xargc, int argc); +void fork_exec(int* child, char**xargv, int xargc, int argc); +void free_xargv(char **xargv, int from, int to); + +int main(int argc, char **argv) +{ + char *xargv[MAXARG]; + + for (int i = 1; i < argc; i++) + { + xargv[i - 1] = argv[i]; + } + argc -= 1; + + int child = 0; + char line[MAX_LINE_LENGTH]; + while (get_line(line) > 0) + { + int xargc; + parse_line(line, xargv, &xargc, argc); + fork_exec(&child, xargv, xargc, argc); + } + + if (child > 0) + { + free_xargv(xargv, 0, argc); + } + + exit(EXIT_SUCCESS); +} + +int get_line(char *buffer) +{ + gets(buffer, MAX_LINE_LENGTH); + return buffer[0] != 0; +} + +void parse_line(char *line, char **xargv, int *xargc, int argc) +{ + *xargc = argc; + int i = 0; + while (line[i]) + { + const char *whitespaces = " \t\n\v"; + while (strchr(whitespaces, line[i])) + { + i++; + } + if (!line[i]) + { + break; + } + + int j = 0; + while (!strchr(whitespaces, line[i])) + { + j++; + i++; + } + xargv[*xargc] = malloc(j * sizeof(char)); + strcpy(xargv[*xargc], line + i - j); + xargv[*xargc][j] = 0; + + line[i++] = 0; + (*xargc)++; + } +} + +void fork_exec(int* child, char**xargv, int xargc, int argc) { + *child = fork(); + if (*child == 0) + { + int result = exec(xargv[0], xargv); + if (result < 0) + { + printf("exec: failed to execute: %s\n", xargv[0]); + free_xargv(xargv, 0, xargc); + exit(EXIT_FAILURE); + } + } + else if (*child > 0) + { + free_xargv(xargv, argc, xargc); + wait(0); + } + else + { + fprintf(STDERR, "error: fork faled\n"); + exit(EXIT_FAILURE); + } +} + +void free_xargv(char **xargv, int from, int to) +{ + for (int i = from; i < to; i++) + { + free(xargv[i]); + } +} \ No newline at end of file From ff28f963c37bdd4eec1ee1509778ba66c77b573a Mon Sep 17 00:00:00 2001 From: Stanislav Kramar Date: Tue, 6 Dec 2022 02:04:21 +0100 Subject: [PATCH 09/12] avoid manual freeing of argv and fix potental memory vulnerability --- user/xargs.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/user/xargs.c b/user/xargs.c index e6a919edf5..da5357411a 100644 --- a/user/xargs.c +++ b/user/xargs.c @@ -8,7 +8,7 @@ int get_line(char *buffer); void parse_line(char *line, char **xargv, int *xargc, int argc); -void fork_exec(int* child, char**xargv, int xargc, int argc); +void fork_exec(int *child, char **xargv, int xargc, int argc); void free_xargv(char **xargv, int from, int to); int main(int argc, char **argv) @@ -17,7 +17,9 @@ int main(int argc, char **argv) for (int i = 1; i < argc; i++) { - xargv[i - 1] = argv[i]; + uint arglen = strlen(argv[i]) + 1; + xargv[i - 1] = malloc(arglen); + strcpy(xargv[i - 1], argv[i]); } argc -= 1; @@ -66,7 +68,7 @@ void parse_line(char *line, char **xargv, int *xargc, int argc) j++; i++; } - xargv[*xargc] = malloc(j * sizeof(char)); + xargv[*xargc] = malloc(j * sizeof(char) + 1); strcpy(xargv[*xargc], line + i - j); xargv[*xargc][j] = 0; @@ -75,7 +77,8 @@ void parse_line(char *line, char **xargv, int *xargc, int argc) } } -void fork_exec(int* child, char**xargv, int xargc, int argc) { +void fork_exec(int *child, char **xargv, int xargc, int argc) +{ *child = fork(); if (*child == 0) { From edf505f274a0017771b3ce97a7fa1d5180bff38c Mon Sep 17 00:00:00 2001 From: Stanislav Kramar Date: Tue, 6 Dec 2022 02:50:42 +0100 Subject: [PATCH 10/12] reformat according webkit --- user/defs.h | 1 - user/find.c | 52 +++++++++++++++++------------------------- user/pingpong.c | 30 ++++++++----------------- user/primes.c | 8 +++---- user/sleep.c | 9 ++++---- user/xargs.c | 60 +++++++++++++++++++------------------------------ 6 files changed, 60 insertions(+), 100 deletions(-) diff --git a/user/defs.h b/user/defs.h index f139f14c60..7af23bf9ee 100644 --- a/user/defs.h +++ b/user/defs.h @@ -11,4 +11,3 @@ #define INT_MAX 1 << (sizeof(int) * 8 - 1) #endif // _XV6_USER_DEFS_H_ - diff --git a/user/find.c b/user/find.c index eb4ce2842f..4f8b0efb89 100644 --- a/user/find.c +++ b/user/find.c @@ -1,41 +1,37 @@ #include "kernel/types.h" -#include "kernel/stat.h" #include "kernel/fs.h" #include "kernel/param.h" +#include "kernel/stat.h" #include "user/defs.h" #include "user/user.h" -void find(char *search_dir, char *search_file); +void find(char* search_dir, char* search_file); -int main(int argc, char **argv) +int main(int argc, char** argv) { - if (argc < 3) - { + if (argc < 3) { fprintf(STDERR, "usage: find directory expression\n"); exit(EXIT_FAILURE); } - char *search_dir = argv[1]; - char *search_file = argv[2]; + char* search_dir = argv[1]; + char* search_file = argv[2]; int search_dir_fd; - if ((search_dir_fd = open(search_dir, 0)) < 0) - { + if ((search_dir_fd = open(search_dir, 0)) < 0) { fprintf(STDERR, "find: cannot open %s\n", search_dir); exit(EXIT_FAILURE); } struct stat search_dir_stat; - if (fstat(search_dir_fd, &search_dir_stat) < 0) - { + if (fstat(search_dir_fd, &search_dir_stat) < 0) { fprintf(STDERR, "find: cannot stat %s\n", search_dir); close(search_dir_fd); exit(EXIT_FAILURE); } close(search_dir_fd); - switch (search_dir_stat.type) - { + switch (search_dir_stat.type) { case T_DIR: find(search_dir, search_file); break; @@ -48,57 +44,49 @@ int main(int argc, char **argv) exit(EXIT_SUCCESS); } -void find(char *search_dir, char *search_file) +void find(char* search_dir, char* search_file) { int dir_fd; - if ((dir_fd = open(search_dir, 0)) < 0) - { + if ((dir_fd = open(search_dir, 0)) < 0) { fprintf(STDERR, "find: cannot open %s\n", search_dir); exit(EXIT_FAILURE); } - + char path[MAXPATH]; - char *path_end = path + strlen(search_dir); + char* path_end = path + strlen(search_dir); strcpy(path, search_dir); struct dirent de; - while (read(dir_fd, &de, sizeof(de)) == sizeof(de)) - { + while (read(dir_fd, &de, sizeof(de)) == sizeof(de)) { char* filename = de.name; - if (de.inum != 0 && strcmp(filename, ".") && strcmp(filename, "..")) - { - if (*(path_end - 1) != '/') - { + if (de.inum != 0 && strcmp(filename, ".") && strcmp(filename, "..")) { + if (*(path_end - 1) != '/') { strcpy(path_end, "/"); path_end += 1; } strcpy(path_end, filename); int fd; - if ((fd = open(path, 0)) < 0) - { + if ((fd = open(path, 0)) < 0) { fprintf(STDERR, "find: cannot open %s\n", path); continue; } struct stat file_stat; - if (fstat(fd, &file_stat) < 0) - { + if (fstat(fd, &file_stat) < 0) { fprintf(STDERR, "find: cannot stat %s\n", path); close(fd); continue; } - switch (file_stat.type) - { + switch (file_stat.type) { case T_DIR: find(path, search_file); break; case T_DEVICE: break; case T_FILE: - if (!strcmp(search_file, filename)) - { + if (!strcmp(search_file, filename)) { printf("%s\n", path); } break; diff --git a/user/pingpong.c b/user/pingpong.c index 076e7a52e1..d412f51a88 100644 --- a/user/pingpong.c +++ b/user/pingpong.c @@ -6,40 +6,33 @@ #define PING 7 // random bytes #define PONG 8 -int main(int argc, char **argv) +int main(int argc, char** argv) { int pingfd[2]; int pongfd[2]; - if (pipe(pingfd) < 0 || pipe(pongfd) < 0) - { + if (pipe(pingfd) < 0 || pipe(pongfd) < 0) { fprintf(STDERR, "error: pipe failed\n"); exit(EXIT_FAILURE); } int child = fork(); - if (child == 0) - { + if (child == 0) { uint8 ping; read(pingfd[0], &ping, sizeof(uint8)); - if (ping == PING) - { + if (ping == PING) { uint8 pong = PONG; fprintf(STDOUT, "%d: received ping\n", getpid()); write(pongfd[1], &pong, sizeof(uint8)); close(pongfd[1]); - } - else - { + } else { fprintf(STDERR, "error: failed to read ping\n"); exit(EXIT_FAILURE); } close(pingfd[0]); - } - else if (child > 0) - { + } else if (child > 0) { uint8 ping = PING; write(pingfd[1], &ping, sizeof(uint8)); close(pingfd[1]); @@ -48,18 +41,13 @@ int main(int argc, char **argv) read(pongfd[0], &pong, sizeof(uint8)); close(pongfd[0]); - if (pong == PONG) - { + if (pong == PONG) { fprintf(STDOUT, "%d: received pong\n", getpid()); - } - else - { + } else { fprintf(STDERR, "error: failed to read pong\n"); exit(EXIT_FAILURE); } - } - else - { + } else { fprintf(STDERR, "error: fork failed\n"); exit(EXIT_FAILURE); } diff --git a/user/primes.c b/user/primes.c index 162f7dca97..c240f15620 100644 --- a/user/primes.c +++ b/user/primes.c @@ -5,10 +5,10 @@ #define SIEVE_LIMIT 35 - void sieve(int pipe_in); -int main(int argc, char **argv) { +int main(int argc, char** argv) +{ int sieve_pipe[2]; if (pipe(sieve_pipe) < 0) { @@ -26,8 +26,8 @@ int main(int argc, char **argv) { exit(EXIT_SUCCESS); } - -void sieve(int pipe_in) { +void sieve(int pipe_in) +{ int child = fork(); if (child == 0) { diff --git a/user/sleep.c b/user/sleep.c index 28f73c2cc3..0ff4e75009 100644 --- a/user/sleep.c +++ b/user/sleep.c @@ -3,11 +3,11 @@ #include "user/defs.h" #include "user/user.h" -int main(int argc, char** argv) { - int exit_code; +int main(int argc, char** argv) +{ if (argc < 2) { fprintf(STDERR, "usage: sleep ticks\n"); - exit_code = EXIT_FAILURE; + exit(EXIT_FAILURE); } else { const char* input = argv[1]; int ticks; @@ -24,6 +24,5 @@ int main(int argc, char** argv) { sleep(ticks); } - exit(exit_code); + exit(EXIT_SUCCESS); } - diff --git a/user/xargs.c b/user/xargs.c index da5357411a..9ccf774f72 100644 --- a/user/xargs.c +++ b/user/xargs.c @@ -6,17 +6,16 @@ #define MAX_LINE_LENGTH 100 -int get_line(char *buffer); -void parse_line(char *line, char **xargv, int *xargc, int argc); -void fork_exec(int *child, char **xargv, int xargc, int argc); -void free_xargv(char **xargv, int from, int to); +int get_line(char* buffer); +void parse_line(char* line, char** xargv, int* xargc, int argc); +void fork_exec(int* child, char** xargv, int xargc, int argc); +void free_xargv(char** xargv, int from, int to); -int main(int argc, char **argv) +int main(int argc, char** argv) { - char *xargv[MAXARG]; + char* xargv[MAXARG]; - for (int i = 1; i < argc; i++) - { + for (int i = 1; i < argc; i++) { uint arglen = strlen(argv[i]) + 1; xargv[i - 1] = malloc(arglen); strcpy(xargv[i - 1], argv[i]); @@ -25,46 +24,40 @@ int main(int argc, char **argv) int child = 0; char line[MAX_LINE_LENGTH]; - while (get_line(line) > 0) - { + while (get_line(line) > 0) { int xargc; parse_line(line, xargv, &xargc, argc); fork_exec(&child, xargv, xargc, argc); } - if (child > 0) - { + if (child > 0) { free_xargv(xargv, 0, argc); } exit(EXIT_SUCCESS); } -int get_line(char *buffer) +int get_line(char* buffer) { gets(buffer, MAX_LINE_LENGTH); return buffer[0] != 0; } -void parse_line(char *line, char **xargv, int *xargc, int argc) +void parse_line(char* line, char** xargv, int* xargc, int argc) { *xargc = argc; int i = 0; - while (line[i]) - { - const char *whitespaces = " \t\n\v"; - while (strchr(whitespaces, line[i])) - { + while (line[i]) { + const char* whitespaces = " \t\n\v"; + while (strchr(whitespaces, line[i])) { i++; } - if (!line[i]) - { + if (!line[i]) { break; } int j = 0; - while (!strchr(whitespaces, line[i])) - { + while (!strchr(whitespaces, line[i])) { j++; i++; } @@ -77,35 +70,28 @@ void parse_line(char *line, char **xargv, int *xargc, int argc) } } -void fork_exec(int *child, char **xargv, int xargc, int argc) +void fork_exec(int* child, char** xargv, int xargc, int argc) { *child = fork(); - if (*child == 0) - { + if (*child == 0) { int result = exec(xargv[0], xargv); - if (result < 0) - { + if (result < 0) { printf("exec: failed to execute: %s\n", xargv[0]); free_xargv(xargv, 0, xargc); exit(EXIT_FAILURE); } - } - else if (*child > 0) - { + } else if (*child > 0) { free_xargv(xargv, argc, xargc); wait(0); - } - else - { + } else { fprintf(STDERR, "error: fork faled\n"); exit(EXIT_FAILURE); } } -void free_xargv(char **xargv, int from, int to) +void free_xargv(char** xargv, int from, int to) { - for (int i = from; i < to; i++) - { + for (int i = from; i < to; i++) { free(xargv[i]); } } \ No newline at end of file From b79fc15a3f21ef1bdc494eee0ce64cea90da5bf7 Mon Sep 17 00:00:00 2001 From: Stanislav Kramar Date: Tue, 6 Dec 2022 16:06:17 +0100 Subject: [PATCH 11/12] fix vulnerability by zeroing path_end --- user/find.c | 1 + 1 file changed, 1 insertion(+) diff --git a/user/find.c b/user/find.c index 4f8b0efb89..791b4f9231 100644 --- a/user/find.c +++ b/user/find.c @@ -64,6 +64,7 @@ void find(char* search_dir, char* search_file) strcpy(path_end, "/"); path_end += 1; } + *path_end = '\0'; strcpy(path_end, filename); int fd; From 3905e9d1a3ed08fcddb4859531914c018ef96b66 Mon Sep 17 00:00:00 2001 From: Stanislav Kramar Date: Tue, 6 Dec 2022 16:06:52 +0100 Subject: [PATCH 12/12] use \0 instead of 0 for chars and add newline at the end --- user/xargs.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user/xargs.c b/user/xargs.c index 9ccf774f72..601f68d6c9 100644 --- a/user/xargs.c +++ b/user/xargs.c @@ -63,9 +63,9 @@ void parse_line(char* line, char** xargv, int* xargc, int argc) } xargv[*xargc] = malloc(j * sizeof(char) + 1); strcpy(xargv[*xargc], line + i - j); - xargv[*xargc][j] = 0; + xargv[*xargc][j] = '\0'; - line[i++] = 0; + line[i++] = '\0'; (*xargc)++; } } @@ -94,4 +94,4 @@ void free_xargv(char** xargv, int from, int to) for (int i = from; i < to; i++) { free(xargv[i]); } -} \ No newline at end of file +}