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.
This commit is contained in:
parent
417a05ee7d
commit
f3612481b1
|
@ -1,4 +1,11 @@
|
|||
//: Automatically aggregating functions into test suites.
|
||||
//: Automatically aggregate functions starting with 'test-' into a test suite
|
||||
//: called 'run-tests'. Running this function will run all tests.
|
||||
//:
|
||||
//: This is actually SubX's first (trivial) compiler. We generate all the code
|
||||
//: needed for the 'run-tests' function.
|
||||
//:
|
||||
//: By convention, temporary functions needed by tests will start with
|
||||
//: '_test-'.
|
||||
|
||||
//: We don't rely on any transforms running in previous layers, but this layer
|
||||
//: knows about labels and will emit labels for previous layers to transform.
|
||||
|
|
|
@ -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
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue