# 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