1 Introduction
2 Related work
3 Background
3.1 Event-B and Rodin
initialisation
serves as a starting point, has no guards, and only runs once to initialize all variables. Event-B does not define which event will be executed in case more than one is enabled simultaneously. The execution of an event modifies the current state according to its actions, which are executed simultaneously. It is possible to limit how often an event may be enabled using a variant, which is either a numeric expression or a finite set whose free identifiers are constants or concrete variables. Events always have a status, which can be either ordinary (default status, the event may occur arbitrarily often), convergent (the event must decrease the variant), or anticipated (the event must not increase the variant). When a machine refines another one, it may introduce details or more variables. The machine events must be refined or kept as they were. In special cases, when the event can never be enabled, it may be deleted from the refined machine. POs define what is to be proved for a model and are automatically generated by Rodin. For example, the POs guarantee that invariants always hold and that refined events do not contradict abstract ones.
c1
(Listing 1b), which is not present in the original model. With this example, we can show Event-B ’s notation and model components, as well as some concepts for modeling sequential programs introduced by Abrial and used in our models. In this model, we only have four events and the state is composed of a few variables. Still, it can be challenging to understand how state transitions occur and which effect events may have on each other. Therefore, it is helpful to visualize the model graphically. Many tools can create visualizations from Event-B models, producing intricate and detailed graphs. For the sake of simplicity, however, we present simple state-machine-like graphs representing our models. As an example, Fig. 1 presents a visualization of the concrete Event-B model of the binary search. The nodes represent the events’ guards, and edges represent the possible state transitions caused by their actions when they execute. The dashed boxes are the guard expressions defining the state that enables the respective event.f
is an array of natural numbers with size n
, and v
is a value in the range of f
(i.e., an element of the array). The program (modeled in the machine, explained later) will search for the index r
in the array, such that f(r)
\(=\) v
. The axiom axm0_4
tells us that the array f is sorted in a non-decreasing way, and the theorem thm0_1
states that there is at least one element in the array. Listing 1b is not part of the model. However, it shows how an Event-B context can extend another to use (and further detail) elements of the extended context, for example, defining the size n
of the array f
. It also defines a carrier set S
and new constants. The axiom setDef
defines S
as a partition of three subsets with one element each, effectively creating an enumerated set of three elements. The machine of the initial model (Listing 1c) sees the Event-B context c0
and declares the variable r
, which represents the index that the program must find. Besides the mandatory initialisation
event, we add two others. Event final
represents the specification of the program, i.e., its post-condition. It has no actions (skip
), and its guards represent the state where the program has found r
. Note that axm0_3
states that v
is in the range of f
, guaranteeing the program will find r
. The other event we add is the anticipated event progress
, which modifies r
in a non-deterministic way. Its refinements will detail how r
is to be found and will eventually be made convergent on a variant, such that the loop it creates is guaranteed to terminate eventually. In the first refinement m1
(Listing 2a), two new variables, p
and q
, are introduced, representing search indexes of the array f
. The abstract event progress
is split into events inc
and dec
, which are made convergent on the variant q
−p
. Since inv1_3
has been introduced, the guard grd0_fin_1
in final
of Listing 1c always holds and can therefore be removed from the refined machine m1
. The second refinement m2
(Listing 2b) defines how r
is to be found within inc
and dec
, defining the increment and decrement intervals that were non-deterministic in m1
. Note that, since event final
remains exactly the same as its abstraction, its guard is omitted and the word refines
is replaced by extends
. The goal state is still f(r)
\(=\) v
.s1
and s2
, where s2
is a subset of s1
. The set s1
is a partition of constants a
to e
(definition omitted for simplicity). A relation r
is a set of pairs where its domain dom(r)
is the set of all elements occurring on the left side of the pairs and its range ran(r)
is the set of elements on the right side. A function is a special case of relation, where each element of the domain is uniquely related to one element of the range. The two relations defined in Listing 3, r1
and r2
, are partial functions from, respectively, s1
and s2
to
. Table 1 shows the values in each relation range. Columns r1
and r2
show the range values defined for these relations, each cell representing the value mapped to the corresponding set element. If an element is not in the relation’s domain, the cell is marked with “-”. If it is, but there is no mapping for it in the relation the cell is left blank. The other columns represent the result of some operations with r1
: The domain subtraction s2
r1
removes all s2
from r1
’s domain, leaving only two mapped values. Domain restriction s2
r1
is the inverse, only leaving values mapped to elements present in s2
. The overwrite operation r1
r2
overwrites the r1
relation with values from r2
. Finally, Listing 3 also shows the results of two other operations as theorems: s1
minus s2
results in a set with all elements from s1
that are not in s2
(thm1). Brackets are used to access a relation element, as exemplified by thm2.Set | r1 | r2 | Dom.Subtraction | Dom.Restriction | Overwrite |
---|---|---|---|---|---|
element | s2
r1 | s2
r1 | r1
r2 | ||
a | 1 | – | 1 | – | 1 |
b | 3 | – | 3 | – | 3 |
c | 5 | 2 | – | 5 | 2 |
d | 7 | – | 7 | 7 | |
e | 4 | – | 4 |
3.2 Model checking and ProB
3.3 Hardware architectures
PC
, the stack pointer SP
, and the status register SR
, which stores status flags, such as interrupt enabled, overflow, etc. The MSP430 offers a very simple architecture with only one execution mode and a fully orthogonal instruction set. There is no privileged mode, nor any memory protection or memory management unit. Once an interrupt occurs, the PC
and SR
registers are pushed onto the stack. Then, further interrupt requests (IRQs) are disabled and the PC
is overwritten with the address of the first instruction of the corresponding interrupt handler. The return from interrupt instruction, i.e., RETI
, restores SR
and PC
from the stack and finally continues where the handler has interrupted the regular execution flow.pc
. The calling convention specification assigns meanings to the other registers, such as a stack pointer sp
, function arguments and return values. Additionally, Control and Status Registers (CSRs) with special access instructions are available for, e.g., managing the CPU or accessing on-chip peripherals in defined privilege levels. An IRQ switches the CPU into a higher privilege level, while software can issue an ECALL
instruction for that. In both cases, returning to user level is done by the instruction URET
.3.4 LLVM
.ll
files, an in-memory data structure used by optimizers, and an on-disk binary “bitcode” [45], with tools provided to transform one form into another.llc
, the LLVM static compiler, to produce architecture-specific assembly code that can be used by a native assembler and linker to generate native binaries.4 Framework overview
llc
, LLVM’s static compiler. While other Event-B code generators only understand basic arithmetics, EB2LLVM also understands the model’s sets and axioms, such that they are used to generate low-level code that refers to target-specific registers and instructions. Taking advantage of LLVM’s backend, we can generate machine code for several different targets.5 Modeling
5.1 Requirements
5.1.1 Hardware assumptions
5.1.2 Software specification
5.2 Refinement strategy: from abstraction to detailed specification
Level | ENV | OS |
---|---|---|
0 | ENV1, ENV2 | OS1 |
1 | – | OS2 |
2 | ENV6, ENV7 | OS3, OS4 |
3 | ENV3 | OS5, OS6 |
4 | ENV4, ENV5 | OS7, OS8, OS9, OS10 |
5 | Target-specific | OS11 |
5.3 Context switch model
5.3.1 Hardware-independent model
location
(Fig. 3a), an abstract representation of memory addresses and registers. In combination with data
, a subset of
, memory and registers can be represented according to ENV1. Two non-overlapping subsets with the same cardinality, context
and savedctx
represent the subset of locations that compose the context (ENV2) and the subset of locations of a saved context (OS1.A), respectively. The bijective function ctx2saved
\(\in \)
savedctx
relates each context location to where it is saved, while saved2ctx
is its inverse (OS1.B). The context is defined as a relation from context
to data
, while a saved context is a relation from savedctx
to data
.
old
context to saved
\(\in \)
, and the other loads new
context to loaded
\(\in \)
. The old
and new
contexts that are copied are simply event parameters, that will later be refined into the actual contexts that are copied. The OS is modeled in the event osProgress
. The event osFinal
is not a part of the OS, but is only introduced to model the state where the OS has successfully finished its execution. This event is composed only of guards, that is, it is enabled once the state represented in its guards is reached but does not change it anymore. The event osProgress
, that represents the OS kernel, is allowed to change the variables saved
and loaded
, but does not yet describe how the copies are made. It is made anticipated, which, in Event-B, means that it may execute several times, but must eventually give up control and allow the model to reach osFinal
. Refinements of an anticipated event must converge, i.e., decrease a variant, thereby proving that it eventually gives up control. The idea is that, since the context is copied in different steps by HW and software, this event can be refined into these steps. The Proof Obligations (POs) generated by Rodin verify that the refinements of this abstraction (Levels 1 to 5) are correct. If we can prove that the model always reaches osFinal
, we prove that the desired state after OS execution is reached. These proofs are shown in Sect. 5.4.osProgress
into two events: osEntry
(OS2.A: kernel entry responsible for saving the old context), and osExit
(OS2.B: kernel exit is responsible for loading the new context). They model state transitions (Fig. 4), and their guards define that entry must happen before exit, and exit may only start after entry is done.
toSave
\(\subseteq \) savedctx
keeps track of the context yet to be saved, while the parameter saveSet
defines the context subset saved in each run of osEntry
. The event is made convergent on the variant toSave
, and saveSet
is subtracted from toSave
in each run (Listing 5, Line 11). This guarantees that, eventually, the save process will complete and we move to a saved state. Similarly, the new variable toLoad
\(\subseteq \) context
represents the context yet to be loaded, while loadSet
defines the context subset loaded in each run of osExit
. The event is made convergent on the variant toLoad
, and loadSet
is subtracted from toLoad
on each run (Listing 6, Line 11), allowing the event to eventually reach the loaded state. Event osFinal
remains unchanged.oldTask
\(\in \) tasks
and oldCtx
\(\in \)
that represent the old task and its context when the kernel was requested. The saving operation in osEntry
must save oldCtx
into oldTask
’s save space. In order to save the context, we must map it to a saved context. Additionally, the context might be modified during the saving process (ENV6), e.g., the stack pointer is changed before it is saved when the architecture automatically pushes some registers onto the stack. We must account for this modification, since what is finally saved is the transformed version of the values, and not the original input values. Thus, we define in Listing 7 the functions transform
(axm1) and ctxTransform
(axm5). The architecture-specific function ctxTransform
is only declared at this level, and represents the modification of each value in the context. The function itself is only fully specified in Level 5. The function transform
converts a context into a saved context according to ctx2saved
(axm3), modifying the values stored in each location according to ctxTransform
. This is modeled by the axiom axm7 in Listing 7.
saved
is refined into t_saved
\(\in \)
(
)
, with glue invariant saved
\(=\) toSave
t_saved(oldTask)
. The new variable rTask
\(\in \) tasks
is equal to the constant oldTask
before kernel body is run, and represents the new scheduled task after the scheduler has run. While osFinal
always uses oldTask
to check if the context has been correctly saved, osEntry
refers to rTask
to save the context. Similarly, as the CPU context is only one, we create cpuCtx
\(\in \)
to represent it, and its relation to oldCtx
and loaded
is given by the glue invariants toSave
oldCtx
\(=\) toSave
cpuCtx
and loaded
\(=\) toLoad
cpuCtx
. This way, all three kernel parts (entry, body, and exit) deal with the same variables, simplifying code generation. We can finally replace the abstract save action in Listing 5 (Line 10) by the action in Listing 8 (Line 9).rTask
, thus we replace the abstract load from Listing 6 (Line 10) by the load in Listing 9 (Line 9). The functions transform
, ctx2saved
, and ctxTransform
, used for saving a context have their inverses also defined in Listing 7: invTransform
(axm2), saved2ctx
(axm4), and ctxInvTransform
(axm6), respectively. Similarly to axm7 for the save functions, axm8 models how invTransform
converts a saved context into a context.old
and new
parameters to reflect the real source and destination of the context copies (OS3.B) in Level 2 (Listing 8, 9, and 10).osBody
models kernel body (Listing 11), abstractly representing the scheduler (OS4). We do not model the typical functionality of the kernel body (e.g., scheduling, resource management, etc.) in this work, but will refine it to guarantee its execution according to the OS requirements.kernelMode
is a flag that indicates when the kernel has been entered. osBody
can only be enabled if it is true, and osFinal
if it is false; kernelCause
records why the kernel has been invoked. It unambiguously identifies each interrupt and syscall, and must be valid within osBody
; interruptEnable
is the interrupt enabled flag (ENV3). It must be false in osBody
, and loaded from the next task’s saved context during kernel exit; currStack
indicates the stack currently in use, abstractly representing a kernel or a task stack. osBody
is enabled if currStack
indicates kernel stack, while osFinal
requires it to indicate task stack. We strengthen osBody
and osFinal
guards to fulfill OS5 and OS6. Modifications of these variables in kernel entry and exit remain nondeterministic, since they are highly hardware-dependent. Listing 12 shows the new sets and variables, Listing 13 shows the new axioms, and Listing 14 shows the new guards for osBody
. We also create the event entryNothingToSave
, that mimics osEntry
and is explained in Level 4.
context
and savedctx
in three sections, as shown in Fig. 3b: manualctx
and manualsaved
for the locations that are only manipulated in the manual part, and two others for those automatically handled by the hardware. autoDIRECTctx
and autoDIRECTsave
for those locations permanently saved by the hardware, and autoTEMPctx
and autoTEMPsave
for those first copied to/from a temp
location. temp
is another subset of location
, created in this level.
osEntry
and osExit
by splitting each in two, and refining their parameters (saveSet
in Listing 8 and loadSet
in Listing 9) to differentiate the partitions in context
and savedctx
. The transform
and invTransform
functions are also refined to reflect the refinements of this level and the separation of the different levels of context copy. The events are refined according to Fig. 5: osAutoEntry
saves values from autodirectctx
into autodirectsave
and copies from autotempctx
to the new variable temp
\(\in \)
. For architectures that only copy parts of the context to temporary locations, not saving anything, we refine entryNothingToSave
into tempSave
, adding a copy to temp
and making it convergent on the variant
(must add elements to temp
). We also keep entryNothingToSave
only modifying variables from Level 3. Then, osManualEntry
saves manualctx
into manualsaved
and copies temp
into the autotempsave
location, completing the saving of the temporary part of the context. The action that represents this is shown in Listing 15a. The structure t_saved
gives us the locations where the context is saved for each task, and rTask
represents the running task, whose context must be saved. The saved context is overwritten (
) in two steps: one takes the values from
, filtering only the manual
part with a domain restriction operator (
), while the other takes the values from
, which has been written by osAutoEntry
with the autotempctx
values from cpuCtx
. The inverse operation is modeled in kernel exit: First, osManualExit
loads from manualsaved
into manualctx
and copies from autotempsave
into temp
, then osAutoExit
loads all autoctx
from temp
and autodirectsave
. The generic model we conclude in this Level is shown in Fig. 6.
5.3.2 Hardware-specific model instantiation
location
, all registers available in the architecture. At the same time, we also define which of them are part of context
(and to which subset), temp
, etc. We also define the locations for saved contexts and the CPU-specific functions ctxTransform
and ctx2saved
. The MSP430 model defines the context
subsets as shown in Listing 16a, while the same subsets are defined for the RISC-V in Listing 16b. Not shown here are the definitions of the savedctx
subsets, which simply mirror the context
elements, and their relations. The relation ctx2saved
, that maps each context
element to its correspondent in savectx
, is only shown for the MSP430.
sees
the Event-B context correspondent to the target, and the architecture-specific actions from Level 3 are made deterministic.osAutoEntry
to represent the interrupt. The event will also be refined to represent the syscalls, becoming two different (but equivalent to their abstraction) events. Since no registers are temporarily saved by the MSP430, tempSave
is removed. Once an interrupt is accepted, the MSP430 takes the interrupt handler’s address from the corresponding index in the interrupt vector table and executes the handler. The MSP430 does not record the interrupt ID anywhere, so we must save it in the specific interrupt handler before proceeding to osManualEntry
. We thus refine entryNTS
to save the interrupt ID and rename it to intHandler
.osAutoEntry
to syscall
executes the actions of both interrupt
and intHandler
events, i.e., it saves the autoctx
, disables interrupts, and stores the syscall ID. In osManualEntry
the rest of the context is saved, the stack is switched to kernel stack, and kernel mode is set to true. Since the MSP430 does not have differently privileged modes, we simulate the switch into kernel by setting a simple variable.mepc
, the status in mstatus
, and the interrupt number goes into mcause
. The CPU then switches to a more privileged mode and continues execution from the address stored in the so called trap vector. We refine kernelMode
to the privilege levels, adding an invariant that relates
to machine mode and
to user mode (OS11). Since the special registers where the autoctx
is stored are not task-specific, they belong to the autotempctx
, and must be permanently saved later. Because nothing is automatically saved (but only “temporarily” stored), tempSave
is refined to represent the interrupt and osManualEntry
follows after the interrupt. Event osAutoEntry
is never enabled and we can remove it.ecall
instruction is designed exactly for that, as it causes an exception, just like a hardware interrupt. We refine tempSave
again, now to represent the instruction ecall
. However, there is a difference: what is recorded in mcause
is not the ID of the syscall itself, as the architecture can only register the occurrence of the instruction. Since each syscall must pass its ID to the kernel and call ecall
explicitly, we refine entryNothingToSave
to do exactly that, also renaming it to syscall
.5.4 Proofs and model checking
osBody
always runs in the specified conditions for its execution, and osFinal
is reached with the specified conditions for task execution (OS5, OS6). Details for verifying the other OS requirements are omitted in this paper since the concept is the same. Instead, we also show how to verify some liveness properties: To guarantee that the model reaches the intended states, we verify that the steps resulting from the OS requirements are executed in the appropriate orders. (L1) The kernel executes in the correct order, and (L2) always finishes execution by always reaching osFinal
.5.4.1 Theorem proving
osFinal
guards) we want to achieve after OS execution, namely that an old context is saved and a new context is loaded. This state must be reached by osProgress
, which models the OS. Thus, event osProgress
is refined into the three main parts of the kernel (entry, body, and exit). Entry and exit are responsible, respectively, for saving the old task’s context and loading the new task into the CPU. The first abstraction is modeled such that osProgress
can not run forever. The idea is that it must change the state until it finally enables osFinal
, i.e., the desired terminal state. Through the refinements, we model how exactly this happens, splitting osProgress
into several events, and creating invariants and actions that model the OS requirements.osProgress
also give up control, and in Sect. 5.4.2 we prove that they indeed modify the state such that it eventually reaches osFinal
. Other POs prove that actions always respect the invariants (INV POs), or that a concrete event’s actions do simulate the abstract correspondents (SIM POs). There are several other rules for PO generation, which we do not detail here. Table 3 summarizes the number of POs generated in each level of abstraction, and shows how many of them were automatically or manually discharged. The manually discharged ones are differentiated according to their discharging complexity: simple POs only required a few steps to be discharged, while the complex POs required more experience with the proving system and the PO’s breakdown in several proving steps.oldCtx
and load the rTask
’s saved context: Discharging the related INV POs proves that, for every refinement, when our model considers the old context as saved and the new context as loaded, they indeed are. Those INV POs were always automatically discharged, except in few refinements, where they were manually discharged in a few steps.Level | #POs | Auto | Simple | Complex |
---|---|---|---|---|
0 | 8 | 8 | 0 | 0 |
1 | 17 | 15 | 2 | 0 |
2 | 63 | 52 | 11 | 0 |
3 | 14 | 14 | 0 | 0 |
4 | 83 | 39 | 35 | 9 |
5 | 70 | 58 | 6 | 6 |
Total | 255 | 186 | 54 | 15 |
100% | 73% | 21% | 6% |
osManualEntry
and osAutoExit
, we had to create a new parameter and an extra theorem in order to discharge the SIM POs. We detail here the proof strategy for the save action SIM PO in osManualEntry
. The same strategy was applied to osAutoExit
. We must prove that the manual save action from Level 4 (Listing 15a) simulates its abstract correspondent of Level 2 (Listing 8, Line 9). We replace the temporary save part of the action by the parameter
5.4.2 LTL model checking
osFinal
is eventually enabled. The model shall (1) eventually reach osFinal
, staying there forever, (2) not reach a state where all events are disabled, (3) always have exactly one event enabled, and (4) implement the specified execution order: entry first, then body, and finally exit; and manual save after auto save (OS7) and manual load before auto load (OS9).context
and savedctx
elements to represent the locations that compose contexts and saved contexts, otherwise the state space explodes and ProB cannot complete verification. For this, we extend the Event-B contexts with the constant instantiations, and refine the machines we want to check. These machines are not modified any further, except for the model checks of Level 3 and 4, where the nondeterministic actions introduced in Level 3 would cause the checks to fail, since paths would exist in which osBody
and osFinal
could not be reached. As our intention is to leave this determinism to the architecture-specific models, the actions are modified to enforce the correct execution path. In Level 5, all actions are left unmodified, and we can check if the variables have been correctly set.osFinal
: An infinite loop is possible, because osBody
does not decrease any variant and does not modify any variables that affect its guards. Thus, we introduce a new boolean variable osBodyRun
, initialize it with false
, and add the guard osBodyRun
\(=\) false
and an action osBodyRun
\(:=\) true
. A similar error was found when entryNothingToSave
was introduced, prompting us to make it convergent and create a variant as explained in Level 4 (Sect. 5.3).osManualEntry
’s guards to require it to only be enabled after all autodirectsave
elements have been saved, as required by OS7. The new guard autodirectsave
toSave
\(=\) \(\emptyset \) forces this order. Similarly, osAutoExit
may only execute after all manualctx
is loaded (OS9), thus the new guard manualctx
toLoad
\(=\) \(\emptyset \) was introduced.6 Code generation
.eb
) allows EB2LLVM to work outside of Rodin. The Rodin plugin we use to export the model is a modified version of the B2Latex plugin [20], that we call EB2LatexLst.
.eb
files into abstract syntax trees that it can process to generate code. For each concrete Event-B machine, EB2LLVM will create one LLVM IR file, containing one function named after the Event-B machine, with one code block for each of its events. In Event-B, control flow between events is not directly modeled, but defined within the event’s guards. To extract this control flow information, we use the event enabling analysis of the model provided by ProB [47]. At the end of each block, compare and jump instructions are created to direct the flow to the appropriate next block.6.1 Factorial example
f(n) = n * f(n-1)
. Our Event-B model will use recursion for the model checking. However, the intended implementation is an iterative one, as Listing 17 shows. This C code is not relevant for code generation, but just to (1) present the algorithm in comparison to the model and (2) to compare the final binaries compiled from the C code and our generated LLVM IR.
fFinal
event that models the goal of the factorial function in its guards, and has no actions (skip
). The guard
assures the correctness of the calculated result. Since some parts of the model are not intended for code generation, such as this guard, any statement with the label starting with “model” will be ignored by EB2LLVM. The event where the factorial of input
is calculated is calc
(Listing 18, Line 16). This event is equivalent to the while
block in Listing 17.factorial
will assume that these pre-conditions hold, and will not, e.g., check if \(\texttt {input} \in 0..\texttt {maxInput} \).
data
type as a function of the target’s bit-width. It is important to notice that, currently, EB2LLVM only supports signed integers with the architecture’s bit-width. Therefore, even though the model’s variables could potentially be represented, e.g., by unsigned long integers, the instantiation must consider them signed integers. It follows that
, where b is the bit-width of the target. For the 16-bit MSP430, this means \(\texttt {maxInt} = (2^{15}-1)\), while for the 32-bit RISC-V, \(\texttt {maxInt} = (2^{31}-1)\).Origin/destination | calc | fFinal |
---|---|---|
INITIALISATION | possible | possible |
calc | possible_disable | possible_enable |
fFinal | syntactic_unchanged | syntactic_independent |
fFinal
with itself is syntactic_independent, since once the event executes, the state does not change anymore. EB2LLVM interprets this as a return instruction. The possible connections in the enabling analysis will all generate conditional branches at the end of the origin block. The condition is generated from the guards of the destination. All possible destinations will be considered: having INITIALIZATION
as origin, both calc
and fFinal
are possible. Therefore, two conditional branches will be generated. The first, in Listing 20 (Lines 7–10) branches to calc
if \(cnt < input\), as required by the guard
. Otherwise, it jumps to a newly created block INITIALIZATION_fFinal
(Lines 32–36) that then jumps to fFinal
if \(cnt = input\) (Line 26). Otherwise, it jumps to the default error
block (Line 29). Similarly, calc
may either jump back to itself (Lines 21–24) or to calc_fFinal
(Lines 38–42).load
and store
instructions generated from Event-B variable reads and assignments. Basic arithmetics are also translated to their LLVM equivalents. In the factorial example, we have addition and multiplication being converted to, respectively, the instructions add
and mul
.
llc
, LLVM’s static compiler. For the factorial example, we use the default optimization -O2
to generate assembly code for the targets MSP430 and RISC-V. The generated code is shown in Listing 21a and Listing 22a respectively. The code generated from the Event-B model, can be compared to the code compiled from the C factorial function in Listing 17. For the MSP430, we used the MSPgcc 6.2.1
compiler, and the resulting assembly is shown in Listing 21b. A clang
compiler was available for RISC-V, and we used version RISC-V rv32gc clang 9.0.0
(with the -march=rv32i
option), the same LLVM version we use in our code generation. The compiled code is shown in Listing 22b. Both used the same level of optimization -O2
.gcc
compiler inserted prologue and epilogue to save and restore registers used within the function. Since our code generator aims at generating exactly what is modeled, these registers are silently overwritten.4 Further, the gcc
version has more instructions to prepare for the while loop, however the instructions sequence inside the loop is more optimized.fFinal
and error
) would result in the execution of a return instruction, which follows the compare in the generated assembly code. However, this renders the compare instruction useless. A similar situation happens in the code generated for RISC-V: all the load instructions between Lines 24 and 31 in Listing 22a are generated from Listing 20, Lines 39 to 41, but never used in the compare/branch for which they were intended.clang
for the RISC-V (Listing 22b) has prologue and epilogue, while the code generated from Event-B (Listing 22a) does not. The while loop is also more optimized, and instead of storing partial results in each iteration, clang
only stores the end values before return. Though slightly less efficient, the code generated from Event-B for both MSP430 and RISC-V can be compiled into binaries that execute as intended.6.2 OS code generation
interrupt
, and automatically generate a return from interrupt instruction when it finds an event named reti
.t_saved
. We know from the model that t_saved
is a two-dimensional array, with one block of savedctx
elements for each task in the system (see Fig. 3b). The running task rTask
is our reference here, and each context element is to be stored in the array t_saved[rTask]
. EB2LLVM will use the HW models (Listing 16) to unroll the action into several store instructions, according to the other elements of the expression. It applies transform
to save each cpuCtx
and temp
value into their appropriate saved context index, filtering only manual*
cpuCtx
values due to the restriction operator (
). The results of this process for both MSP430 and RISC-V are shown in Listing 15b and Listing 15c, respectively.transform
involves further axioms and its evaluation during code generation is rather complex. Listing 23 shows it step by step for saving one MSP430 register. First, the save action is unrolled, resulting in several expressions like ①. Then, the code generator matches the right-hand side (②) to axm7
from Listing 7, also using the hardware-specific definition of ctx2saved
(axm22
in Listing 16a) to solve ③. Finally, ctxTransform
(④) is evaluated in a similar way. In this particular case it does nothing (i.e., \(\texttt {ctxTransform} (\texttt {el})(\texttt {ctx} (\texttt {el}))=\texttt {ctx} (\texttt {el}))\), so the final expression that will be translated into LLVM IR shows in ⑤. If a modification was defined by ctxTransform
in the model, it would be taken into account and the appropriate instruction would be generated.
read_register
(\(\rightarrow \) Line 1). In Lines 4 and 6 we can see that the accesses to elements in a relation are converted to LLVM’s GEP instruction6, and the value from the register is finally stored in the appropriate array position mR8
. Note that the save index mR8
is defined, in the model, as a constant value, and therefore expressed as the immediate value 5
in Line 6. This is repeated for each element that must be saved, according to the model. The LLVM IR code for RISC-V is similar to Listing 24: The register name and its index, as well as the data sizes are replaced according to the RISC-V model.
llc
. This time, we turn off optimizations (-O0
) to assure that unintended modifications to the model’s logic will not occur. For the MSP430, each block that saves one register (such as in Listing 24) is translated to assembly as shown in Listing 25a. The index for the array access in Line 3 is automatically calculated by llc
as \(\texttt {mR8} * ByteWidth\), and since the MSP430 is a 16-bit (2 bytes) architecture: \(5 * 2 = 10\) (Line 3). Listing 25b shows the RISC-V generated code to save register x8
in the save index \(\texttt {mX8} = 7\). Our model only uses the architecture’s specified register names, but llc
uses the Abstract Binary Interface (ABI) names (x8
= s0
). The index calculated for RISC-V ’s 32-bit (4 bytes) architecture is \(7 * 4 = 28\) (Line 7).llc
is needed to assure that any modification caused by a generated instruction is intentional according to the model.