| Next revision | Previous revision |
| en:multiasm:paarm:chapter_5_7 [2024/09/27 20:27] – created pczekalski | en:multiasm:paarm:chapter_5_7 [2025/12/04 12:48] (current) – [Interrupts] eriks.klavins |
|---|
| ====== Procedures and call standards ====== | ====== Procedures and Functions Call Standards ====== |
| | |
| | There is no difference between procedures and functions in assembler. The distinction is more about their purpose and structure - functions usually return values and are often called from multiple places. Procedures, on the other hand, are often more focused on a specific task and typically do not return a value. In this section, we will use functions, as this term is commonly used in other programming languages as well. The function is a labelled block of code that performs a specific task. This block can be reused from different parts of a program. Functions follow a consistent structure that includes setting up a stack frame, saving necessary registers, performing the task, and returning control to the function caller. |
| | First, we need to get familiar with branch instructions. Branching is a way in which a processor handles decision-making and control flow. Branches jump to another location in the code, either conditionally or unconditionally. These locations are usually labelled with a unique label. They let the program repeat specific actions, skip parts of code, or handle different tasks based on comparisons. |
| | |
| | ''<fc #800000>B</fc> exit_loop <fc #6495ed>@ unconditional branch to exit_loop</fc>''\\ |
| | This instruction forces the CPU to jump directly to exit_loop, and the processor will proceed with the following instruction right after the exit_loop label. It does not check any statuses in the status register. This instruction has a range restriction: 128 MB of address space from the current PC register address. This means that the label must be within 128 MB of the device. If the program code is small, i.e. 2MB of instruction code, then this restriction can be ignored. Otherwise, this must be considered, but there is another instruction that does not have such restrictions.\\ |
| | ''<fc #800000>ADR</fc> <fc #008000>X0</fc>, exit_loop <fc #6495ed>@ load exit_loop address into X0 register</fc>''\\ |
| | ''<fc #800000>BR</fc> <fc #008000>X0</fc> <fc #6495ed>@ unconditional branch to address stored in X2 register</fc>''\\ |
| | But the register must hold an address for that location. The address can be loaded in many ways; in this example, the ''<fc #800000>ADR</fc>'' instruction is used to load it. |
| | |
| | |
| | ===== Conditional Branches ===== |
| | |
| | Conditional branches rely on the status flags in the status register. These flags are: |
| | * N - Negative |
| | * C - Carry |
| | * V - Overflow |
| | * Z - Zero |
| | |
| | Example code:\\ |
| | ''<fc #800000>SUBS</fc> <fc #008000>X0</fc>, <fc #008000>X1</fc>, <fc #ffa500>#3</fc> <fc #6495ed>@ subtract and update status register</fc>''\\ |
| | ''<fc #800000>AND</fc> <fc #008000>X1</fc>, <fc #008000>X2</fc>, <fc #008000>X3</fc> <fc #6495ed>@ Perform logical AND operation</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>EQ</fc> label <fc #6495ed>@ conditional branch using flags set with SUBS instruction</fc>''\\ |
| | As the comments pointed out, the logical AND instruction does not update the status register, which is why the branch condition relies on the SUBS instruction result. In the status register, all status flags are updated by the last instruction that issues the status flag update. AArch64 supports many conditional branches. Here are examples of conditions:\\ |
| | ''<fc #800000>B</fc>.<fc #800080>EQ</fc> label <fc #6495ed>@ if Z==1 Equal</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>NE</fc> label <fc #6495ed>@ if Z==0 not Equal</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>CS</fc> label <fc #6495ed>@ if C==1 Carry set Greater than, equal to, or unordered (identical to HS).</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>HS</fc> label <fc #6495ed>@ if C==1 Identical to B.CS</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>CC</fc> label <fc #6495ed>@ if C==0 Less than (identical to B.LO)</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>LO</fc> label <fc #6495ed>@ if C==0 identical to B.CC</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>MI</fc> label <fc #6495ed>@ if N==1 Less than. The result is negative</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>PL</fc> label <fc #6495ed>@ if N==0 Greater than, equal to, or unordered. The result is positive or zero</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>VS</fc> label <fc #6495ed>@ if V==1 Signed overflow</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>VC</fc> label <fc #6495ed>@ if V==0 No signed overfollow</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>HI</fc> label <fc #6495ed>@ if C==1 AND Z==0 Grater than</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>LS</fc> label <fc #6495ed>@ if C==0 AND Z==1 Less than or equal</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>GE</fc> label <fc #6495ed>@ if N==V Greater than or equal</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>LT</fc> label <fc #6495ed>@ if N==V Less than</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>GT</fc> label <fc #6495ed>@ if N==V and Z==0 Greater than</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>LE</fc> label <fc #6495ed>@ if N==V and Z==0 Less than or equal to</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>AL</fc> label <fc #6495ed>@ branch always</fc>''\\ |
| | ''<fc #800000>B</fc>.<fc #800080>NV</fc> label <fc #6495ed>@ branch never</fc>'' \\ |
| | These listed instructions check the condition flags set by a previous instruction that updates the status register, such as ''<fc #800000>CMP</fc>'' or ''<fc #800000>ANDS</fc>'' instruction. Note that ''<fc #800000>B</fc>.<fc #800080>AL</fc>'' and ''<fc #800000>B</fc>.<fc #800080>NV</fc>'' are logically useless instruction condition. The condition ''<fc #800000>B</fc>.<fc #800080>AL</fc>'' is mostly replaced with the B instruction (Branch) without any condition. That’s because the result is the same. The ''<fc #800000>B</fc>.<fc #800080>NV</fc>'' condition isn’t used because it's not needed. This is something like a NOP (No Operation) instruction, which forces the processor to do nothing. It can be pointed out that many conditions check the same condition flag, but they differ in naming. These mnemonics exist only for code readability. Taking a deeper look at the instruction set documentation, there are also instruction aliases, not only condition aliases. ARM keeps both mnemonic sets (aliases) because historically ARM assembly used CS (Carry Set) or CC (Carry Clear) mnemonics for arithmetic carry. Then came value comparison, for example, unsigned comparison HS (higher or same) or LO (Lower) to make the comparison meaningful. ARM assembly supports all comparison mnemonics, and the aliasing mnemonics produce the same binary code. Similarly, the aliasing instructions also share the same meaning, to preserve the code readability, even if the binary code for the aliasing instructions is equal. This, of course, makes reverse engineering harder because binary code can be translated into any of the aliasing instructions. |
| | |
| | Example: Loop with Condition\\ |
| | |
| | <codeblock code_label> |
| | <code> |
| | loop_start: |
| | CMP X0, #0 |
| | B.EQ loop_end |
| | SUB X0, X0, #1 |
| | B loop_start |
| | loop_end: |
| | NOP |
| | </code> |
| | </codeblock> |
| | This loop runs until X0 becomes zero. |
| | |
| | Example: Compare and branch\\ |
| | <codeblock code_label> |
| | <code> |
| | CBZ X0, label @ Branch if zero |
| | CBNZ X0, label @ Branch if not zero |
| | </code> |
| | </codeblock> |
| | These combine a test and a branch into one instruction. Useful for tight loops and fast decisions. |
| | |
| | Example: Test and branch\\ |
| | <codeblock code_label> |
| | <code> |
| | TBZ X1, #3, label @ Branch if bit 3 is zero |
| | TBNZ X1, #3, label @ Branch if bit 3 is not zero |
| | </code> |
| | </codeblock> |
| | These check a single bit in a register and branch based on its value. Suitable for flag or bit checks. |
| | |
| | <note important>Note that these branch instructions do not update the link register and cannot be used to call the functions or procedures.</note> |
| | |
| | The branch with a link is one of the instructions that updates the link register. The following example uses the ''<fc #800000>BL</fc>'' instruction to call a function ''add_two''. |
| | <codeblock code_label> |
| | <code> |
| | add_two: |
| | ADD X0, X0, X1 @ X0 = X0 + X1 |
| | RET |
| | |
| | MOV X0, #5 @ write value 5 into X1 register |
| | MOV X1, #3 @ write value 3 into X1 register |
| | BL add_two @ branch to function "add_two" and store PC+4 into Link register |
| | </code> |
| | </codeblock> |
| | This branches to a function, and the link register is updated at the same time. To return and execute the next instruction after the branch instruction, the RET instruction must be used as the very last instruction of the function. Functions in assembly rely on the same calling conventions used in higher-level languages. The AArch64 calling convention specifies how arguments are passed, how return values are handled, and which registers must be preserved across function calls.\\ |
| | The link register is a special-purpose register in the AArch64 architecture. It is held in the ''<fc #008000>X30</fc>'' register. The primary purpose of the ''<fc #008000>X30</fc>'' register is to store the address of the program code instruction that the program needs to return to after a function call. It maintains the return address when a function is called. The processor sets it automatically when using branch instructions like ''<fc #800000>BL</fc>'', which stands for //branch with link//. This allows the program to return to the correct place after a function finishes.\\ |
| | The link register helps avoid extra memory access. It saves the return address in a register rather than pushing it to the stack. This makes function calls faster. A function also begins with a unique label that is not the same as any instructions. If a function needs some arguments, then arguments are passed using registers ''<fc #008000>X0</fc>'' up to ''<fc #008000>X7</fc>''. Register use restricts the number of arguments and sizes. When more than eight 64-bit variables must be passed to the called function, or when an array or a specific data structure must be passed, the stack must be used. If the function must return a value, the ''<fc #008000>X0</fc>'' register can be used to store a single value. Like function status can be returned in the ''<fc #008000>X0</fc>'' register, but if the function returns more than just one value, then the stack would be the best place to store multiple values. \\ |
| | A typical function:\\ |
| | ''function_name: <fc #6495ed>@ Function label or function name</fc>''\\ |
| | ''<fc #800000>STP</fc> <fc #008000>X29</fc>, <fc #008000>X30</fc>, [<fc #008000>SP</fc>, <fc #ffa500>#-16</fc>]<fc #800080>!</fc> <fc #6495ed>@ Prologue of function</fc>''\\ |
| | ''<fc #800000>MOV</fc> <fc #008000>X29</fc>, <fc #008000>SP</fc> <fc #6495ed>@ Prologue of function</fc>''\\ |
| | ''... <fc #6495ed>@ Function body</fc>''\\ |
| | ''<fc #800000>LDP</fc> <fc #008000>X29</fc>, <fc #008000>X30</fc>, [<fc #008000>SP</fc>], <fc #ffa500>#16</fc> <fc #6495ed>@ Epilogue of function</fc>''\\ |
| | ''<fc #800000>RET </fc> <fc #6495ed>@ Epilogue of function</fc>''\\ |
| | This code saves the Stack pointer and Link register, then updates the Stack pointer. After doing the work, it restores the saved values and returns. However, if a function calls another function, or if the return address needs to be saved across longer sections of code, then the value in ''<fc #008000>X30</fc>'' should be saved to the stack. This prevents it from being overwritten. The use of ''<fc #800000>MOV</fc>'', ''<fc #800000>STR</fc>'', or ''<fc #800000>STP</fc>'' instructions can help save necessary information, including the stack pointer and the link register, to the stack memory. For example, a line of code, ''<fc #800000>STP</fc> <fc #008000>X30</fc>, [<fc #008000>SP</fc>,<fc #ffa500> #-16</fc>]<fc #800080>!</fc>'' stores register ''<fc #008000>X30</fc>'' (the Link Register) on the stack and updates the stack pointer. Later, the link register can be restored with the ''<fc #800000>LDP</fc> <fc #008000>X30</fc>, [<fc #008000>SP</fc>], <fc #ffa500>#16</fc>'' instruction. Such a pattern is typical at the very beginning and right after the end of a function, and it may be modified slightly. As in a typical function example with ''<fc #800000>LDP</fc>'' or ''<fc #800000>STP</fc>'' instructions, these instructions load and store register pairs. This ensures the program can return to the correct place even after nested function calls. |
| | |
| | |
| | ===== The Stack pointer ===== |
| | |
| | The stack pointer (''<fc #008000>SP</fc>'') is a special register in AArch64 that always points to the top of the stack. The stack itself is an organised block of memory where data is stored in sequential order. It means that the data cannot be placed everywhere inside the stack. It is used to store the data, return addresses, local variables temporarily, and saved registers during function calls. In AArch64, the stack grows downward, meaning that each time data is pushed onto the stack, the stack pointer value decreases and the stack moves to a lower address. In AArch64, each exception level has its own stack pointer. This allows the processor to handle function calls, interrupts, and exceptions safely without mixing data from different exception levels. |
| | |
| | The stack is a Last In, First Out (LIFO) structure. The data pushed onto the stack will become the first data to be removed later. The stack pointer always indicates the current top of the stack. |
| | <note important>On AArch64, the stack must always remain **16-byte-aligned**.</note> |
| | These requirements are defined by the ABI (Application Binary Interface). This alignment ensures compatibility with SIMD and floating-point operations and avoids unaligned memory access errors. SIMD (NEON) instructions operate on 128-bit registers ''<fc #008000>Q0</fc>''.. ''<fc #008000>Q31</fc>''(16 bytes). The processor is optimised for aligned load and store instructions. This allows faster memory operations, reducing data transfer cycles and more. |
| | |
| | The stack pointer behaves like a general-purpose register in most arithmetic and memory operations, but with some restrictions. It cannot be used as the destination for instructions if the result would be an unaligned address. \\ |
| | ''<fc #800000>STR</fc> <fc #008000>X0</fc>, [<fc #008000>SP</fc>, <fc #ffa500>#-16</fc>]**<fc #800080>!</fc>** <fc #6495ed>@stores the value in X0 at the address SP-16, then updates SP</fc>''\\ |
| | ''<fc #800000>LDR</fc> <fc #008000>X0</fc>, [<fc #008000>SP</fc>],<fc #ffa500> #16</fc> <fc #6495ed>@loads a value from the top of the stack into X0 register and then increases SP by 16</fc>''\\ |
| | Prologues of a function include instructions to push data into the stack, and epilogues include instructions to pop data out of the stack. But actually, data stays in memory; the stack pointer points to the location where the new data can be written. The stack is physical memory, but in the entire memory address space, it reserves only a small portion of that range. |
| | |
| | Working with stack and function calls, four key elements are used: Stack Pointer itself, the stack (a memory), the stack frame and the frame pointer (the register ''<fc #008000>X29</fc>''). The stack pointer and the stack are already explained. The stack pointer points to the stack, but the stack itself is built from multiple Stack Frames. The Stack Frame is an area of the stack that holds data and register values to be preserved during function calls. The register ''<fc #008000>X29</fc>'' (''<fc #008000>FP</fc>'' – Frame Pointer) contains the memory address of the previous (pre-Prologue) ''<fc #008000>X29</fc>'' value that is residing in the Current Stack Frame. Pushing the stack means creating a new stack frame, and this is done with function prologues. |
| | |
| | When a function begins executing, it often needs to preserve specific registers and allocate space for local variables. This is called creating a stack frame. The frame holds all the data that must be restored when the function returns. A typical function prologue (the code that runs when entering a function) might look like this:\\ |
| | ''<fc #800000>STP</fc> <fc #008000>X29</fc>, <fc #008000>X30</fc>, [<fc #008000>SP</fc>,<fc #ffa500> #-16</fc>]**<fc #800080>!</fc>**''\\ |
| | ''<fc #800000>MOV</fc> <fc #008000>X29</fc>, <fc #008000>SP</fc>''\\ |
| | This saves the previous frame pointer (''<fc #008000>X29</fc>'') and link register (''<fc #008000>X30</fc>'') on the stack, then updates the current frame pointer to the new value of ''<fc #008000>SP</fc>''. The link register holds the return address, so saving it ensures the program can return to the caller correctly after the function finishes. |
| | |
| | When the function is done, it uses an epilogue to restore the saved registers and free the stack space:\\ |
| | ''<fc #800000>LDP</fc> <fc #008000>X29</fc>, <fc #008000>X30</fc>, [<fc #008000>SP</fc>], <fc #ffa500>#16</fc>''\\ |
| | ''<fc #800000>RET</fc>''\\ |
| | This restores the old frame pointer and link register, moves the stack pointer back up, and returns to the saved address. |
| | |
| | A function may need to back up registers to free up registers for a task. When such a scenario occurs, the callee-saved registers are saved first. If a callee-saved register needs to be saved, the first one to be used is ''<fc #008000>X19</fc>''. If a function needs more, it will work its way up the register list till ''<fc #008000>X28</fc>'', the final callee-saved register. |
| | The prologue will be like this example:\\ |
| | ''<fc #800000>STP</fc> <fc #008000>FP</fc>, <fc #008000>LR</fc>, [<fc #008000>SP</fc>, <fc #ffa500>#-0x20</fc>]**<fc #800080>!</fc>** <fc #6495ed>@ Push (make) new Frame with size of 32 bytes</fc>''\\ |
| | ''<fc #800000>STP</fc> <fc #008000>X19</fc>, <fc #008000>X20</fc>, [<fc #008000>SP</fc>, <fc #ffa500>#0x10</fc>] <fc #6495ed>@ Backup (save) X19 and X20 onto the Frame</fc>''\\ |
| | ''<fc #800000>MOV</fc> <fc #008000>FP</fc>, <fc #008000>SP</fc> <fc #6495ed>@ Update fp to update the back chain</fc>'' |
| | |
| | The epilogue for the above prologue would be like this example:\\ |
| | ''<fc #800000>LDP</fc> <fc #008000>X19</fc>, <fc #008000>X20</fc>, [<fc #008000>SP</fc>, <fc #ffa500>#0x10</fc>] <fc #6495ed>@ Restore old X19 and X20</fc>''\\ |
| | ''<fc #800000>LDP</fc> <fc #008000>FP</fc>, <fc #008000>LR</fc>, [<fc #008000>SP</fc>], <fc #ffa500>#0x20</fc> <fc #6495ed>@ Restore old FP, LR, and SP. Frame is popped (destroyed)</fc>''\\ |
| | ''<fc #800000>RET</fc> <fc #6495ed>@ End function, return to Caller</fc>'' |
| | |
| | ** Stack Pointer in Exception Levels ** |
| | |
| | AArch64 supports multiple exception levels (EL0 to EL3). Each level can have its own stack pointer. The processor provides two stack pointers for EL1 and above: |
| | * SP_EL0 – stack pointer used when running code at EL0 (user mode) |
| | * SP_ELx – stack pointer used for each higher exception level (EL1, EL2, EL3) |
| | When an exception or interrupt occurs, the CPU automatically switches to the appropriate stack pointer for the new level. This prevents user-level code from corrupting kernel or hypervisor data and keeps the stacks for each privilege level separate. |
| | It can be manually accessed or configured by using the system registers:\\ |
| | ''<fc #800000>MRS</fc> <fc #008000>X0</fc>, SP_EL0 <fc #6495ed>@ Read user-mode stack pointer</fc>''\\ |
| | ''<fc #800000>MSR</fc> SP_EL0, <fc #008000>X1</fc> <fc #6495ed>@ Write a new value to it</fc>'' |
| | |
| | ** Using the Stack for Parameter Passing ** |
| | |
| | In AArch64, the first eight function arguments are passed in registers ''<fc #008000>X0</fc>'' up to''<fc #008000>X7</fc>''. If there are more than eight arguments, the rest are passed on the stack. The caller places these extra arguments at known offsets below ''<fc #008000>SP</fc>'' before executing a ''<fc #800000>BL</fc>'' (branch with link) instruction. The callee can access them either through the frame pointer or via ''<fc #800000>LDR</fc>'' instructions relative to the ''<fc #008000>SP</fc>'' stack pointer. Example:\\ |
| | ''<fc #800000>STR</fc> <fc #008000>X8</fc>, [<fc #008000>SP</fc>, <fc #ffa500>#-16</fc>]**<fc #800080>!</fc>**''\\ |
| | ''<fc #800000>BL</fc> long_function''\\ |
| | ''<fc #800000>ADD</fc> <fc #008000>SP</fc>, <fc #008000>SP</fc>, <fc #ffa500>#16</fc>''\\ |
| | This pushes the ninth argument onto the stack before calling //long_function//, which can then read it back. |
| | |
| | ===== Interrupts ===== |
| | |
| | An interrupt is a special signal that may cause the processor to temporarily stop normal program execution to handle an event that requires immediate attention. Interrupts are part of the previously described exception system or exception layer. |
| | Interrupts on the processors are handled similarly, regardless of the architecture – the processor saves the current program state, program counter and status register. AArch64 then switches to privileged exception levels and finally jumps to the exception vector – a specific address that tells the processor where to find the instructions to handle the current event. The exception vector contains the address of a specific function. In the ARMv8 documentations, the interrupt is treated as a subset of exceptions. The exception is any event that can force the CPU to stop current normal code execution and start with the exception handler. |
| | There are four types of exceptions. |
| | |
| | Synchronous exception – the exceptions of this type are always caused by the currently executed instruction. For example, the use of the str instruction to store some data at a memory location that does not exist or for which write operations are not available. In this case, a synchronous exception is generated. Synchronous exceptions can also be used to create a “software interrupt”. A software interrupt is a synchronous exception generated by the svc instruction. |
| | |
| | IRQ (Interrupt Request) – these are normal interrupts. They are always asynchronous, meaning they have nothing to do with the currently executing instruction. In contrast to synchronous exceptions, asynchronous exceptions are not always generated by the processor itself but by external hardware. |
| | |
| | FIQ (Fast Interrupt Request) – this type of exception is called “fast interrupts” and exists solely for prioritising exceptions. It is possible to configure some interrupts as “normal” and others as “fast”. Fast interrupts will be signalled first and will be handled by a separate exception handler. |
| | |
| | SError (System Error) – like IRQ and FIQ, SError exceptions are asynchronous and are generated by external hardware. Unlike IRQ and FIQ, SError always indicates some error condition. |
| | |
| | Each exception type needs its own handler, the special function that handles an exact event. Also, separate handlers should be defined for each different exception level at which an exception is generated. If the current code is working on EL1, those states can be defined as follows: the EL1t Exception is taken from EL1, while the stack pointer is shared with EL0. This happens when the SPSel register holds the value 0. EL1h Exception is taken from EL1 at the time when the dedicated stack pointer was allocated for EL1. This means that SPSel holds the value 1. EL0_64 Exception is taken from EL0 executing in 64-bit mode, and EL0_32 Exception is taken from EL0 executing in 32-bit mode. |
| | In total, 16 exception handlers must be defined (four exception levels multiplied by four execution states). A special structure that holds addresses of all exception handlers is called the exception vector table, or just the vector table. The [[https://developer.arm.com/documentation/ddi0487/ca/|AArch64 Reference Manual]] has information about the vector table structure. Each exception vector has its own offset: |
| | {{:en:multiasm:paarm:ddi0487c_a_armv8_arm.pdf_-_adobe_acrobat_reader_64-bit_.jpg|}} |
| | |
| | There is no fixed number of interrupts available for the processor. The total number of available interrupts is defined by the Generic Interrupt Controller (GIC) implemented in the system. The Raspberry Pi 5 have a GIC-500 interrupt controller, and according to [[https://documentation-service.arm.com/static/5e9085b8c8052b1608761814?token=|ARM GIC architecture]], the Raspberry Pi 5 can have up to 1020 different interrupt IDs: |
| | * ID0..ID15 is used for Software Generated Interrupts (system calls) |
| | * The next 16 IDs are used for Private Peripheral Interrupts for a single core |
| | * The rest of the IDs are for Shared Peripheral interrupts |
| | |
| | Practically, the Raspberry Pi 5 may have hundreds of interrupts from different sources in use, because the SoC chip BCM2712 have a lot of internal peripheral interrupts, the RP1 chip (the one that handles I/O lines and other peripherals on board) uses additional interrupts over PCIe bus. The Linux OS creates its own software interrupt, and finally, Linux combines them through the GIC. |
| | |
| | The interrupts can be disabled and enabled. For example:\\ |
| | ''<fc #800000>MSR</fc> DAIFCLR, <fc #ffa500>#2</fc><fc #6495ed> @ enable IRQ (interrupt request)</fc>''\\ |
| | ''<fc #800000>MSR</fc> DAIFCLR,<fc #ffa500> #1</fc><fc #6495ed> @ Enable FIQ</fc>''\\ |
| | ''<fc #800000>MSR</fc> DAIFSET, <fc #ffa500>#2</fc><fc #6495ed> @ disable IRQ</fc>'' |
| | |
| | |
| | ** The Stack Pointer and Interrupt Handling ** |
| | |
| | When an interrupt or exception occurs, the processor automatically saves the minimal state. It then switches to the stack pointer associated with the current exception level. The interrupt handler can safely use the stack at that level without overwriting user or kernel data. For example, when an IRQ occurs at EL1, the CPU switches from the user’s stack (SP_EL0) to the kernel’s stack (SP_EL1). This change is invisible to user code and helps isolate privilege levels. |
| | Inside an interrupt handler, the code must save and restore any registers it modifies. A minimal handler might look like this:\\ |
| | ''irq_handler: <fc #6495ed>@ the label for the interrupt handler</fc>''\\ |
| | ''<fc #800000>STP</fc> <fc #008000>X0</fc>, <fc #008000>X1</fc>, [<fc #008000>SP</fc>, <fc #ffa500>#-16</fc>]**<fc #6495ed>!</fc>**''\\ |
| | ''<fc #6495ed>@ Handle the interrupt (event)</fc>''\\ |
| | ''<fc #800000>LDP</fc> <fc #008000>X0</fc>, <fc #008000>X1</fc>, [<fc #008000>SP</fc>], <fc #ffa500>#16</fc>''\\ |
| | ''<fc #800000>ERET</fc> <fc #6495ed>@ retorn from innetrupt (exception) handler</fc>'' |
| | |
| | Here, the stack pointer ensures the handler has a private area to store data safely, even if multiple interrupts occur. |
| | |
| | <codeblock code_label> |
| | <caption>Simple examples of interrupt handlers</caption> |
| | <code> |
| | irq_el1_handler: |
| | @ Save registers |
| | STP X0, X1, [SP, #-16]! |
| | STP X2, X3, [SP, #-16]! |
| | |
| | @ Acknowledge interrupt (example for GIC) |
| | MRS X0, ICC_IAR1_EL1 @ Read interrupt ID |
| | CMP X0, #1020 @ Spurious? |
| | BEQ irq_done |
| | |
| | @ Handle interrupt (custom code here) |
| | BL handle_device_irq |
| | |
| | @ Signal end of interrupt |
| | MSR ICC_EOIR1_EL1, X0 |
| | |
| | irq_done: |
| | @ Restore registers |
| | LDP X2, X3, [SP], #16 |
| | LDP X0, X1, [SP], #16 |
| | ERET @ Return from exception |
| | </code> |
| | </codeblock> |
| | |