16/12/2025

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

Generate Data Abort exception

It's time to play. Let's generate Data Abort exception. Aborts of both read and write types are done in init_board() function (barium.c file) in exactly the same manner as mentioned above:


/* Raise Data Abort Exceptions (Write and Read) */ raw_writel(0, INVALID_ADDRESS); raw_readl(INVALID_ADDRESS);

And check the output:


Exception: 04, ALU Core №: 0 Class: DAF: FFFFFFFFFFFFFFFF Memory operation attempt: WR Adjusted ELR_EL3 forward (4) Exception: 04, ALU Core №: 0 Class: DAF: FFFFFFFFFFFFFFFF Memory operation attempt: RD Adjusted ELR_EL3 forward (4)

We see exception group, ALU Core number, exception class (DAF — Data Abort Fault), address the attempt to access was made to (FFFFFFFFFFFFFFFF) and operation type — WR or RD.

Generate Data Alignment exception

To achieve this goal we'll write a small routine in assembly language — _fetch_mem (see it in a newly added file low_level.s) which looks like:


.globl _fetch_mem _fetch_mem: # According to ABI x0 is the first parameter and # the return value, thus all we have to do is just: ldr x0, [x0] ret

and is declared in barium.c file as:


extern uint64_t _fetch_mem(uint64_t _Addr);

Let's test this function with some correct input value — our IMAGE_LOAD_ADDR is the good one:


lVal = _fetch_mem(IMAGE_LOAD_ADDR);

And see what we get:


Read Value: 24F239D584007AB2

We know that there should be ARMv8 opcodes. Let's check that by decoding the values we've just got:


mrs x4, s3_1_c15_c2_1 orr x4, x4, #0x40

This looks very familiar. Let's recall where we've seen this before — our very first instructions from start_64.s file:


.globl _start_64 _start_64: # Load CPU Extended Control Register: mrs x4, S3_1_C15_C2_1 # Enable SMP: orr x4, x4, #(1 << 6)

This means that our _fetch_mem routine works properly and we can proceed to raising Alignment Fault exception. We'll do this with:


lVal = _fetch_mem(0xDEAD);

And see what we get:


Exception: 04, ALU Core №: 0 Class: DAL: 000000000000DEAD Memory operation attempt: RD Adjusted ELR_EL3 forward (4) Read Value: ADDE000000000000

We see exception group, ALU Core number, exception class (DAL — Data Alignment Fault), address the attempt to access was made to (000000000000DEAD) and operation type — WR or RD.

Generate Invalid Instruction exception

For this purpose we have a small routine _udf (low_level.s):


.globl _udf _udf: udf #0 ret

The output we see is: 


Exception: 04, ALU Core №: 0 Class: UNHANDLED: EC: 000000 ELR Values: 0000000000921820 ESR Values: 0000000002000000 Adjusted ELR_EL3 forward (4)

Here we output some information but it's almost useless.

Generate exception by executing SMC instruction

After setting VBAR and implementing handler for SMC exception we can place SMC instruction in our code and it will generate exception and ALU will select corresponding handler and run it by just branching to its address. Let's review the SMC instruction itself. Its format is:


smc #imm16

#imm16 means that instruction takes so called 16-bit «immediate value» — a value that can be obtained during compilation only — a number itself or a #define. In such case we cannot use register as a parameter to such instruction. ARMv8 instructions are 32-bit long and consist of opcode and its parameters. During translation of assembly code into machine code, assembler forms code of this instruction using specified immediate value. That's what we have about SMC instruction and its nature.

We can obtain all 16 bits of this value later in handler from exception syndrome — it was shown in our exception_handler() in previous part of this post. But how can we specify this parameter when we need to pass different values? For example, we want to pass some information via this parameter to our handler. It looks like this parameter is designed exactly for that purposes but it is not usable because it is «immediate value». Well, yes, it is probably not usable for that purposes. Of course, we can implement a big block switch/case of if/else which would look like:


if (a) smc #1 else if (b) smc #2 ... else smc #0xFFFF

But this would be enormous block of boring code. But we don't like big blocks of boring code and we used to do something exceptional in our posts. Okay, let's not make exceptions in this today. We will present a method to pass variable argument to SMC instruction at run-time and it will be a small piece of code.

How can we do that? We'll do this by forming out the SMC instruction itself with specified immediate value, write it to some memory address and execute it — we will implement a function that will do a part of assembler's work but in run-time. In case we need to perform SMC instruction we will branch to some function that forms SMC instruction and after that executes it. This is done in smc.s in _smc function. What is actually done here? First we form SMC 0 instruction — it will be the base for the one we need. Its opcode is D4000003h. Then we cut 16 bits off of parameter of _smc function (x0), shift it by 5 — this is the exact offset of #imm16 in SMC instruction, and add this value to base opcode we've formed above. That's all about forming opcode of SMC instruction with given #imm16 as a parameter. After that we store this opcode in address of _smc_instruction label. That could look like complete solution — just branch to (or fall to) _smc_instruction, but it would not work. And that is because of caches. We remember that we have turned them on already and that our application is so tiny that it fits in caches entirely. So, the code runs completely inside cache. Thus we need to force ALU to refetch our newly generated instruction. This is done by flushing caches — marking some memory address as outdated in cache. And now this is the last part. We flush caches and fall to our newly formed instruction (_smc_instruction) without branches because it is located right after _smc function. Below you can see the code:


.globl _smc _smc: # Form instruction - smc with given immediate value. # Form instruction SMC 0 - the base for desired one, # its opcode is D4000003h: mov x1, 0x0003 movk x1, 0xD400, lsl 16 # Ensure we have exact amount of bits we need - immediate is 16 bit long: and x0, x0, 0xFFFF # Shift the immediate value to position it takes in instruction: lsl x0, x0, #5 # Put the immediate value into instruction code by orring: orr x0, x0, x1 # Obtain the address we want to modify: ldr x1, =_smc_instruction # Write new instruction to destination address: str w0, [x1] # As we have caches enabled we have to mark memory region that # contains our newly generated instruction as outdated to # force ALU to refetch the instruction: adr x1, _smc_instruction # Flush (invalidate) D-caches: dc cvau, x1 dsb ish # Flush (invalidate) I-caches: ic ivau, x1 dsb ish isb

Now let's review the template of _smc_instruction function. Here we have a SMC instruction with base immediate value, which we took as 0. What we have to keep in mind here is that we still are in function at the moment — we didn't branch to it, but we've fallen to section _smc_instruction which was generated by _smc function from the last one. _smc_instruction contains our SMC instruction. After executing it, the ALU will run exception handler and, after returning from it, will run the instruction immediately following SMC. Thus we have to add ret instruction as part of _smc function.

Any function has some return value. What could we return from _smc? It could be interesting to return the opcode of generated instruction. That's exactly what we'll do. But we will not do any additional moving of data here because we already have our SMC opcode in x0 — which is the register that contains return value of a function (according to ABI). You can see this section below:


# Fall to newly formed instruction: _smc_instruction: # smc with default immediate value: smc 0x0000 # We keep in mind that we are still in function (_form_smc), which # is called from C-code. Thus, have to put ret here. Also we use the # return value for reviewing of instruction code we've generated. # At this moment it is stored in w0, thus we do not move any values. ret

Now we have code that causes exception — SMC with immediate value as given parameter. So it's time to put call to this routine somewhere with some parameter (immediate value). What could be interesting? How can we put it all together to get a nice result? We'll use symbols we get from UART as an immediate values for SMC and output its opcode:


loop_uart: /* Read from UART and output received bytes in a loop */ lVal = uart_get_char(uart_base); /* Form exception and raise it */ lVal = _smc(lVal); uart_output_string(uart_base, "Instruction opcode: "); uart_output_hex(uart_base, (uint8_t*)&lVal, 0, 4); uart_output_string(uart_base, "\r\n\r\n"); goto loop_uart;

Here's what we have as a result:


Exception: 04, ALU Core №: 3 Class: SMC, #imm Value: 0020 Instruction opcode: 030400D4

First two lines are from exception handler. The first of it outputs exception group, and core number on which exception occurred. The second outputs exception type (class) and its immediate value. Third line if from barium_main() function — it outputs the return value of _smc function which is the opcode of instruction we've generated.

We have collected initial VBARs from all ALU Cores. Let's see what we have:


ALU Core №: 0 Vector BAR: 0000000000000000 ALU Core №: 1 Vector BAR: 0000000000000000 ALU Core №: 2 Vector BAR: 0000000000000000 ALU Core №: 3 Vector BAR: 0000000000000000

What we see here? VBAR initially is set to zeros for all cores, as stated by datasheet (p.706 — Figure 6-2. Internal ROM and RAM memory map) — vectors are located at zero.

That's all for today. We've reviewed the basics of exception model of ARMv8 machine with templates and some practice. As we've mentioned in first part of this post — there is much more about exceptions and it's up to you to learn further. 

You can clone the final repository from Barium No-Boot (iMX8MP) (see «Stage IV» directory).