Verification of Concurrent Machine Code Running on a Single-Core Machine

In this paper, we propose a machine-independent logic to verify concurrent machine code running on a single-core machine, using a combination of separation logic and rely/guarantee reasoning. Separation logic is employed to reason about local resources, and rely/guarantee reasoning is used to analyze the shared state. We formalize and prove the soundness of this logic. To illustrate the applicability of the logic in realistic platforms, we instantiate it with ARMv7 architecture and apply the instantiated logic to verify a UART driver in the presence of interrupts.


INTRODUCTION
Formal verification of high-level concurrent programming languages received attention over the past decades.Various techniques [15,21,26] and tools [2,12,16] have been proposed in this area.Applying these techniques to interrupt-based software is not straightforward, as their concurrency models are different.For instance, these techniques typically assume that threads can preempt each other's execution freely, while interrupt-based systems may prioritize interrupts, allowing an interrupt to preempt only those with lower priorities.Since interrupts can occur simultaneously, the processor must adopt a safe and suitable mechanism, such as interrupt prioritization, to handle them safely [23].In particular, reasoning about a concurrent system where threads and interrupts run simultaneously poses a unique challenge.
The high-level program verification approaches [15,21,26] often ignore interrupts, and only a few research efforts have focused on the verification of interrupt-driven programs (e.g., [14,25]).These approaches often handle interrupts at the statement level, while accurate analysis of interrupt-based software requires controlling executions at the machine instruction level, providing finer granularity.For instance, processors commonly check if an interrupt has been triggered before executing each instruction, whereas this check is done before each statement in high-level code analysis, potentially making the analysis less precise.In addition, high-level program verification methods usually consider only the program variables in their analysis, ignoring the rest of the machine state, including hardware state.However, the occurrence of interrupts depends on the hardware state as well.Verification at the level of high-level languages does not always provide the highest level of assurance, as it relies on compiler correctness.Compilers may introduce bugs in compiled code or alter the original code's behaviour.Therefore, we need techniques to verify machine code, i.e., the actual code running on the machine.
Tools and techniques have been introduced to verify sequential machine code, e.g., [1,9,13,17].Verification of the mikrokernel seL4 [22] is one of the successful verification attempts that verifies the kernel at the machine code level [18], employing a tool called decompiler [22].To the best of our knowledge, limited research has been conducted on the verification of concurrent machine code considering interrupts.Feng et al. [6] proposed a two-layer framework named AIM for the formal verification of concurrent machine code in a single-core system.However, this platform-dependent framework is tailored for an abstract machine architecture and a specific abstract kernel.Due to the coupling of kernel behaviour with processor behaviour in this framework, it lacks flexibility for verifying real-life machine code running on an arbitrary kernel/machine.
In this paper, we propose a simple logic to verify concurrent machine code running on a single-core CPU.The judgments and inference rules of this logic do not contain any platform-specific information.Based on separation logic and rely/guarantee reasoning [5,26], our approach abstracts away low-level details of the machine architecture and kernel structure from logic judgments.We do not concern ourselves with scheduling algorithms, interrupt handling mechanisms, kernel behaviour, etc., focusing solely on their interference in our analysis.All concurrency units, including interrupts and threads, are treated, modelled, and formalized uniformly.This enables the (co-)verification of interrupt-driven systems and multi-threaded programs using an identical mechanism.The logic is formalized in HOL4, and its soundness is proven semi-manually using this proof assistant.We instantiate this logic to prove some Interrupt Service Routines (ISRs) of a UART driver

Machine Code Semantics
We partition a machine state into two disjoint parts [5,26]: (i) the shared state accessible by several processes, and (ii) the local state accessible by a single process-where a process can be a thread or an interrupt handler.Shared resources encompass registers, shared memory, device ports, co-processors, etc. Shared memory may contain a code segment or data segment.A state is formally defined as a pair ⟨, ⟩, where  and  represent the local and shared parts of the state, respectively.Let  be the set of all possible non-partitioned states, and  be the set of all possible states partitioned into local and shared parts.We define a function F :  →  that takes an ordinary state and returns its representation in terms of a partitioned state.If  is a machine state and F () = (, ), a partial state of (, ) ∈  is a state like ( 0 , ) where  0 ⊆ .It shows the state of a specific process, representing a subset of the local states accessible by that process (i.e.,  0 ) in addition to the global resources (i.e., ) shared among all processes.A state (, ) is total if  contains all local resources, typically representing the states of all processes-the entire machine state.
At the machine code level, if the machine behaviour is deterministic, the semantics of instructions is typically defined using a singlestep execution function next :  → .However, in the presence of interrupts, the machine behaviour becomes non-deterministic, and state updates can occur due to both the execution of a CPU instruction and the handling of an interrupt.Therefore, we specify the behaviour of a machine with interrupts as next :  → 2  -meaning the next state of the system is no longer deterministic.To facilitate local and modular reasoning about processes, we need to specify the machine behaviour in terms of partial states rather than total states.Given a single-step execution function next, we define a new step function sep_next :  → 2  .This function is the smallest relation expressing the semantics of machine instructions based on partial states, defined as follows, where ⊎ represents the disjoint union operator: This rule informally asserts that the local states of other processes cannot influence the execution of a specific process with the local state  1 , and  1 is updated to  ′ 1 .A substate  2 is non-influencing on an instruction execution if replacing it with any other substate does not result in a different partial target state.This rule guarantees the safety monotonicity property required to reason about a process locally and reuse the local results in a broader context-specifically, in concurrent execution with other processes:

Process Formalization
Concurrent programming languages typically use language constructs for modeling concurrency (e.g., the parallel composition operator ||).They define the semantics of each component separately and use concurrency primitives to obtain the semantics of their composition.In contrast, (realistic) machine code languages are unstructured and lack explicit support for many abstractions (e.g., threads, synchronization constructs, schedulers).To reason about concurrent machine codes, we need abstraction mechanisms to specify concurrency units and concepts.We formalize the notion of processes that informally represents the parts of the machine state space where a specific process is active.Processes are of two types: primitive and composite.A primitive process represents a (partial) hardware interrupt handler or thread, while a composite process is obtained by composing smaller processes using a few operators.The formal definition of processes is based on the notion of events, where an event  ⊆  ×  is defined as a binary relation over partial states.Events are used to represent the start or termination of processes.The abort event abt is a special event defined by the user to indicate aborting a program.The definition of abort, like any other events, depends on the platform and the machine architecture.For example, it can be raised if an unpredictable situation happens, or the program accesses a part of memory that it has no right to access.
A primitive process is formally specified as a tuple T = ⟨ 1 ,  2 ,  T ,  T ⟩ where  1 indicates its start event,  2 denotes its termination event,  T is an invariant that holds only when this process executes, and the predicate  T defines the partial local state of T .The execution of T starts when the start event  1 occurs.The invariant  T holds during its execution, and it may be preempted to schedule other processes, i.e., the environment of T .Interruptions occur to handle a hardware interrupt, where control is passed to the environment.The process T terminates upon the occurrence of the termination event  2 .Note that the termination and start events must be disjoint; two events  and  ′ are disjoint if (,  ′ ) ∈ , then (,  ′ ) ∉  ′ , and vice versa ( ∩ ′ = ∅).A process invariant is defined using a precise assertion, an assertion that holds only for a unique partial state of a total state.In other words, an assertion  is precise if and only if, for all (, ) ∈  , there is at most one  0 ⊆  such that ,  0 |= .
A composite process is constructed from simpler processes using four operators: sequential composition (;), parallel composition (||), choice (+), and preemptable parallel composition (||⟩): The sequential composition of two arbitrary processes T 1 and T 2 , denoted by T 1 ; T 2 , is a composite process that executes T 1 followed by T 2 .The parallel composition of two processes T 1 and T 2 with disjoint events and invariants, denoted by T 1 ||T 2 , is a process that executes T 1 and T 2 concurrently.The preemptive parallel composition is used to model preemption.In the process T 1 ||⟩T 2 , if T 2 starts its execution, control is not returned back to T 1 as long as T 2 is not terminated.The operator + is used to model non-deterministic choice.
Example 2.2.Let an interrupt handler receive have a higher priority than the handler send.The expression send||⟩receive + receive; send states that the handler send may be interrupted by receive, but not vice versa.The operator || is commonly used to model concurrent threads because interrupt handlers often do not run in an interleaved way.

THE LOGIC
In this section, we introduce our logic to verify concurrent machine code.We use a combination of separation logic and rely/guarantee, as similarly done for a high-level programming language in [26]; rely/guarantee reasoning is employed to deal with the shared state, and separation logic is used to analyze the local state.

Executions
Let  =  0 − →  1 − → • • • denote an execution of a machine with the step function next, i.e., (  ,  +1 ) ∈ next, 0 ≤ .The state-partitioned form of this execution is an execution  =  0 − →  1 − → • • • where F (  ) =   = (  ,   ), and (  ,  +1 ) ∈ sep_next for 0 ≤ .The states in  and  represent the whole machine state where each transition is performed by a specific process, i.e., the whole executions comprise of the transitions of different processes running on the machine.A process can change its own private data freely, yet its interference on global state should not corrupt other processes executions.For instance, when a process is interrupted, its context (e.g.registers, banked registers etc) should be stored in the stack and restored safely upon return.This means that other processes should not modify its context in the stack, i.e., there will be no stack overflow and the context switching process is safe.We define the interference of a process's environment in its executions as a relation  that expresses the updates done in global state by the environment.An execution of a process running in an environment with interference  is a sequence is the process partial observation of the machine state, a transition is either a single instruction execution by the CPU denoted by   ↠  +1 , or it is a sequence of instruction executions by the environment with at most interference , denoted by   ⇝  +1 where (  ,  +1 ) ∈  (See Figure 4).We show the -th element of   by   (), the -th prefix of   by for  ≤ .

Assertions and Judgments
We use a unified syntax for specifying assertions on shared state and local state [26]; an underlined assertion is evaluated on the public state, and an ordinary assertion is evaluated on the local part of the state.The semantics of some of the primitive assertions is defined as follows: The semantics of other logical operators such as quantifiers are defined ordinarily.An assertion  is stable under an interference relation , if and only if whenever it holds initially, it also holds after any update that preserves , i.e., This notion is mainly used to ensure that a process context/state is preserved by its environment in case of an interruption.A process context usually contains information on the data memory, registers content and the memory pointed to by the current program counter which shows the current instruction to be executed, if there is no preemption.If a process gets interrupted, the environment may modify its code and/or its data.Hence, it must be guaranteed that the process state is preserved by the environment.
A judgment of our logic is of the form ⟨, ⟩⊢ T ⟨, ⟩, which means, for any (partial) execution of T running in an environment with interference , if the initial state of execution satisfies the precondition  and  is stable under , (i) the system does not abort (i.e., the event abt does not happen as long as T is not terminated), (ii) the process T guarantees the relation  in each one of its transitions, and (iii) if eventually T terminates, the postcondition  holds.Note that if T gets interrupted before starting its execution, the environment may modify its precondition .Hence, it must be guaranteed that  is preserved by the environment, i.e., it is stable under the environment interference .

Inference Rules
Figure 1 shows some of the inference rules of our logic where T = ⟨ 1 ,  2 , ,  T ⟩ is a primitive process.In all rules, the process events are disjoint with abt.The first three rules are the ordinary rules of sequential composition, strengthening the preconditions and/or the environment interference, and weakening the postcondition and/or processes guarantees.The rules Step and envStep specify the semantics of machine instructions in terms of our judgments, i.e., one step executions.The rule Step expresses the semantics of an instruction in case of no interruptions by the environment (because T is active in  and subsequently takes the step  ↠  ′ ), while the proof rule EnvStep describes the semantics of an instruction executing in an environment with interference .
Step states that if T starts in a state with the precondition , it will make the interference , terminate and satisfy the postcondition .EnvStep states that if T running in an environment with interference  takes over the control in a state   , the precondition  holds in the state  1 of the execution and is stable under , then the  + 1-th transition transforms  to , guarantees , triggers  2 and does not abort.Note that the execution is defined as  0 →  1 → . . .  . .., and the premise of the rule includes a fairness condition that we Seq ⟨, ⟩⊢ T 1 ⟨,  ⟩ ⟨, ⟩⊢ T 2 ⟨,  ⟩ stable(, )  omitted for simplicity.This condition states that if an environment with interference  takes over the control from a process T , it will finally return the control back to it in the state   , T executes in the state   and is inactive in the states before   .
The rule parcomp states that if T runs in an environment with interference  ∪  ′ and it guarantees , and T ′ runs in an environment with interference  ∪  and it guarantees  ′ , then their parallel composition executing in an environment with interference , will have at most interference/guarantee  ∪  ′ .Furthermore, their postconditions remain separated provided that their preconditions are separated.The prerequisite of this rule states that the postcondition of each process should be stable under interference of the other process in addition to the environment, and the guarantee relations must be transitive (trans()).It also requires all the assume and guarantee relations to be safe monotonic, i.e., if they hold over two partial states, then they will hold in the extension of those partial states as well.
The rule preem is a relaxed version of the rule of parallel composition.While T ′ is running, the process T cannot execute according to the semantics of preemption, and this rule requires T ′ to execute safely in an environment with interference .The last rule is the ordinary frame rule of separation logic used for local reasoning.This rule states that if a process T starts its execution safely from a partial state satisfying , it can also be executed safely from an extended state satisfying  * , and the additional part of the state will not be affected, provided that it is partially stable under  and is disjoint with  's local state.A predicate  is partially stable under  if it is precise, and if it holds in a partial state  of  and (,  ′ ) ∈  , then  holds in a partial state  ′ of  ′ as well.Two partial states (, ) and ( ′ ,  ′ ) are disjoint, iff  and  ′ are disjoint.The predicate safedemon() holds, iff for all states  = ( ∪  0 , ) and  ′ = ( ′ ∪  0 ,  ′ ), if (,  ′ ) ∈  holds, then ((, ), ( ′ ,  ′ )) ∈  .

Soundness
To be able to prove soundness, we define the semantics of simple tasks and different operators in Appendix.In this section, we first define the judgment's semantics based on the process semantics in Appendix.Then, we introduce the soundness theorem.A partial execution of a process specifies the behavior of the process partially, likely before being terminated, while a total execution of a process means that this process has been terminated at some point of the execution.To define the logic judgment's semantics, we first define safe executions: be a partial execution of T running in an environment with the at most interference ,  be an assertion of our logic, and  be a relation defined over states.We say    is a safe execution of T, denoted by safe(   , , , T), iff for all  < , • if   ↠  +1 , 0 < , the event abt does not occur, and  holds by the transition, i.e., (  ,  +1 ) ∈  and is a total execution of T, it implies that  holds in  +1 .• T modifies only the shared state and its own local state, i.e., if The semantics of a triple is defined as the following: We formalized the logic in HOL4 and proved its soundness semiautomatically using this proof assistant (with some lemmas proven manually).The formulation of our logic judgments consists of approximately 550 lines of code, and the entire soundness proof script has a length of around 3000 lines of code.While our logic is currently not supported by a tool for automatic verification, we believe it is possible to implement such a tool to semi-automatically verify a program using this logic.

SPECIFICATIONS FOR ARMV7
To be able to use our logic for verification, it's important to formulate contracts, interferences, and threads or interrupt handlers in a way that the proof rules can be applied.In this section, we propose some specifications used to analyze interrupt service routines in the ARMv7 architecture [23].ARMv7 Architecture.Figure 2 shows a typical ARMv7 machine that can run in seven different execution modes: the non-privileged user mode in addition to six privileged modes (abt, fiq, irq, svc, und, and sys).The ARM registers include sixteen general purpose registers ( 0 , . . .,  15 ) that are available from all modes in addition to the banked registers of each privileged mode (apart from sys) that are accessible only in the corresponding mode.Furthermore, ARMv7 provides a few program status registers that hold some status flags, e.g., the flag "I" to indicate whether the hardware interrupt is enabled or not, or "M" to show the current execution mode etc.The program status registers include cpsr (Current Program Status Register) and one banked psr register for each privileged mode except from the sys mode, e.g., spsr_fiq is the banked psr in the fiq mode.The memory is a collection of cells within a specific address range where each cell has an address and a stored value.
ARMv7 supports six types of exceptions (including interrupts), and each exception handler executes in a specific mode.Among these modes, the two modes irq and fiq are used for interrupt handling: fiq is used for interrupts requiring a fast response and low latency, and irq is used to handle general interrupt services.Once an exception occurs, the CPU configures the program status registers (e.g., sets the flag I in cpsr to 1 to disable irq interrupts), and the program counter (pc) is altered to point to an entry in the vector table.Each entry in the vector table contains an instruction for jumping to the entry point of an exception/interrupt handler.The exception handler should first save the context in a stack so that the context can be restored upon return.After handling the exception, the context should be restored, and pc is altered to point to the next instruction to be executed prior to the interruption.
State Elements in ARM.In our specification, we represent the register   in the mode  with aR_, the memory in the address  with aM[], the execution mode with aD, the interrupt type with aIT, and the psr register with spsr among others.We use aPC to refer to aR15_ (program counter), aSP to refer to aR13_ (stack pointer) and aLR to refer to aR14_ (link register).All state elements except from the local memory of a process are in the shared state, e.g., the registers, flags, and the interrupts stack.Observe that if each process uses its own stack, then its stack will be considered in the local state.
ARMv7 Exception Handlers Specification .We assume that an interrupt handler has a few entry points Ept and exit points Ext from where it starts and finishes its execution respectively.The handler code is stored in the memory locations PC and runs in a mode from modes while the stack pointer points to a location in SP.
To specify an exception handler, we need to specify its start event  1 , termination event  2 , invariant  and local state .The activeness of an ISR (Interrupt Service Routine) depends on the stack design and the interrupt handling scheme.In the simple interrupt handling scheme where interrupts are disabled during handling an interrupt, some constraints on the CPU mode and the program counter would be sufficient to determine whether an ISR is active or not, as it will not be interrupted to schedule another process.However, in nested schemes where an ISR may be interrupted by another interrupt running the same ISR, we have to consider the stack pointer as well to make the activation conditions unique.The reason is that the constraints on the mode and the program counter will be the same for two instances of the same ISR, however, these two ISRs should point to different parts of the stack to become distinct.We formally define an ISR as I = ⟨ 1 ,  2 , active, ⟩ where the activeness predicate is expressed as the following function: active = .aD →  * aSP →  * aPC →  * aIT → ⊥ ∧  ∈ modes ∧  ∈ stack ∧  ∈ code and the notation  →  means that the state element  has the value .This  expression says that an ISR is active in the state , if the CPU is in the mode  ∈ modes, the stack pointer is within a specific range of memory addresses stack allocated to the ISR from the stack space, the program counter points to a memory location from code where the ISR code is stored in and no hardware interrupt is occurred (aIT is ⊥).Note that an ISR can execute in several modes, e.g., a nested handler usually switches to the system mode before enabling the interrupts.The start event occurs by a transition  →  ′ if I is not active in , the program counter points to an entry point of I in  ′ , and the handler becomes activated by that event: The finish event  2 is defined similarly where I becomes inactive and the program counter points to an exit point in the target state: Pre/Postconditions.If the execution of a process (e.g., an ISR) is preempted by an interrupt, its context should be stored in the stack so that it can be restored properly upon return.When a process is active, its context includes the CPU state (registers/flags/coprocessors, etc.) and its local memory (e.g., its code, its stack, and possibly its local data).The context of a suspended process comprises all this information stored in the stack and its local memory.The contracts of a process usually constrain the registers/flags content and/or the memory.In our logic, the contract should be stable under the environment's interference, and obviously, resources such as registers/flags/coprocessors are shared among all processes and might be altered by them.Hence, we need to specify the contracts in a way that they become stable under the environment's interference.We use the following general specification to describe a (partial) pre/postcondition cond on the process context defined on shared resources that should be stable under the environment's interference: where cntxt is the condition defined on the CPU state, and stored is a predicate defined on the stored context.We also define the environment relation in a way that preserves this condition (See next section).
Assume/Guarantee Relations.To guarantee that a condition on registers holds when a process is resumed after interruption, we need to ensure that it is stored correctly in the memory, and the environment guarantees that the context is stored, restored, and manipulated correctly too.We define the following general specification to express the environment interference Inter on the context of a process.
The guarantee of a process should ensure that (i) the process does not alter the stored context (stack) of other processes, and (ii) the registers and the interrupt flags of the interrupt controller are configured correctly to indicate that a specific interrupt handling scheme is used.Both the rely and guarantee relations may contain other application-specific conditions.The interrupt controller configuration should be preserved by the environment as well.Note that the above relations do not hold while a context-switching routine executes, and therefore, cannot be used to verify a contextswitching routine using our logic.If the context-switching routine is not preempted by other processes and runs sequentially, then we can verify it like sequential code.

CASE STUDY
We use an interrupt-based UART device driver as a case study in this paper, as introduced in [4].A UART is a serial communication device with two basic communication functions: transmitting and receiving.This driver has several memory-mapped registers, including the line status register (LSR) to represent the current state of communication, the transmit hardware register (THR) to store data to be transmitted, the receive buffer register (RBR) to store data to be received, the interrupt enable register (IER) to enable/disable interrupts, and the interrupt identifier register (IIR) which represents the source of occurred interrupts, among others.The driver consists of three ISRs: send, receive, and error.The ISR send transmits a character from the circular transmit buffer to the THR register when the THRE bit in LSR is set.The THRE bit indicates if THR has space for transmitting new data.The ISR receive reads a character from RBR and copies it to the circular receive buffer.The error ISR executes when an error occurs, such as a buffer overrun.
State Partitioning.In the UART case study, we consider the register THR as the local state of send, the receive buffer as the local state of receive, and the transmit buffer and RBR as the local states of the environment.Each interrupt handler can have its own stack or a single stack can be shared among all interrupts.We assume the latter case, i.e., all interrupts use a single stack.The stack is considered in the shared state.The partitioning function F decomposes a state into two partitions: We consider RBR, the transmit buffer, LSR, IER and IIR in the global state, while it is also possible to consider them as a part of the environment's local state.We assume no memory address translation, which makes F independent from the MMU (memory management unit) or MPU (memory protection unit) configurations.We then define the relation ARMsep_next to partition the state and . . . . 1 f 8 : cmp r7 , r 0 / / c h e c k w h e t h e r t h e t r a n s m i t b u f f e r ( r 0 ) i s empty 1 f c : moveq r2 , r 3 2 0 0 : moveq i p , #0 2 0 4 : beq 2 3 4 < u a r t 0 I S R +0 x f 8 > / / i f s o , j u m p t t o e x i t 2 0 8 : l d r b r2 , [ r4 , r 0 ] / / l o a d r 2 fro m t h e n e x t c h a r i n t h e b u f f e r 20 c : add r0 , r0 , #1 / / i n c e r e m e n t t h e b u f f e r i n d e x 2 1 0 : s t r b r2 , [ r 1 ] / / c o p y t h e c h a r i n r 2 t o THR 2 1 4 : and r0 , r0 , # 1 2 7 2 1 8 : l d r b r2 , [ r1 , # 2 0 ] / / E n t r y p o i n t , l o a d r 2 f rom LSR 21 c : t s t r2 , # 3 2 / / c h e c k w e t h e r LSR ' s THR b i t i s s e t , i .e ., THR i s empty 2 2 0 : bne 1 f 8 < u a r t 0 I S R +0 xbc > / / i f n o t , jump t o 1 f 8 t o do t h e work .describe the ARM instructions semantics in terms of our arbitrary state structure.
Contracts.Figure 3 shows the commented code for send whose entry point is the address 218w.We use x to refer to a word from the memory address x.THR is stored in the address  1 , LSR is stored in the memory location  1 + 20, the front index of the transmit buffer is in  0 , and the transmit buffer head is stored in  4 +  0 .This routine first checks whether the THR bit of LSR is set indicating THR is empty.If so, it exits.otherwise, it jumps to the address 1f8 (the first instruction in the code) to check whether the transmit buffer is empty.If so, then it exits.Otherwise, the first character in the buffer is copied to THR.We should prove that (i) it successfully copies a character from the transmit buffer to THR and increments its front index by one, provided that THR has room and there is a data for transmission, (ii) if THR is full or there is no data for transmission, it does not change THR and the buffer indexes, and (iii) it does not alter any other part of the memory.The preconditions/postconditions to prove (i) are specified as the following: where config is used to specify the interrupt handling scheme, and X describes some conditions on THRE bit and the buffer fullness.The predicate cond is defined according to the specification introduced in Section 4. Note that the predicate aM[] →  holds in  = (, ), if either the address  is in the local partition of memory and this state element belongs to , or  is in the global partition of memory and the element belongs to .The guarantee relation is defined as the following which states send should not modify the memory, apart from the stack and the transmit buffer's front index, to ensure the memory safety property (iii): The definition of the rely relation  depends on the interrupt handling scheme.The triple to prove for send is ⟨, ⟩⊢ send ⟨, ⟩.
Simple Interrupt Handling Scheme.In this scheme, the predicate config states (i) the irq interrupt (the flag  in cpsr) is set to indicate that the interrupts are disabled and (ii) the CPU mode is irq, i.e., config = aI → ⊤ * aD →  Verification of an ISR in this scheme is basically sequential machine code verification, as the step rule is reduced to the next function (See the definition of step).We use the rule Step to prove the correctness, i.e., we do not need to prove stability of the pre/postconditions (and subsequently the handler's context) under the environment interference, i.e., the precondition on the context is defined as cond = cntxt.
Nested Interrupt Handling Scheme.In this scheme, an interrupt can preempt handling of another interrupt.If we enable THR interrupt source, another instance of send may execute and modify the transmit buffer while it should be stable under environment interference.For this reason, send should not be reentrant.In this scheme, config states that the interrupts are enabled to allow nested handler execution, the CPU executes in the svc mode or irq mode and THRE bit in IER is set to disable THR interrupts (i.e., to disallow re-entrant handling), i.e., The predicate config is preserved by send and must be preserved by the environment as well.The rely relation ensures that the ISR context, the code, the transmit buffer and THRE bits in IER/IIR/LSR are not modified by the environment: where  is the set of memory addresses that store the code, the transmit buffer and LSR.We first apply the rule EnvStep to obtain the semantics of instructions in terms of our logic judgments.As the side-conditions of this rule, we should prove stability of the preconditions under the environment's interference.We also assumed that the environment is fair to the ISRs, i.e., the ISR finally executes its current instruction.To prove the correctness of a single ISR, we should prove a few side conditions including (i) stability of the preconditions under the environment's interference and/or the ISR interference (required by the rules Frame and EnvStep), (ii) transitivity of the rely relation, (iii) safe-monotonicity of the guarantee relation and safe-demonotonicity of the rely relation (the frame rule).Since the rely and guarantee relations are defined over the global state, it is straightforward to show that they are safe-monotone and safedemonotone.In the nested scheme, we can verify the correctness properties for the composite nested tasks such as (send||⟩receive) + (receive||⟩send) using the rules Preem and Choice.

RELATED WORK
Several works have been done in the area of sequential machine code analysis, such as type-based assembly languages [19,24], or logic-based approaches [1,13,18].In contrast to these approaches that focus on the verification of sequential code, we analyze concurrent code.Feng et al. [6,7] propose a two-layer framework, called AIM, for the formal verification of concurrent machine code running on a single-core system.The Hoare-style logic of this framework supports interrupts and (preemptive) threads, and it is defined for an abstract machine architecture and a specific kernel structure.In contrast to our logic, whose judgments are platform-independent (i.e., the judgments do not include any platform-specific information), the judgments of this approach are platform-specific and may not support the verification of codes written for other platforms.Furthermore, this framework uses only one type of interrupt (irq), while in embedded systems, several interrupts may exist, each with its own entrance point to the handler.It is unclear if this framework can be applied to architectures with realistic interrupt models.SAGL [5] is a logic designed for the verification of machine code, combining separation logic and rely/guarantee reasoning.This logic lacks a standard version of the frame rule of separation logic.It is tailored for a simple abstract kernel and a specific machine architecture, limiting its applicability to verifying arbitrary machine code.Duan [4] proposes an abstract device model that can be integrated into a formal model of instruction set architecture.This model is utilized to verify the UART device driver.However, the focus of this thesis is on a simple interrupt handling scheme and does not support advanced schemes such as nested ones.Goel et al. [9] present an approach to verify user-level x86 programs that make system calls.They implement a model of the x86 ISA in the ACL2 theorem prover and use it to simulate and verify user-level machine code programs.In [14], the authors propose a formal approach to verify interrupt-based systems using symbolic execution.Gotman et al. [10,11] introduce a logic for modular verification of an OS kernel using concurrent separation logic.The verification process decomposes into verifying the scheduler and the rest of the kernel separately.While this work considers interrupts in the verification procedure, the verification is conducted at a high-level programming language.
Regehr et al. [20] propose an approach for the verification of interrupt-driven programs by transforming the code into multithreaded code, where each thread is assigned a fixed priority.Scheduling is then done based on thread priorities.However, differences in the semantics of interrupts across hardware platforms and compilers limit the general applicability of this approach.For instance, it may not be suitable for platforms like ARM Cortex-M0, where interrupt priorities can change dynamically.Additionally, the correctness of the transformation is not proven.Sung et al. [25] present an approach based on abstract interpretation to modularly verify interrupt-based C programs.They first analyze each handler separately, identify possible data flows between different handlers, and then re-analyze the handlers.Due to its modular analysis, the approach improves scalability but may suffer from false positives.In contrast to our work, it offers automation, but the analysis is specifically designed for C programs.

DISCUSSION
In this paper, we proposed a logic for verifying concurrent machine code running on a single-core machine, modelling all concurrency units uniformly.This logic is platform-independent, i.e., its judgments and inference rules do not involve any platform-specific information, such as interrupt types, handling schemes, registers, memory management units.We believe this property makes it promising for verifying code running on various architectures and for co-verifying threads and interrupts.As a proof of concept, we instantiated it with ARMv7 and derived specifications to verify a UART's interrupt handlers.We also plan to introduce specifications to specify threads, interrupts, and contracts for each platform, enabling us to co-verify threads and interrupt handlers.
The logic currently lacks automation, and as part of our future work, we plan to develop a tool to automate the verification process.Since the judgments share similarities with the RGSep logic introduced in [26] for verifying high-level programs, we believe it should be possible to automate to a reasonable extent by restricting the specification to a specific form.Our initial investigation into automating the verification of ARM code is promising.We aim to develop a script in HOL4 based on Fox's model of the ARMv7 ISA [8] to automatically specify the semantics of instructions, similar to [17] (i.e., defining the function F ).We can use a modified version of a module used for machine code verification in [17].Notably, the state partitioning is application-specific, and the tool should support different partitioning schemes.Subsequently, we plan to use the rule EnvStep to obtain the semantics of each instruction under interference  in terms of our logic triples.This will enable us to verify code running in an environment with interference .The instantiation of primitive processes modeling single instruction executions will be derived from the interrupt specifications introduced in Section 4. We intend to use the inference rules of our logic to compute postconditions after code execution and check if the guarantee relation holds during execution, aiming for a semi-automated approach.The level of automation depends on our ability to automatically verify the stability of contracts under interferences.This can be achieved by writing specifications in specific patterns, akin to what we demonstrated in Section 4 to facilitate more automation.Given a tool supporting automatic reasoning using our logic, we plan to conduct further studies to co-verify threads and interrupt handlers in different platforms.