597 lines
28 KiB
Text
597 lines
28 KiB
Text
~ ~~~~~~~~~~~~~~~~~~~~~
|
|
~ ~~ Execution model ~~
|
|
~ ~~~~~~~~~~~~~~~~~~~~~
|
|
~
|
|
~ We use Forth-style dual stacks, one for values and one for control. We
|
|
~ use rsp for values, just like C does. We use rbp for the control stack,
|
|
~ which is a special Forth-y stack: These are pointers into the bodies of
|
|
~ Forth words, not return addresses.
|
|
~
|
|
~ The choice of rsp and rbp for the stack pointers imitates Jonesforth;
|
|
~ I'm hopeful that it gives us convenient addressing modes, and will report
|
|
~ back about that when I feel that I understand the implications.
|
|
~
|
|
~ In Forth, everything is a "word", including mutable variables.
|
|
~ Conceptually, a word is a unit of execution, which may be implemented
|
|
~ either in machine code or as an array of pointer to other words.
|
|
~
|
|
~ This polymorphism is implemented by having each word's contents begin
|
|
~ with a "codeword", which is a pointer to machine code that "interprets"
|
|
~ the rest of the contents. In the case of words implemented in machine
|
|
~ code, the codeword points directly to that code, which is normally right
|
|
~ next to it.
|
|
~
|
|
~ Variables, to Forth, are simply one more thing that can be executed; the
|
|
~ effect of executing a variable is to push its address onto the value
|
|
~ stack.
|
|
~
|
|
~ We adopt this model of words, codewords, and variables-as-words. It's
|
|
~ really nice how it doesn't force anything else on us, not even a heap,
|
|
~ though we do end up using the log as a sort-of heap.
|
|
~
|
|
~ We specifically implement a version of calling and returning that Forth
|
|
~ calls indirect threaded code: The control stack is a stack of pointers
|
|
~ into the middle of interpreted words. The interpreter snippet, called
|
|
~ docol, implements calling. Each word is responsible for making sure
|
|
~ returning works properly. Interpreted words accomplish this by ending with
|
|
~ the word "exit", while machine-code words accomplish it by ending with a
|
|
~ verbatim snippet called "next".
|
|
~
|
|
~ Conceptually, "next" returns, but more specifically it accomplishes this
|
|
~ by doing the caller's next dispatch for it; thus control never actually
|
|
~ goes back to the caller's interpreter after initial setup. For performance
|
|
~ reasons, "next" is always inlined, so we define it as a macro.
|
|
~
|
|
~ The docol routine is just ordinary code, not a macro. It's defined later
|
|
~ in this file, as a label.
|
|
~
|
|
~ Notionally, we could consider not having a dictionary, and not giving
|
|
~ our words names. However, it feels silly to stop when we're so close to
|
|
~ being a full Forth, and using names for things solves a bootstrapping
|
|
~ problem related to log management - see the write-up of cold-start about
|
|
~ how the log is created, below. So, we add an additional header before the
|
|
~ codeword for this purpose.
|
|
~
|
|
~ The Forth dictionary is usually a linked list of every word that has
|
|
~ ever been defined, with the newest at the head; the names of words are
|
|
~ stored in string fields, often right next to the link pointer. We adopt
|
|
~ this model, with the field sizes and order shown in the quick reference
|
|
~ below. We break with Forth tradition in one way: Rather than having a
|
|
~ length field, we use a null-terminated string. Thus, there's no length
|
|
~ limit on names. This necessitates breaking out the flags (to be explained
|
|
~ later) into their own byte, rather than taking bits from the length field
|
|
~ for them.
|
|
~
|
|
~ There's an important performance consideration: Executable words
|
|
~ reference each other by pointers to their respective codewords. However,
|
|
~ dictionary entries reference each other by pointers to their respective
|
|
~ link fields. Traversing from the link field to the codeword is easy,
|
|
~ though it's a non-constant-time operation: Just walk the string. In order
|
|
~ to make Forth words easy to "decompile", it would be nice to also have a
|
|
~ way to traverse backwards. We solve this by making the name field be
|
|
~ null-terminated at both ends. Fun, yeah?
|
|
~
|
|
~
|
|
~
|
|
~
|
|
~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
~ Quick Reference
|
|
~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
~
|
|
~ The layout of an interpreted word:
|
|
~
|
|
~ (overall start)
|
|
~ 0x00 - 0x07 Link (to next-oldest word)
|
|
~ 0x08 - 0x08 HM00000I Flags
|
|
~ H - hidden
|
|
~ M - metadata
|
|
~ I - immediate
|
|
~ all other bits reserved
|
|
~ (name start)
|
|
~ 0x09 - 0x09 Null byte (terminates name)
|
|
~ 0x0a - name-end - 1 Name, as UTF-8
|
|
~ name-end - name-end Null byte (terminates name)
|
|
~ (padding start)
|
|
~ name-end + 1 - codeword-start - 1 Zero-pad to 8-byte boundary
|
|
~ (it's possible this will be zero bytes long)
|
|
~ (codeword start)
|
|
~ ... + 0x00 - ... + 0x08 Codeword (ie. address of docol)
|
|
~ (8-byte chunks) Addresses of other words
|
|
~ - ... (end) Address of "exit" word
|
|
~
|
|
~ The layout of a machine-code word is different only from the codeword on:
|
|
~
|
|
~ ... + 0x00 - ... + 0x08 Addresss of next byte
|
|
~ ... + 0x08 - ???? Arbitrary machine code
|
|
~ - ... (end) Inlined implementation of next
|
|
~
|
|
~ Also, words always start at 8-byte boundaries.
|
|
~
|
|
~
|
|
~ REGISTER usage conventions:
|
|
~
|
|
~ * rsi is the "instruction pointer" for the "interpreter".
|
|
~ That is, it points to some word-pointer inside an array of
|
|
~ word-pointers inside the content of the word they're part of. It always
|
|
~ points to the next word that should be executed, whose execution hasn't
|
|
~ begun yet.
|
|
~
|
|
~ * rbp points to the top of the control stack
|
|
~ These are former values of rsi, to eventually be returned to, from
|
|
~ successively older callers as you look further up the stack. The stack
|
|
~ grows downwards in memory. Since values are kept separately, the only
|
|
~ thing on the control stack is return addresses, one per layer of call.
|
|
~
|
|
~ * rsp points to the top of the value stack
|
|
~ The value stack has no specific format, but it grows downwards in
|
|
~ memory. In particular there's no concept of stack frames, because items
|
|
~ on the stack don't belong to any particular word; the value stack in
|
|
~ Forth is in part a mechanism for passing values between words.
|
|
~
|
|
~ Additionally, immediately after beginning execution of a word:
|
|
~
|
|
~ * rax points to the address of the codeword being executed
|
|
~ The value of rax is purely for the callee's benefit, and does not need
|
|
~ to be preserved.
|
|
~
|
|
~ Other registers are purely discretionary, and are not preserved across
|
|
~ calls.
|
|
~
|
|
~
|
|
~ FLAG usage:
|
|
~
|
|
~ * DF should be 0
|
|
~ We use lodsq extensively and that makes it increment rsi after using it.
|
|
~
|
|
~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
~ The macros next, beforenext, pushcontrol, and popcontrol are implemented
|
|
~ in execution-support.e. It's a good idea to go read about them now.
|
|
~
|
|
~ TODO the following high-level summary should go... somewhere?
|
|
~ When a docol word is invoked, it immediately does pushcontrol (see
|
|
~ execution-support.e) to push rsi onto the control stack, whose top is
|
|
~ indicated by rbp. When an assembly word is invoked, it leaves rsi and rbp
|
|
~ alone; assembly words don't normally call other words, so there's no need
|
|
~ for them to appear on the control stack at all.
|
|
~
|
|
~ When an assembly word returns, it looks for an execution token pointed
|
|
~ to by rsi, increments rsi, and invokes that word as if calling it. This
|
|
~ is the threaded calling approach to Forth execution. When a docol word
|
|
~ returns, it calls exit, which needs to remove itself from the control
|
|
~ stack, so exit pops the top of the control stack into rsi to get back into
|
|
~ its caller's context, then does the same steps an assembly word would do.
|
|
|
|
|
|
~ Constants
|
|
~ ~~~~~~~~~
|
|
~
|
|
~ These are used in cold-start, just below.
|
|
|
|
: log-requested-address 0x0000001000000000 ; ~ (very arbitrary)
|
|
: log-size 0x0000000001000000 ; ~ 16 MiB
|
|
: control-stack-size 0x10000 ; ~ 64 KiB
|
|
|
|
|
|
~ Routine cold-start
|
|
~ ~~~~~~~~~~~~~~~~~~
|
|
~
|
|
~ This is the entry point of the whole program, the very first code we
|
|
~ actually execute. Linkers traditionally call this _start, but let's live a
|
|
~ little. Anyway, the ELF header points to it and exec() jumps to it; from the
|
|
~ kernel's perspective, there's no such thing as names. Also, though this code
|
|
~ could be anywhere in the code part of the output, in order to make the
|
|
~ hexdump pretty we put it at the start.
|
|
~
|
|
~ The kernel gives us most registers zeroed, and rsp pointing to the
|
|
~ command-line stuff (argc, argv, envp), which is at an ASLR'd address with
|
|
~ some stack space allocated for us, despite the fact we didn't request any.
|
|
~ It also gives us all the flags clear except IF, but we don't rely on that.
|
|
~ Lastly, of course, it loads our code segment and sets the instruction
|
|
~ pointer where we asked; we don't need to check what those addresses are,
|
|
~ because they're not randomized.
|
|
~
|
|
~ Notably, though cold-start points rbp to the control stack, it leaves
|
|
~ the control stack empty, so trying to return will pop a value that doesn't
|
|
~ exist, and crash. Eventually, the bottom value on the control stack will
|
|
~ be the word "quit", which loops forever and is the closest thing Forth has
|
|
~ to a "main" word, but it's up to warm-start to make that happen.
|
|
~
|
|
~ Cold-start is really only responsible for one-time initialization.
|
|
~
|
|
~
|
|
~
|
|
~ Registers in:
|
|
~
|
|
~ * rsp points to the logical top of the value stack
|
|
~ The kernel sets this up for us, and we need to save it somewhere so
|
|
~ Forth can use it.
|
|
~
|
|
~ Registers out:
|
|
~
|
|
~ * rsi points within "warm-start"
|
|
~ * rsp points to the top of the value stack
|
|
~ * rbp points to the top of the control stack
|
|
~
|
|
~ Stack in:
|
|
~
|
|
~ * argc, argv, envp in the usual Unix way
|
|
~ We ignore them, though.
|
|
~
|
|
~ Stack out:
|
|
~
|
|
~ * The value of "log", as a pointer
|
|
~ The meaning of this will be explained below.
|
|
~
|
|
~ Registers within:
|
|
~
|
|
~ * rdi points to the base the log was allocated at, once it exists
|
|
~ This is the same value that "log" will hold, once we reach a point
|
|
~ where we have variables. Of course, variables are stored on the log,
|
|
~ hence this temporary measure.
|
|
~
|
|
~ We also take this opportunity to define soeme memory layout parameters
|
|
~ that this routine will be responsible for doing something with:
|
|
~
|
|
~ (output memory start, current output point
|
|
~ -- output memory start, current output point)
|
|
: output-cold-start
|
|
~ : blank-line
|
|
~ : blank-line
|
|
~ : ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
|
|
~ : ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
|
|
~ : ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
|
|
~ : ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
~ : blank-line
|
|
~ : This is the start of the cold-start routine, which is the first code
|
|
~ : of any kind the executable runs. It has the responsibility of creating
|
|
~ : the Forth execution environment, then invoking the warm-start routine,
|
|
~ : which is defined below.
|
|
~ : blank-line
|
|
~ : indent
|
|
|
|
current-offset L' cold-start set-label
|
|
cld ~ clear the DF flag
|
|
|
|
~ Prepare the log.
|
|
~
|
|
~ We could ask for a data segment in the program header, but where's the
|
|
~ fun in that? Instead, we call mmap().
|
|
~
|
|
~ If we wanted the kernel to do ASLR for us, passing address zero would
|
|
~ cause it to pick somewhere at random, but instead we choose our own
|
|
~ location. It's still not guaranteed to be where we ask for, so we still
|
|
~ do the work to record where it wound up. We could pass the "fixed" flag
|
|
~ and the kernel would trust us, but this gives us more options for
|
|
~ interoperating with other runtimes.
|
|
~
|
|
SYS_mmap :rax mov-reg64-imm64 ~ : mmap()
|
|
log-requested-address :rdi mov-reg64-imm64
|
|
~ : 0 address (very arbitrary)
|
|
log-size :rsi mov-reg64-imm64 ~ : size (one meg)
|
|
PROT_READ PROT_WRITE | PROT_EXEC | :rdx mov-reg64-imm64
|
|
~ : 0 protection (read+write+exec)
|
|
MAP_PRIVATE MAP_ANONYMOUS | :r10 mov-extrareg64-imm64
|
|
~ : 0 flags (private+anonymous)
|
|
-1 :r8 mov-extrareg64-imm64 ~ : file descriptor (must be -1 on
|
|
~ : FreeBSD for anonymous mappings,
|
|
~ : ignored on Linux)
|
|
0 :r9 mov-extrareg64-imm64 ~ : offset (must be 0 on FreeBSD for
|
|
~ : anonymous mappings, ignored on
|
|
~ : Linux)
|
|
syscall
|
|
|
|
~ The return value of the system call is in rax, we'll use it in a sec.
|
|
~ We need to save this somewhere in case we ever want to munmap() it;
|
|
~ there's no widely-used name for it so we have to make one up. S0 and r0
|
|
~ are widely-used names for the physical tops (logical bottoms) of the
|
|
~ value and control stacks, respectively, and we will eventually set those
|
|
~ up as well, so we should keep those names in mind. The control stack
|
|
~ lives within the log, while the value stack is its own segment. This
|
|
~ value, though, is the physical bottom of the segment, meaning that it
|
|
~ stays the same even as we allocate and deallocate things within it. This
|
|
~ is unlike the two stack pointers, so we give it a name that doesn't
|
|
~ suggest similarity: "log".
|
|
~
|
|
~ Once Forth is fully set up, its internal variables will be accessed
|
|
~ through variable-words like any other Forth data, including "log". To
|
|
~ get to that point, though, we need to be able to hold onto variable data
|
|
~ between now and then. In fact, if we don't have at least one of "log"
|
|
~ and "here" (its counterpart which points to the logical top end), all
|
|
~ our efforts to hold onto anything seem a bit doomed.
|
|
~
|
|
~ So, we temporarily dedicate rdi to "log" - only within this routine -
|
|
~ and store everything else in ways that let us find things by reference
|
|
~ to it. We choose rdi because it works with the indexing modes we care
|
|
~ about, and its name suggests its function.
|
|
~
|
|
~ The strategy Jonesforth uses is not applicable to us; Jonesforth
|
|
~ takes advantage of the linker to let its code segment refer to specific,
|
|
~ pre-allocated objects in the data segment. We are our own linker, and we
|
|
~ don't care to have a data segment. Hence, this approach.
|
|
~
|
|
~ Keying things off "log" is the fundamental decision, but to make sure
|
|
~ our variables are accessible both during early bootstrapping, and later,
|
|
~ we also have to be thoughtful about data structures. More on that in a
|
|
~ moment.
|
|
~
|
|
:rax :rdi mov-reg64-reg64
|
|
|
|
~ We also initialize rbp. We could hold off and let "quit" do this, but
|
|
~ doing it now is the easiest way to initialize the r0 variable, since
|
|
~ there's no instruction that moves a 64-bit immediate to memory.
|
|
~
|
|
~ This is the moment at which we decide where the control stack starts!
|
|
~ Fun, right? "Allocation" is just a fancy word for picking where we want
|
|
~ something, then being consistent about it - like placing furniture in
|
|
~ your home. See below for a little more thought about why here in
|
|
~ particular.
|
|
~
|
|
:rdi control-stack-size :rbp lea-reg64-disp32-reg64
|
|
|
|
~ Now we save some stuff onto the log. These are the locations that
|
|
~ will eventually be the backing stores of the Forth variables, but we
|
|
~ don't create the word headers yet, since there's no requirement that
|
|
~ they be next to the backing stores. We'll do that later, once we have
|
|
~ word-writing infrastructure in place. For now, we just use their offsets
|
|
~ relative to the physical bottom of the log, which are fixed.
|
|
~
|
|
~ These will be the permanent homes of these values, though we have
|
|
~ copies of them elsewhere while we're still in this routine.
|
|
~
|
|
:rdi control-stack-size 0x00 + :rdi mov-reg64-disp32-reg64 ~ log
|
|
:rsp control-stack-size 0x08 + :rdi mov-reg64-disp32-reg64 ~ s0
|
|
:rbp control-stack-size 0x10 + :rdi mov-reg64-disp32-reg64 ~ r0
|
|
L@' final-word-name :rax mov-reg64-imm64
|
|
:rax control-stack-size 0x18 + :rdi mov-reg64-disp32-reg64 ~ latest
|
|
:rdi control-stack-size 0x28 + :rax lea-reg64-disp32-reg64
|
|
:rax control-stack-size 0x20 + :rdi mov-reg64-disp32-reg64 ~ here
|
|
~
|
|
~ * "log" is the physical bottom of the log
|
|
~ The log grows upwards in memory, so this is also the logical
|
|
~ bottom. This comes from the address mmap() just returned to us.
|
|
~ * "s0" is the logical bottom of the value stack
|
|
~ The value stack grows downwards in memory, so this is the physical
|
|
~ top of it. This comes from the stack pointer the kernel initialized us
|
|
~ with.
|
|
~ * "r0" is the logical bottom of the control stack
|
|
~ The control stack also grows downwards, so this is its pysical top
|
|
~ as well. We allocate this dedicated space within the log right here,
|
|
~ in this routine, through our choice of where to put things.
|
|
~ * "here" is the physical start of the unallocated space in the log
|
|
~ We allocate log space from bottom to top, by incrementing this
|
|
~ value. So, it would also be accurate to say that it points immediately
|
|
~ after the physical top of the allocated space. At any rate, the
|
|
~ address it points to is the first one that hasn't been used yet.
|
|
~ * "latest" is the address of the most-recently-defined word's header
|
|
~ Defining new words changes this value.
|
|
~
|
|
~ s0 and r0 are mostly used when we want to initialize or reinitialize
|
|
~ their respective stacks - that is, discard all their contents at once.
|
|
~
|
|
~ The value of r0 is the same address these variables start at, so
|
|
~ you'll want to do a close read of the implementation of pushcontrol
|
|
~ and convince yourself that it only ever writes things just below the rbp
|
|
~ address it receives, never right on top of it.
|
|
~
|
|
~ As an aside, by the way, please notice that strictly speaking, r0
|
|
~ could be a constant... but it isn't known until runtime, so we might as
|
|
~ well make it a variable. That will play nicely with any astonishing
|
|
~ memory shenanigans someone might wish to do in the future.
|
|
~
|
|
~ Notice also that "here" points immediately after itself. This is just
|
|
~ a convenience, making it the last one like that so that the concern is
|
|
~ dealt with in a single place and is easy to keep up-to-date with code
|
|
~ changes.
|
|
~
|
|
~ A little more detail about why we offset everything by
|
|
~ control_stack_size: We're carving out some space at the bottom of the
|
|
~ log - which grows low-to-high - to be the control stack - which grows
|
|
~ high-to-low. So the control stack is allocated out of the log as a
|
|
~ fixed-size, one-time thing, and then the variables come immediately
|
|
~ after that. We do need to use 32-bit displacement indexing to access
|
|
~ them this way, but that's no big deal.
|
|
~
|
|
~ This is perhaps questionable, they should maybe be separate segments
|
|
~ created with separate calls to mmap(), but for now we're not worried
|
|
~ about overflow so we use the same allocation for both.
|
|
~
|
|
~ We'll come back to these variables a bit later and generate the word
|
|
~ headers that point at them, but now we're almost ready to switch to
|
|
~ proper threaded-execution, so we finish that setup first...
|
|
|
|
~ Push the value of "log" onto the value stack so that it can be the
|
|
~ breadcrumb the threaded code needs to find... the backing store of
|
|
~ "log". Yes, self-reference can be weird like that sometimes. There's
|
|
~ nothing stopping "quit" from reading rdi, it just violates the
|
|
~ abstraction...
|
|
:rdi push-reg64
|
|
|
|
~ We are about to set up rsi, we did rbp already, and rsp came to us
|
|
~ already set up. That's all that "next" needs, so take it away!
|
|
L@' warm-start L@' origin + :rsi mov-reg64-imm64
|
|
pack-next
|
|
~ : deindent
|
|
;
|
|
|
|
|
|
~ Routine warm-start
|
|
~ ~~~~~~~~~~~~~~~~~~
|
|
~
|
|
~ This routine runs as Forth code. That is, indirect threaded execution has
|
|
~ been initialized, and the structure of this routine is an array of codeword
|
|
~ pointers. It was directly jumped to from cold-start. There's nowhere to
|
|
~ return to, so it needs to never return.
|
|
~
|
|
~ (output buffer start, output point, input string pointer
|
|
~ -- output buffer start, output point)
|
|
: output-warm-start
|
|
~ : blank-line
|
|
~ : blank-line
|
|
~ : ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
|
|
~ : ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
|
|
~ : ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
|
|
~ : ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
~ : blank-line
|
|
~ : This is the start of the warm-start routine, which is the first Forth
|
|
~ : code the executable runs, after the cold-start routine has created the
|
|
~ : Forth execution environment.
|
|
~ :
|
|
~ : It does some initial setup here at the top; then the vast bulk of it
|
|
~ : is actually the log-load routine, directly included; then finally there
|
|
~ : is a small amount of finishing work done at the tail end of the
|
|
~ : warm-start routine.
|
|
~ :
|
|
~ : The routine's body is interpreted Forth code, meaning that it is an
|
|
~ : an array of pointers to the codewords of Forth words, occasionally
|
|
~ : interspersed with literal values.
|
|
~ :
|
|
~ : The codeword pointers were located by using the label system, which
|
|
~ : means the labels they reference must have been defined somewhere else,
|
|
~ : normally by the label transform. The label-transformed code normally
|
|
~ : occurs after the warm-start routine in the executable, which means these
|
|
~ : are forward references to words defined far below.
|
|
~ : blank-line
|
|
~ : indent
|
|
|
|
~ While it's not actually a requirement that codeword pointers be
|
|
~ word-aligned, it's highly likely that it helps performance. (Whether it
|
|
~ does is up to Intel's microcode.)
|
|
3unroll
|
|
8 packalign
|
|
current-offset L!' warm-start
|
|
~ (input string pointer, output buffer start, output point)
|
|
|
|
~ Before handing off to us, cold-start pushed a single value onto the
|
|
~ stack, a pointer to the beginning of the log. Now, we load our entire
|
|
~ Forth implementation onto that log, beginning with the minimal set of
|
|
~ words needed to define more words. We do this because we need variables as
|
|
~ infrastructure so we can eventually have dynamic definitions.
|
|
~
|
|
~ There's something non-obvious here: words implemented statically as
|
|
~ part of the executable image can't contain things that vary at runtime.
|
|
~ That means that even if these words tried to implement some sort of
|
|
~ dynamic lookup, they would have no way to find the root of whatever
|
|
~ dynamic data structure they use. Dynamism needs to be bootstrapped.
|
|
~
|
|
~ In a more traditional C-style program, static code could look up
|
|
~ variables based on fixed addresses that are the same on every run.
|
|
~ Failing that, we could dedicate a register to it, though that's a
|
|
~ considerable expense. We chose not to do either of those things, because
|
|
~ we want the versatility that comes with not being picky about our
|
|
~ address space: It allows us to contemplate future improvements such as
|
|
~ ASLR, or embedding into other processes that impose their own addressing
|
|
~ constraints, or even coexisting with multiple versions of ourselves.
|
|
~ That choice does mean we have the hard version of this bootstrapping
|
|
~ problem, and copying ourselves to the log is how we solve it.
|
|
~
|
|
~ We do have the log address right now, though that won't last. In case
|
|
~ it's unclear why not: keeping it on the stack would require all future
|
|
~ references to walk the stack, and somehow know when they've reached the
|
|
~ bottom. The stack is a good place to keep things with clearly delimited
|
|
~ lifetimes and visibility, but when we want something to live for our
|
|
~ entire program and be easy to find from any code within it, we need to
|
|
~ do something else. Anyway, since we have the address, we can use it for
|
|
~ the next little bit of setup. We have a bunch of helper words, from
|
|
~ log-load.e, which make this easier.
|
|
~
|
|
~ The first few words we define are our variables, which hardcode the
|
|
~ addresses they will return - but since we're doing this at runtime,
|
|
~ "hardcoding" can reflect where our log is. This is the fundamental
|
|
~ trick that makes the log usable.
|
|
~
|
|
~ One more thing to notice: We already allocated the backing stores of
|
|
~ these variables, and populated their initial values, in cold-start. The
|
|
~ words we're defining return those same addresses for the same backing
|
|
~ stores. So, we have continuity: Stuff defined in terms of the
|
|
~ variable-words we're defining now will interoperate with the stuff that
|
|
~ we define using the log-load helpers, which includes those very words.
|
|
~ Both the log-load code and the later code are dealing with the same data
|
|
~ structures, they're just using a different technique to find them.
|
|
~
|
|
~ This is the only hardcoding we need to do; by building on top of it,
|
|
~ we will soon reach a point where the rest of the system can be defined
|
|
~ within itself.
|
|
|
|
L@' log-load-log offset-to-target-address-space pack64
|
|
~ : -8 log-load-log #-- (codeword pointer)
|
|
L@' litstring offset-to-target-address-space pack64
|
|
~ : -8 litstring #-- (codeword pointer)
|
|
s" log" packstring 8 packalign
|
|
L@' log-load-variable offset-to-target-address-space pack64
|
|
~ : -8 log-load-variable #-- (codeword pointer)
|
|
|
|
L@' log-load-s0 offset-to-target-address-space pack64
|
|
~ : -8 log-load-s0 #-- (codeword pointer)
|
|
L@' litstring offset-to-target-address-space pack64
|
|
~ : -8 litstring #-- (codeword pointer)
|
|
s" s0" packstring 8 packalign
|
|
L@' log-load-variable offset-to-target-address-space pack64
|
|
~ : -8 log-load-variable #-- (codeword pointer)
|
|
|
|
L@' log-load-r0 offset-to-target-address-space pack64
|
|
~ : -8 log-load-r0 #-- (codeword pointer)
|
|
L@' litstring offset-to-target-address-space pack64
|
|
~ : -8 litstring #-- (codeword pointer)
|
|
s" r0" packstring 8 packalign
|
|
L@' log-load-variable offset-to-target-address-space pack64
|
|
~ : -8 log-load-variable #-- (codeword pointer)
|
|
|
|
L@' log-load-latest offset-to-target-address-space pack64
|
|
~ : -8 log-load-latest #-- (codeword pointer)
|
|
L@' litstring offset-to-target-address-space pack64
|
|
~ : -8 litstring #-- (codeword pointer)
|
|
s" latest" packstring 8 packalign
|
|
L@' log-load-variable offset-to-target-address-space pack64
|
|
~ : -8 log-load-variable #-- (codeword pointer)
|
|
|
|
L@' log-load-here offset-to-target-address-space pack64
|
|
~ : -8 log-load-here #-- (codeword pointer)
|
|
L@' litstring offset-to-target-address-space pack64
|
|
~ : -8 litstring #-- (codeword pointer)
|
|
s" here" packstring 8 packalign
|
|
L@' log-load-variable offset-to-target-address-space pack64
|
|
~ : -8 log-load-variable #-- (codeword pointer)
|
|
|
|
~ Having done that, nothing else needs to be defined in an unusual way, so
|
|
~ we can go ahead and dispatch to the log-load transform, and do the rest of
|
|
~ the code through that.
|
|
|
|
~ (input string pointer, output buffer start, output point)
|
|
3roll log-load-transform
|
|
~ (output buffer start, output point)
|
|
|
|
~ : blank-line
|
|
~ : We're now done with the log-load routine; the next little bit of code
|
|
~ : here is the final bit of the warm-start routine.
|
|
|
|
~ Now everything we need has been added to the log and we're almost ready
|
|
~ to jump into it. It's inconvenient for code under the log-load transform
|
|
~ to interact with the label system, so if we want to point
|
|
~ main-input-buffer at a string, this is the place to do it.
|
|
|
|
~ The next layer is built now, so let's move on to it.
|
|
L@' litstring offset-to-target-address-space pack64
|
|
~ : -8 litstring #-- (codeword pointer)
|
|
s" quit" packstring 8 packalign
|
|
L@' log-load-find-execution-token offset-to-target-address-space pack64
|
|
~ : -8 log-load-find-execution-token #-- (codeword pointer)
|
|
L@' swap offset-to-target-address-space pack64
|
|
~ : -8 swap #-- (codeword pointer)
|
|
~ : Get rid of that heap pointer on the stack, we're finally done with it!
|
|
L@' drop offset-to-target-address-space pack64
|
|
~ : -8 drop #-- (codeword pointer)
|
|
L@' execute offset-to-target-address-space pack64
|
|
~ : -8 execute #-- (codeword pointer)
|
|
~ : deindent
|
|
;
|
|
|
|
|
|
~ Where next?
|
|
~ ~~~~~~~~~~~
|
|
~
|
|
~ The definitions of "docol" and "exit" are very tightly bound up with the
|
|
~ execution model. They're defined and explained in core.e, because they need
|
|
~ to be part of the build process in two different ways, like the other core
|
|
~ functionality.
|
|
|