26/09/2025

Barium No-Boot (6): exceptions (I)

Preface

Well, today we are going to continue to learn the ARMv8 machine.

One day we've started with «one-dimensional» product — one core executing straight, linear code. In previous post, we've added new, second «dimension» — we've made the product multi-core. Now it has four ALU cores running independent code simultaneously, in parallel. This time, we will add another one dimension (or at least a half of a dimension) to our product. We will review one important concept of all computing machines, and ARMv8 is not an exception — exceptions.

Theory

Let's start with the theory. The conception of exceptions consists of three parts: cause (in terms of ARMv8 — «syndrome»), handler (a code that processes arised exception) and a entity that binds cause and handler (in terms of ARMv8 — «Vector Table»).

The handlers, despite that they are divided in groups, from the technical point of view are absolutely identical for all exceptions. Vector table, the entity that associates exceptions with their corresponding handlers, is just a 2kB of aligned in a certain way code, divided in 16 equal-sized blocks. Each of that blocks consists of 32 ARMv8 instructions. As we can see, Vector Table is binding entity by its structure (strictly regulated by ARMv8 standard) and a set of handlers by its content.

The causes or, generally speaking — exceptions themselves, are of two types — asynchronous and synchronous. Asynchronous are those what take place when, let's say — some «external» event occurs. Interrupt is a good example of asynchronous exception — you never know when an interrupt will occur while you are writing code. Synchronous exceptions are those what arise immediately after some instruction is executed or, in some cases, somewhere inside of execution process. In other words, synchronous exception is just a reaction to instruction. It can be arised by an instruction that caused some error — the situation when the machine can't continue to function normally without handling the error. An example of such an exception is a case of illegal instruction — situation where a fetched instruction could not be decoded (and executed). Attempt to access invalid address of memory is another example of synchronous exception. «Handling» such errors implies analysing condition of computing machine and an attempt to fix that state prior to allowing the code to continue to run, or, in worst cases — preventing the machine from running further code by (usually) leaving it in an infinite loop. And at last, there is a set of synchronous exceptions that are designed not to handle errors, but to serve regular, normal duties on a developers purpose. Synchronous exceptions will be the topic of our today's post — we'll review Data Abort and Data Alignment exceptions as examples of error/fault and so called System Monitor Call (SMC instruction) as example of exception for developers purpose. We chose these as subject of this post.

Let's review theory of the exact exceptions we are about to practice on.

The first — Data Abort. This exception arises on an attempt to access to a non-existent memory address. Data Abort exception can be generated easily with following:


raw_writel(0, INVALID_ADDRESS);

or


raw_readl(INVALID_ADDRESS);

where INVALID_ADDRESS can be defined, for example, as:


#define INVALID_ADDRESS (UINT64_MAX)

The ARMv8 architecture allows to determine if it was an attempt to read or to write, as well as address an attempt to access was made to. We'll review this functionality in code later.

Data Alignment exception can be caused by an attempt to load or store from or to an unaligned memory address by executing, for example:


ldr x0, 0xDEAD

The simplest way to generate Illegal Instruction exception is:


udf 0

Or assembly illegal instruction and try to execute it. In our practice part of this post we will assembly DEADBEEFh opcode, which does not represent any valid ARMv8-A instruction. 

SMC is another one synchronous exception and from the developer's point of view looks like a regular call (in terms of ARMv8 — branch) to a normal function, because its handler will be executed immediately after this instruction and before executing the next one and doesn't require to fix any things (because it is not a reaction to any kind of error or fault). It is good as a case of exception to learn on ARMv8 and to play with.

Thus, here is our plan for this post:

  1. Construct Vector Table
  2. Configure ALU to use our Vector Table
  3. Design handler for Data Abort exception
  4. Design handler for Data Alignment exception
  5. Design handler for Invalid Instruction exception
  6. Design handler for SMC exception
  7. Generate Data Abort exception
  8. Generate Data Alignment exception
  9. Generate Invalid Instruction exception
  10. Generate exception with SMC instruction

Let's get it started and run through the plan (in two parts).

Construct Vector Table

The Vector Table is a block of regular ARMv8 code 2kB in size, split in 16 sections 128 bytes in size each. The placement of Vector Table also must be aligned to 2kB boundary. We will place our Vector Table in a separate file — vectors_64.s. It is aligned to 2kB and consists of 16 section aligned to 128 bytes. The minimal acceptable template of Vector Table of ARMv8 could consist of header like:


.arch armv8-a .balign 0x800 .globl _vectors_el3 _vectors_el3:

and 16 equal blocks of handlers that look like:


.balign 0x80 bl some_exception_handler eret

In our case, we have default handler as shown below:


stp x0, x1, [sp, #-16]! stp x2, x3, [sp, #-16]! stp x29, x30, [sp, #-16]! bl _exception_entry bl exception_handler ldp x29, x30, [sp], #16 ldp x2, x3, [sp], #16 ldp x0, x1, [sp], #16 eret

Here we preserve registers that we'll use, call local function _exception_entry and global routine exception_handler() (located in exceptions_64.c file). Where _exception_entry is declared as:


_exception_entry: # Calculate Exception group No. as first parameter of default handler: # Exception Group No. = (instruction address - _vectors_el3) / 80h # We get instruction address from the Link Register as it points to # address of instruction next to bl, which lead to this function, thus # it represents address inside of an exception group in Vector Table: mov x0, lr ldr x1, =_vectors_el3 sub x0, x0, x1 mov x1, 0x80 udiv x0, x0, x1 # Get core number from MPIDR and store it as second parameter: mrs x1, MPIDR_EL1 and x1, x1, 0xFF # Pass Exception Link Register as third parameter: mrs x2, ELR_EL3 # Pass Exception Syndrome Register as fourth parameter: mrs x3, ESR_EL3 ret

In this function we prepare some data for real exception handler: gather information for later review — calculate the number of exception group, get number of ALU core on which exception arised, exception link register and exception syndrome register. exception_handler() will be reviewed later.

Configure ALU to use our Vector Table

Vector Bar on ARMv8 is set up by writing its address to VBAR_EL3 register. VBAR stands for Vector Base Address Register. This is done in crt0_64.S in a newly appended function _set_vbar_el3 which is called from _crt0_main_64 function. Here we store the initial value of VBAR_EL3 register as fourth parameter of barium_main() for later review. The rest is simply and clear here:


_set_vbar_el3: # Store initial RVBAR as fourth parameter: mrs x3, VBAR_EL3 ldr x7, =_vectors_el3 msr VBAR_EL3, x7 dsb sy isb ret

Design handlers for Data Abort, Data Alignment, Invalid Instruction and SMC exceptions

The whole exception_handler() routine is depicted in list below:


void exception_handler(uint64_t _EG, uint64_t _CoreNo, uint64_t _ELR, uint64_t _ESR) { uint64_t lVal; uint16_t lEC; uint8_t lDFSC; uart_output_string(uart_base, "Exception: "); uart_output_hex(uart_base, (uint8_t*)&_EG, 0, 1); uart_output_string(uart_base, ", ALU Core №: "); uart_output_dec(uart_base, _CoreNo); uart_output_string(uart_base, "\r\nClass: "); /* * Get Exception Class (EC) field - it is [31:26] bits of ESR_EL3: * EC, bits [31:26], ARM DDI0601 (ID092025), p.693 */ lEC = (_ESR >> EC_SHIFT) & EC_MASK; if ((lEC & EC_SMC) == EC_SMC) { uart_output_string(uart_base, "SMC, #imm Value: "); /* * Get immediate value - it is [15:0] bits of ESR_EL3: * imm16, bits [15:0], ARM DDI0601 (ID092025), p.712 */ lVal = bswap_64((_ESR & SMC_IMM_MASK)); uart_output_hex(uart_base, (uint8_t*)&lVal, 6, 2); uart_output_string(uart_base, "\r\n"); } else /* Data Abort & Data Alignment Faults */ if ((lEC & EC_DATA_ABORT) == EC_DATA_ABORT) { lDFSC = _ESR & DFSC_MASK; if (lDFSC == ISS_DAB_FAULT) uart_output_string(uart_base, "DAF: "); else if (lDFSC == ISS_DAL_FAULT) uart_output_string(uart_base, "DAL: "); else { uart_output_hex(uart_base, (uint8_t*)&lDFSC, 0, 1); uart_output_string(uart_base, "h: "); } /* * Data Abort Exception leaves Exception Link Register pointing to * instruction that caused Data Abort. For testing purposes we adjust * ELR_EL3 to make it pointing to next instruction */ lVal = bswap_64(_get_far_el3()); uart_output_hex(uart_base, (uint8_t*)&lVal, 0, 8); uart_output_string(uart_base, "\r\nMemory operation attempt: "); uart_output_string(uart_base, _ESR & (1 << ISS_DA_WNR_BIT) ? "WR" : "RD"); uart_output_string(uart_base, "\r\nAdjusted ELR_EL3 forward (4)\r\n"); _adjust_elr_el3(4); uart_output_string(uart_base, "\r\n"); } else /* Unhandled Exception */ { uart_output_string(uart_base, "UNHANDLED: EC: "); uart_output_bin (uart_base, _ESR, EC_SHIFT, EC_LENGTH); uart_output_string(uart_base, "\r\nELR Values: "); lVal = bswap_64(_ELR); uart_output_hex(uart_base, (uint8_t*)&lVal, 0, 8); uart_output_string(uart_base, "\r\nESR Values: "); lVal = bswap_64(_ESR); uart_output_hex(uart_base, (uint8_t*)&lVal, 0, 8); uart_output_string(uart_base, "\r\nAdjusted ELR_EL3 forward (4)\r\n"); _adjust_elr_el3(4); uart_output_string(uart_base, "\r\n"); } return; }

Let's take a closer look at functionality of this routine. First, it outputs Exception Group — the exact number of a section in Vector Table we've got our exception to (or from — depends on point of view). Second, it outputs number of ALU Core on which exception arised. After that goes parsing of the data we've gathered in our _exception_entry routine and output of information about exception being arised. The information used in this analysis is stored in ESR — Exception Syndrome Register. We get Exception Class field of ESR. This is the top-level starting point in handling of any exception — all exceptions are divided and distinguished from each other by EC. The first exception that is processed is SMC. We output its immediate value. We'll examine the nature of SMC instruction a little later. The second exception being parsed is Data Abort. If you've skimmed through the text of exception_handler() routine, you could notice that there is no Data Alignment section in EC. That's right. Data Abort and Data Alignment both belong to one EC. We decide if it Access or Alignment fault by another field of ESR — DFSC. DFSC stands for Data Fault Status Code. By examining this field we know if it was Access or Alignment fault. But what we want to see if we get Data Access or Alignment fault? We want to see two things — the address the attempt to access was made to and the type of access — was it read or write. Thus, here we get FAR — Fault Address Register, register that contains the exact memory address the software attempted to access. And via Write Not Read bit field we get type of operation that lead to exception. Usually, both Data Abort exceptions are used to correct things — to get page from swap, for example. Because of that, ARMv8 ALU leaves Exception Link Register (the register containing address where the program flow will continue after exception handler exit) containing address of the exact instruction that was trying to access memory instead of address of next instruction. We should fix problems with memory and return from exception and ALU should re-execute the same instruction. But in our case, for our small learning and researching purposes, we'll solve this problem in a different manner. As ARMv8 has fixed instruction length we just adjust the ELR by 4 bytes (_adjust_elr_el3 — simple routine in assembly language). And after that manipulations the flow will continue (with some values received as result of memory access operation), while without adjusting ELR we would get into infinite loop of instruction and this exception. But where is the Invalid Instruction? On ARMv8 it falls to so called unhandled exception and we have little to display and to fix in this case. Just to let our code make its way ahead we adjust ELR forward. That all about exception handler. There is a lot more to explore, you see titles of documents and pages in comments — it's up to you to learn and play onward.

That's all for today. In next post we'll continue to learn and practice exceptions on ARMv8 machine.