about summary refs log blame commit diff stats
path: root/subx/057stop.subx
blob: 8d574b9638db3c0bf95959fd5ad36aea2c8b1db0 (plain) (tree)
































































































































                                                                                                                                                                                    
# 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