summary refs log tree commit diff
path: root/linux-dynamic.e
diff options
context:
space:
mode:
Diffstat (limited to 'linux-dynamic.e')
-rw-r--r--linux-dynamic.e356
1 files changed, 356 insertions, 0 deletions
diff --git a/linux-dynamic.e b/linux-dynamic.e
new file mode 100644
index 0000000..4c73bb7
--- /dev/null
+++ b/linux-dynamic.e
@@ -0,0 +1,356 @@
+~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~ ~~ More system calls for Linux ~~
+~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~
+~   Everything that takes a struct is here, because that lets us define the
+~ system call next to the allocation stuff, for ease of reference. There are
+~ also higher-level facilities, later in the file.
+
+
+~   Since the only way to know the struct layout is by reading the kernel
+~ source, we put that definition here, as well.
+~ (-- struct pointer)
+: allocate-timespec 2 8 * allocate ;
+~ (struct pointer -- field pointer)
+: timespec-seconds ;
+: timespec-nanoseconds 8 + ;
+
+
+~ (nanoseconds -- result code)
+: nanosleep
+  1000000000 /%
+  ~ (nanoseconds, seconds)
+  allocate-timespec
+  dup timespec-seconds 3roll swap !
+  dup timespec-nanoseconds 3roll swap !
+  allocate-timespec
+  dup timespec-seconds 0 swap !
+  dup timespec-nanoseconds 0 swap !
+  dup 3unroll
+  35                             ~ syscall number
+  syscall-2
+  drop drop ;
+
+
+~   The old name was sigaction(), but per the glibc manpages, that was
+~ replaced by rt_sigaction() quite some time ago, to allow larger signal set
+~ bitmaps for the benefit of realtime signals. Both are still present; we use
+~ rt_sigaction(). There are also two versions of rt_sigaction; grep
+~ ODD_RT_SIGACTION in the kernel source for more details. Neither x86
+~ architecture has the "odd" version of rt_sigaction(), so we use the regular
+~ one, not to be confused with the "compat" one. Its definition is in
+~ kernel/signal.c, in case you need to check the struct sizes and layouts.
+~
+~   The parameters are: An integer signal identifier; an optional pointer to a
+~ struct describing the new action to bind (or unbind if NULL); an optional
+~ pointer to another of the same struct to hold a copy of the old action; and
+~ a 64-bit word which must describe the correct size of sigset_t, which is a
+~ C type used for the mask field of that struct. Any other value for the size
+~ field is an error.
+~
+~   Mandatory size fields start to make sense when you have a compatibility
+~ situation this convoluted... Think of it as the caller promising they know
+~ which version of the API they're calling. The unmarked version of
+~ rt_sigaction() takes a 64-bit sigset_t, and the parameter wants the size in
+~ bytes, so we use a value of 8.
+~
+~   Anyway, we aren't C and don't have POSIX naming obligations, so we just
+~ call it sigaction.
+~
+~ (signal number, new action pointer, old action pointer -- result code)
+: sys-sigaction
+  8                              ~ size of sigset_t in bytes
+  13                             ~ syscall number
+  syscall-4 ;
+
+
+~ (new stack pointer, old stack pointer -- result code)
+: sys-sigaltstack
+  131                            ~ syscall number
+  syscall-2 ;
+
+
+~   Since the only way to know the struct layout is by reading the kernel
+~ source, we put that definition here, as well.
+~
+~   There are MANY versions of this struct; this is the appropriate one for
+~ amd64.
+~
+~ (-- struct pointer)
+: allocate-sigaction here @ 128 packalign here ! 4 8 * allocate ;
+~ (struct pointer -- field pointer)
+: sigaction-action ;
+: sigaction-flags 8 + ;
+: sigaction-restorer 2 8 * + ;
+: sigaction-mask 3 8 * + ;
+
+
+: allocate-sigaltstack here @ 128 packalign here !  3 8 * allocate ;
+: sigaltstack-pointer ;
+: sigaltstack-flags 8 + ;
+: sigaltstack-size 2 8 * + ;
+
+
+~ High-level facilities
+~ ~~~~~~~~~~~~~~~~~~~~~
+
+~   It's possible to set up an alternate stack for signal handlers. We don't,
+~ though, so it's possible this code has bitrotted. At the very least, it
+~ should be more configurable than this.
+~
+~   Note that for it to actually be used, there also needs to be a flag set
+~ at the time the action is bound.
+: prepare-signal-stack
+  here @ 2048 packalign here !
+  1024 1024 * 4 * allocate
+  ~ (stack address)
+  allocate-sigaltstack
+  ~ (stack address, struct pointer)
+  2dup sigaltstack-pointer !
+  dup sigaltstack-flags 0 swap !
+  dup sigaltstack-size 1024 1024 * 32 * swap !
+  ~ (stack address, struct pointer)
+  allocate-sigaltstack
+  sys-sigaltstack
+  drop ;
+
+
+~   On amd64, and on no other architecture, the Linux kernel requires that
+~ the language runtime use a "restorer" when binding signal handlers. This
+~ is that restorer. The kernel wants a raw pointer that it can use as a
+~ C-style return address on the C stack, which is our value stack. So, this
+~ has to be an assembly word, it can't use docol. When the data structure is
+~ populated by bind-signal, we'll dereference the codeword and pass that
+~ value, but there's no chance to give it the usual callee address in rax, so
+~ we can't use a Forth-style interpreter codeword, it has to be a
+~ self-codeword.
+~
+~   The purpose of the trampoline is to be invoked by our signal handler. The
+~ manner of that invocation MUST be returning into it with the "ret"
+~ instruction; otherwise the C stack won't be right for cleanup to work
+~ properly.
+~
+~   When invoked, the trampoline invokes the syscall "sigreturn", whose sole
+~ purpose is to be called from this trampoline. That syscall won't return in
+~ a conventional way, so we don't bother handling the scenario where it does.
+~
+~   According to commentary in the Go compiler's internals[1][2][3], gdb
+~ recognizes the trampoline based on its exact byte values, since the intent
+~ is only to be compatible with glibc. We intend to be our own debugger
+~ anyway, so we don't worry about that. We're not seeking fame and we don't
+~ have a corporate image to uphold, so that level of fragility and contortion
+~ is just too much. The thing about compatibility constraints is knowing when
+~ to work with them and when to walk away.
+~
+~   Experimentally, it is also possible to avoid using this trampoline by
+~ faking it: set the "restorer" bit in the action flags, but pass a null
+~ pointer as the restorer, then have the handler pop 8 bytes from the stack
+~ and invoke sigreturn directly. The actual requirement seems to be that rsp
+~ points at the saved state, at the time of invoking sigreturn. We're not
+~ doing that, because signal handling is not intended to be
+~ performance-critical and it feels like asking for trouble, but the
+~ possibility is noted here against future use.
+~
+~   This doesn't have an interface definition comment, because it doesn't use
+~ the Forth execution model.
+~
+~ [1] https://go.googlesource.com/go/+/refs/heads/master/src/runtime/sys_linux_amd64.s#472
+~ [2] https://go.googlesource.com/go/+/refs/heads/master/src/runtime/os_linux.go#476
+~ [3] https://go.googlesource.com/go/+/refs/heads/master/src/runtime/defs_linux_amd64.go#118
+: signal-return-trampoline
+  [ here @
+    15 :rax mov-reg64-imm64        ~ sigreturn
+    syscall
+    here ! ] ;asm
+
+~   This accepts an execution token. It creates a hidden word on the log which
+~ wraps that execution token with necessary setup and teardown to run as a
+~ Unix signal handler, and returns the execution token of the wrapper.
+~
+~   Specifically, on invocation, the wrapper ensures that rsi points to its
+~ second half and rbp points to the top of the control stack; loads the target
+~ execution token into rax; then indirectly calls it. This is the usual
+~ interface of a normal call in the Forth execution model, so the wrapped word
+~ can be based on docol, on a self-codeword, or on any other interpreter word
+~ it wants. It can also freely call whatever Forth things it wants.
+~
+~   We don't have to do anything about rsp because the invariants for our use
+~ of it as the value stack are a subset of the invariants for C's use of it as
+~ its only stack. It's already working the way we need it to.
+~
+~   When the wrapped word returns, it uses the rsi the wrapper provided to do
+~ so, which places control in the second half of the wrapper. This second half
+~ simply executes a "ret" instruction, which is the necessary invocation of
+~ the signal return trampoline (see above). This will transfer control back
+~ to the kernel, and will ultimately result in Forth execution resuming where
+~ it left off before the signal was delivered.
+~
+~   Crucially, the wrapper relies on the kernel preserving the value of rbp
+~ that existed at the moment before the control transfer began. Signal
+~ delivery is an UNCONTROLLED control transfer, meaning that we as the
+~ language runtime do not have an opportunity to execute any cleanup before it
+~ happens. If it were a controlled transfer, we would be able to save rbp to
+~ a global variable somewhere, and restore it in the wrapper. It's not, so we
+~ don't have that chance.
+~
+~   Notionally we could freshly allocate a new control stack somewhere else,
+~ and set rbp to point to it, but it would be challenging to do that without
+~ relying on the control stack, and inefficient to execute, and the call for
+~ now is that that's not worth it.
+~
+~   The only situation in which this limitation will become a practical
+~ concern is if, at the time of signal delivery, something outside the Forth
+~ execution model is happening. In that case, the wrapper will likely crash.
+~
+~   As a long term strategy, the way to mitigate this would be to make sure
+~ that all non-signal transfers from within the Forth execution model to
+~ outside it are controlled, and that they save global state that can be
+~ reconstructed here. For now, we leave this as future work.
+~
+~ (execution token -- execution token)
+: wrap-signal-handler
+  ~   We generate a word entry for the wrapper, and hide it. Since it's
+  ~ hidden, the name doesn't have to be unique. This keeps the log clean, so
+  ~ that all the space on it will always be attributable to some specific
+  ~ word. Remember kids, keeping the log clean is everyone's responsibility!
+  s" signal-handler-wrapper" create make-hidden
+
+  ~ This self-codeword will be consumed by bind-signal.
+  self-codeword
+
+  here @ dup
+  ~ (inner execution token, saved location, output point)
+
+  ~   It's our responsibility as a caller to set rsi to point to the address
+  ~ of an execution token, which will pick up where we left off. That token
+  ~ will be our own second half, whose address we don't yet know, so we
+  ~ output a placeholder opcode here and overwrite it once we do. That's why
+  ~ we've saved the current location.
+  0 :rsi mov-reg64-imm64
+
+  ~   We also need to make sure that rbp points to an area that can be treated
+  ~ as the top of the control stack (there's no need to ever unwind past it,
+  ~ so it doesn't have to be the "real" one). Fortunately, it comes to us
+  ~ already valid and we don't have to do anything about that. Plus it even
+  ~ does happen to be the real one, which will let stack tracing code run in
+  ~ a handler, and we do care about that.
+
+  ~ We also need to set DF = 0, since that's also part of our ABI.
+  cld
+
+  ~   Compare this snippet to "execute" in core.e. Instead of taking rax from
+  ~ the stack, we set a hardcoded value picked at the time we generate the
+  ~ wrapper. We then do the same indirect jump via the codeword it points to,
+  ~ which allows the codeword's implementation to take advantage of rax
+  ~ pointing to the callee; that's the property docol cares about.
+  3roll :rax mov-reg64-imm64
+  :rax jmp-abs-indirect-reg64
+  ~ (saved location, output point)
+
+  8 packalign
+  here !
+  ~ (saved location)
+
+  ~   Now we have our second half, which has another codeword that rsi will
+  ~ point to for our callee's benefit. This half runs after the wrapped word,
+  ~ and has the responsibility of cleaning up and returning control to the
+  ~ kernel, which it does by returning to the restorer trampoline. Yes, this
+  ~ is a trampoline which passes control to another trampoline.
+  ~
+  ~   Although we run under the log-load transform, we won't ever actually be
+  ~ invoked until at least log-load time, if not ultimate runtime. Both of
+  ~ those are in the target address space. So there's no address translation
+  ~ going on behind our back. Nonetheless, we avoid directly outputting any
+  ~ address except what we get via self-codeword, which would be recommended
+  ~ practice under the transforms. Yeah, it's a little convoluted, perhaps
+  ~ unnecessarily so.
+  here @
+  self-codeword
+  @ 8 -
+  ~ (saved location, second half execution token)
+
+  ~   Something subtle here: That above "codeword" was actually a word
+  ~ pointer. See, because we're pretending the wrapper is Forth word, even
+  ~ though we're writing it in assembly, so "returning" to it means invoking
+  ~ the next word pointer in the word pointer array that is its compiled form.
+  ~ Instead of creating a separate memory area though, we just put the pointer
+  ~ target right here, as another codeword...
+  self-codeword
+
+  ~   Now we treat the saved location as an output point, and re-output the
+  ~ mov instruction that we stubbed out above. Because our assembler words
+  ~ always output a specific, exact form of the instruction, we know it will
+  ~ take up the same number of bytes.
+  :rsi mov-reg64-imm64
+  drop
+
+  ~   Having done that, we can get on to the body of our second half. Happily,
+  ~ it's quite short.
+  here @
+
+  ret
+
+  8 packalign
+  here !
+
+  ~   Our caller wants an execution token that invokes all this. Since we
+  ~ used "create" above, that's easy to get.
+  latest @ entry-to-execution-token ;
+
+
+~   This accepts an execution token and a Unix signal number, and binds the
+~ token to be the handler for the signal. It also does other necessary setup,
+~ including picking appropriate flags for the binding and attaching the
+~ return trampoline (see above).
+~
+~   Typically, you will want this execution token to be one returned by
+~ wrap-signal-handler. Doing this will allow the handler to use the Forth
+~ execution model in any way it wants, including calling both docol words and
+~ assembly words, and working with both the control and value stacks at will.
+~
+~   There is an important limitation of wrap-signal-handler, described in more
+~ detail above: Its wrapper only functions correctly when Forth code was
+~ running at the time of the control transfer. For example, if Forth had
+~ called into C, and then that C were interrupted by a signal, the signal
+~ handler would have no way of finding the top of the control stack.
+~
+~   If your program involves many callbacks back-and-forth between C and
+~ Forth, you may wish to forego the use of the wrapper and provide an
+~ execution token meant to be invoked directly by the kernel. In this case,
+~ bear in mind that its execution must not use the control stack - that is,
+~ it must not rely on having been given sensible values of rsi or rbp. This
+~ means it can't call other Forth words (unless it does something about that
+~ on its own).
+~
+~   Regardless of whether the execution token is a copy of the wrapper or not,
+~ it must be an assembly word, not a docol word. The kernel wants a raw
+~ pointer that it can simulate a C-style call to, so we dereference the
+~ codeword and pass that value, just as we do with the return trampoline. Also
+~ as with the return trampoline, there is no way to pass the callee in rax,
+~ which is the usual interface docol and other interpreter words expect. So,
+~ it needs to be a self-codeword.
+~
+~ (execution token, signal number --)
+: bind-signal
+  allocate-sigaction
+  dup sigaction-action 4 roll @ swap !
+  dup sigaction-mask 0 swap !
+  dup sigaction-flags 0x04000000 swap !
+  dup sigaction-restorer
+  ' signal-return-trampoline entry-to-execution-token @ swap !
+  ~ (signal number, struct pointer)
+  allocate-sigaction
+  sys-sigaction drop ;
+
+
+: handle-crash list-callers 1 sys-exit ;
+
+: install-crash-handler
+  ' handle-crash entry-to-execution-token wrap-signal-handler
+  11 bind-signal ;
+
+~   There are scenarios where someone might want to disable this, for example
+~ if calling back and forth between C and Evocation, but for now we always
+~ enable it.
+install-crash-handler
+