03/07/2015 at 11:31 •
One of the most important features for the vm is the way that native code can be accessed. After playing around with several ideas and methods, I found it was best just to keep it simple. Two registers control reading and writing to native memory, one controls the address of native memory being affected, the other is a pseudoregister which when written to/read from performs the read/write on the corresponding address in native memory. Use of these registers with the standard set of instructions, allows easy direct manipulation of native memory with little overhead. In addition, there are several instructions which serve to aid in handling native code. One instruction - ASSIGN_NATIVE_INTERRUPT - allows the programmer to assign a vm function as a native interrupt handler without writing any native code (though some native code will be generated by the VM to permit this functionality). Similiarly - UNASSIGN_NATIVE_INTERRUPT - allows the programmer to remove a VM interrupt handler that was previously assigned. Both of these operations come with an error code that indicates whether the interrupt was successfully assigned, or if restrictions by the underlying operating system prevent doing so. Jumping to native code is a task which can get complicated easily, so to keep things simple, the VM provides an instruction, - EXECUTE_NATIVE - which simply takes a native address as an argument and jumps to that address. At this point you are probably thinking "How do you prepare arguments for a native function call? How does a native function access values in the VM, and how does it ultimately return control back to the VM?" All of these issues are difficult to work around with a framework that is supposedly "simple," so to solve this problem, we need a few other features. First of all, there are native functions that are part of the VM specification which permit returning back to the VM, as well as reading/writing VM memory. The native addresses of these functions is given in a pseudoregister that can be accessed at any time in the VM. In addition there is an instruction which allows native memory to be allocated for use by the VM for any combination of reading/writing/executing. Once allocated for execution, the allocated segment can be written with native opcodes that perform the necessary VM accesses to read arguments, initialize a call stack, and any other tasks needed before calling a native library function. Thus for any setup which cannot be done simply by writing values to native memory, native instructions can act as a mapping layer between the VM and the native code. So at this point, you may be wondering "Why not just allocate a small chunk of native memory for a stack, use the native registers to write your arguments to it then jump to some native code?" Well the answer to that question is that the way in which systems implement function calls and stacks can vary widely. Even 2 operating systems such as Linux and Windows running on the same hardware may use different calling conventions when calling native functions, in fact there is not even a guarentee that the native code you want to execute obeys a strict calling convention anyways, especially if it was written in assembly. Often as an optimization, certain arguments will be passed to a function in native registers. For such functions VM code cannot exist for passing arguments as the VM itself may make use of those regisers for other purposes. As another example, the MOS-6502 processor (famously used in the NES and APPLE II) has a stack pointer register as well as several instructions for pushing and pulling registers off the stack, the RTS instruction "returns" from a function by popping the program counter and status register off the stack. In this case it would be impossible for the VM running on a 6502 to directly manipulate the stack since it would have to read the stack pointer, a value which would change after reading since the VM's intrnal operations may very well change the value of the stack pointer before a write occurs. This VM of course is not specifically targeted to a 6502 of course, however the 6502 instruction set is a good example of how limitations imposed by the host system can create the need for handling native function calls in this way. With much of the native issues resolved, the next step is defining a simple but powerful set of atomic operations for implementing semaphores and all that fun stuff, as well as a means of communicating between multiple virtual/physical processor cores.
02/26/2015 at 08:54 •
So far on this project, I have started writing the specification for how this virtual processor should behave in a plain text file. I have the registers, pseudoregisters, and instructions defined with full details about what registers are affected by each operation and how they are affected. I wrote some test programs in C and Java to try out ideas about how certain aspects of the VM should behave. Mostly the things I wrote were simulations for things like how a borrow flag is carried through an arbitrary length subtraction. In this way I was able to confirm the arithmetic definitions for the instructions in the VM. Currently I am working on a few instruction definitions that relate to native code behavior, and defining the constraints on certain native subroutines which will be needed as a return address from native calls to allow the native code to jump back into the vm, and have the VM pick up where it left off.