about summary refs log tree commit diff stats
path: root/subx/057stop.subx
diff options
context:
space:
mode:
authorKartik Agaram <vc@akkartik.com>2018-10-21 21:04:38 -0700
committerKartik Agaram <vc@akkartik.com>2018-10-21 21:17:22 -0700
commitf3612481b1b589fff31ecd10ab0ddb083a5c8d35 (patch)
tree0796e7d3a796625c320109d3f06c5e77852d2085 /subx/057stop.subx
parent417a05ee7dbecbf846c54cd1e186e000415fb0a3 (diff)
downloadmu-f3612481b1b589fff31ecd10ab0ddb083a5c8d35.tar.gz
4713
Initial sketch of a dependency-injected wrapper around the exit() syscall.

I don't have the primitives yet, just a sketch of how they should work
-- and a passing test for non-local jumps without support for passing the
exit status to the caller.
Diffstat (limited to 'subx/057stop.subx')
-rw-r--r--subx/057stop.subx129
1 files changed, 129 insertions, 0 deletions
diff --git a/subx/057stop.subx b/subx/057stop.subx
new file mode 100644
index 00000000..8d574b96
--- /dev/null
+++ b/subx/057stop.subx
@@ -0,0 +1,129 @@
+# stop: dependency-injected wrapper around the exit() syscall
+#
+# We'd like to be able to write tests for functions calling exit() in production,
+# and to make assertions about whether they exit() or not.
+#
+# The basic plan goes like this: `stop` will take an 'exit descriptor' that's
+# opaque to callers. If it's null, it will call exit() directly. If it's not
+# null, it'll be a pointer into the stack. `stop` will unwind the stack to
+# that point, and use the value at that point as the address to 'return' to.
+#
+# No other processor state will be restored. We won't bother with registers,
+# signal handlers or anything else for now. A test function that wants to
+# protect against exit will create an exit descriptor (directly, without
+# wrapping function calls; the value of the stack pointer matters) and pass it
+# in to the function under test. After the function under test returns,
+# registers may be meaningless. The test function is responsible for determining
+# that.
+#
+#   to create an exit descriptor:
+#     store current value of ESP (say X)
+#
+#   to exit in the presence of an exit descriptor:
+#     copy value at X
+#     ensure ESP is greater than X + 4
+#     set ESP to X + 4
+#     save exit status in the exit descriptor
+#     jump to X
+#
+#   caller after returning from a function that was passed in the exit
+#   descriptor:
+#     check the exit status in the exit descriptor
+#     if it's 0, exit() was not called
+#       registers are valid
+#     if it's non-zero (say 'n'), exit() was called with value n-1
+#       registers are no longer valid
+#
+# An exit descriptor looks like this:
+#   target: address  # containing the return address to restore stack to
+#   value: int  # exit status if called
+#
+# It's illegal for the exit descriptor to be used after its creating function
+# call returns.
+#
+# This is basically a poor man's setjmp/longjmp. But setjmp/longjmp is defined
+# in libc, not in the kernel, so we need to implement it ourselves. Since our
+# use case is simpler, only needing to simulate exit() in tests, our implementation
+# is simpler as well. It's impossible to make setjmp/longjmp work safely in
+# all C programs, so we won't even bother. Just support this one use case and
+# stop (no pun intended). Anything else likely requires a more high-level
+# language with support for continuations.
+
+== code
+
+# instruction                     effective address                                                   operand     displacement    immediate
+# op          subop               mod             rm32          base        index         scale       r32
+# 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes
+
+# main:  (manual test if this is the last file loaded)
+  e8/call  run-tests/disp32  # 'run-tests' is a function created automatically by SubX. It calls all functions that start with 'test-'.
+  # syscall(exit, Num-test-failures)
+  8b/copy                         0/mod/indirect  5/rm32/.disp32            .             .           1/r32/EBX   Num-test-failures/disp32          # copy *Num-test-failures to EBX
+  b8/copy-to-EAX  1/imm32
+  cd/syscall  0x80/imm8
+
+# initialize an exit descriptor that has already been allocated by caller.
+# invoking `stop` on the exit descriptor will return to the caller's stack frame.
+create-exit-descriptor:  # address -> ()
+  # TODO
+
+stop:  # exit-descriptor, value
+  # TODO
+
+test-stop-skips-returns-on-exit:
+  # call _test-stop-1 with its exit descriptor: the location of its return
+  # address.
+  #
+  # This argument is currently uninitialized, but will be initialized in the
+  # 'call' instruction.
+  #
+  # The address passed in depends on the number of locals allocated on the
+  # stack.
+    # push arg
+  8d/copy-address                 1/mod/*+disp8   4/rm32/sib    4/base/ESP  4/index/none              0/r32/EAX   -8/disp8        .                 # copy ESP-8 to EAX
+  50/push-EAX
+    # call
+  e8/call  _test-stop-1/disp32
+    # discard arg
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # signal check passed: check-ints-equal(1, 1, msg)
+    # push args
+  68/push  "F - test-stop-skips-returns-on-exit"/imm32
+  68/push  1/imm32
+  68/push  1/imm32
+    # call
+  e8/call  check-ints-equal/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+  c3/return
+
+_test-stop-1:  # unwind-mark : address
+  # prolog
+  55/push-EBP
+  89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
+  # _test-stop-2(unwind-mark)
+    # push arg
+  ff          6/subop/push        1/mod/*+disp8   4/rm32/sib    5/base/EBP  4/index/none  .           .           8/disp8         .                 # push *(EBP+8)
+    # call
+  e8/call  _test-stop-2/disp32
+  ## should never get past this point
+    # discard arg
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
+  # signal test failed: check-ints-equal(1, 0, msg)
+    # push args
+  68/push  "F - test-stop-skips-returns-on-exit"/imm32
+  68/push  0/imm32
+  68/push  1/imm32
+    # call
+  e8/call  check-ints-equal/disp32
+    # discard args
+  81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
+  # epilog
+  89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
+  5d/pop-to-EBP
+  c3/return
+
+_test-stop-2:  # unwind-mark : address
+  # non-local jump to unwind-mark
+  8b/copy                         1/mod/*+disp8   4/rm32/sib    4/base/ESP  4/index/none              4/r32/ESP   4/disp8                           # copy *(ESP+4) to ESP
+  c3/return  # doesn't return to caller