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).