In this lab, we will implement the ability to run custom programs. Those. processes and all dependent infrastructure. In the beginning we will figure out how to switch from privileged code, how to switch process contexts. Then we implement a simple round-robin scheduler, system calls and virtual memory management. In the end, we will derive our shell from kernel space to user space.
First Laba: the younger half and the older half
The second laba: the younger half and the older half
As in the previous parts, for guaranteed work it is required:
In the 3-spawn
turnip, there is nothing but questions, but no one interferes with stealing:
git clone https://web.stanford.edu/class/cs140e/assignments/3-spawn/skeleton.git 3-spawn
After that, when it’s useless, the directory structure should look something like this:
cs140e ├── 0-blinky ├── 1-shell ├── 2-fs ├── 3-spawn └── os
But inside the os
-repy it will still be necessary to switch to the 3-spawn
branch:
cd os git fetch git checkout 3-spawn git merge 2-fs
Most likely you will again see merge conflicts. Something like this:
Auto-merging kernel/src/kmain.rs CONFLICT (content): Merge conflict in kernel/src/kmain.rs Automatic merge failed; fix conflicts and then commit the result.
Conflicts of merge need to be resolved manually by changing the file kmain.rs
. In doing so, you need to make sure that you saved all your changes from labs 2. After the conflicts have been resolved, add the git add
files and commit it all. In order to get more information on this topic - see the tutorial on githowto.com .
In this task, we will constantly refer to three official documents on ARM. These three:
I highly recommend that you download these manuals to your disk. So it will be easier to open them every time. Especially the first because it is very, very large. By the way about this.
How do you even read this? We do not need to read it completely. Therefore, it is extremely important to start to know what we want to find in this manual. This manual has a good fit structure. It is divided into several parts. We are interested in AArch64 and are not interested in too deep immersion (we are not processor manufacturers). So we are not interested in many chapters from the word at all. In fact, parts of A, B and some information from C and D are enough for us. In the first two parts, the general concepts are described as applied to architecture and to AArch64 in particular. Part C describes the instruction set. We will use this part as reference for the most basic instructions and registers (for example, SIMD does not interest us now). Part D describes some details of the AArch64. In particular, about interrupts and all that.
In this phase, we will study the ARMv8 architecture, switch to a less privileged level, tweak processor exception vectors, handle timer interrupts and breakpoints interrupt. Examine the exception levels in the ARM architecture. We are mainly interested in how to catch these exceptions and interrupts.
In this subphase, we will study the architecture of ARMv8. Here we will not write any code, but there are questions for self-examination.
ARM (Acron RISC Machine) is a microprocessor architecture with more than 30 years of history. Currently there are eight versions of this architecture. The latest ARMv8 was introduced in 2011. The Broadcom BCM2837 chip contains the ARM Cortex-A53 cores, which are ARMv8.0 based cores. Cortex-A53 (and the like) is an implementation of the architecture. And this is the realization that we will study in this whole part.
ARM microprocessors dominate the mobile market.
ARM is approximately 95% of the global smartphone market and 100% of the flagship smartphones. Including Apple iPhone or Google Pixel.
So far, we have tried to avoid issues related to the processor architecture. Rust did everything for us. In order to work with us in usspaces, we will need to carry out a certain amount of work at a low level. Programming directly proceeds will require familiarization with the assembler of this architecture and with all related concepts around it. We'll start with an overview of the architecture and deal with the most basic assembler instructions.
The ARMv8 architecture has the following registers ( ref : D1.2.1):
r0
... r30
- 64-bit general purpose registers. Access to registers is carried out by pseudonyms (aliases). The registers x0
... x30
are aliases for the 64-bit version (ie, full). There are aliases w0
... w30
. The latter access the lower 32 bits of the register.lr
- 64-bit reference register. Alias ​​for x30
. Used to store a jump address. The bl <addr>
instruction saves the current command counter (PC) to lr
and goes to the address addr
. The ret
work will be done by the ret
instruction. She will take the address from lr
and assign it to the PC.sp
- stack pointer. The lower 32 bits are available by wsp
. The stack pointer should always be aligned by 16 bytes.pc
- program counter. This register can not be written directly, but can be read. It is updated on the transition instructions, when calling interrupts, when returning.v0
... v31
- 128-bit SIMD and FP registers. These are used for SIMD vector operations and for floating point operations. These registers are available by alias. q0
... q31
- aliases for all 128 bits of the register. Registers d0
... d31
are the lower 64 bits. Besides, there are aliases for the lower 32, 16 and 8 bits in the prefixes s
, h
and b
respectively.xzr
is a null case. This is a pseudo-register, which may or may not be a hardware register. Always contains 0
. This register can only be read.There are many more special-purpose registers . We will talk about them a bit later.
At any time, the percent ARMv8 allows you to access the program status through a pseudo-register named PSTATE ( ref : D1.7). This is not an ordinary case. It cannot be read or written to it directly. Instead, there are several special-purpose registers that can be used to operate on parts of the PSTATE pseudo-register. On ARMv8.0 this is:
NZCV
status flagsDAIF
- bit mask of exceptions, which is used to enable and disable these exceptions.CurrentEL
- the current level of exceptions (to be described later)SPSel
- stack pointer selector (there are actually several)Similar registers belong to the class of system or special registers ( ref : C5.2). Regular registers can be read from RAM with ldr
or written to memory with str
. System registers cannot be used this way. Instead, you need to use special commands mrs
and msr
( ref : C6.2.162 - C6.2.164). For example, in order to read the NZCV
in x1
we should use the following entry:
mrs x1, NZCV
At any point in time, ARMv8 percent is executed with a certain execution state. In total, there are exactly two such states. AArch32 - compatibility mode with 32-bit ARMv7. And AArch64 - 64-bit ARMv8 mode ( guide : 3.1). We will only work with AArch64.
At any given time, our percents are executed with a certain security state (guide: 3). This garbage can also be searched for in security mode or security world. Only two states: secure and non-secure . Those. safe and ordinary. We will work entirely as usual.
Besides, there are also exception levels ( guide : 3). Each level of exceptions corresponds to a certain level of privilege. The higher the level of exceptions, the more privileges the program starts at this level. There are 4 levels in total:
The Raspberry Pi processor is loaded into EL3. At this stage, the firmware is launched, provided by the Raspberry Pi foundation. The firmware switches the processor to EL2 and runs our kernel8.img kernel8.img
. Thus, our core starts from EL2. A little later, we will switch from EL2 to EL1, so that our core works at the appropriate level of exceptions.
A number of system registers, such as ELR
, SPSR
and SP
, are duplicated for each exception level. At the same time, the suffix _ELn
is put to their names, where n
is the level of exceptions to which this register belongs. For example, ELR_EL1
is the exception reference register for EL1, and ELR_EL2
is the same, but for EL2.
We will use the x
suffix (for example in ELR_ELx
) when we need to refer to a register from the target exception level x
. The target exception level is the exception level to which the CPU switches (if necessary) when the exception vector is triggered.
We will use the suffix s
(for example, in SP_ELs
, when you need to refer to the register in the original exception level s
. The original exception level is the exception level at which the CPU was executed before the exception occurred.
There is exactly one mechanism for increasing the level of exclusion and exactly one mechanism for reducing the level of exclusion.
To switch from a higher level to a lower level (privilege reduction), the running program must perform a return (return) from this level of elimination using the eret
( ref : D1.11). When executing the eret
for an ELx
processor level:
ELR_ELx
.SPSR_ELx
.The SPSR_ELx
register ( ref : C5.2.18), besides, contains the level of exceptions to which it is necessary to go. In addition, you should pay attention to the following additional effects of changing the levels of exceptions:
ELs
, sp
set to SP_ELs
if SPSR_ELx[0] == 1
or to SP_EL0
if SPSR_ELx[0] == 0
.The transition from a lower level to a higher one occurs only as a result of an exception ( guide : 10). If not configured otherwise, the percents will intercept the exceptions for the next level. For example, if an interrupt is received while running in EL0, then the percent will switch to EL1 to handle the exception. When switching to ELx
percent will do the following:
PSTATE.DAIF = 0b1111
off (disguises) all exceptions and interrupts: PSTATE.DAIF = 0b1111
.PSTATE
save PSTATE
and any in SPSR_ELx
.ELR_ELx
( ref : D1.10.1).sp
to SP_ELx
if SPSel
is 1
.ESR_ELx
( ref : D1.10.4).pc
to the address corresponding to the exception vector (we will describe it a bit later).Please note that the exception syndrome register is valid only for synchronous exceptions. All general registers and SIMD / FP registers will contain the values ​​that they had when an exception occurred.
When exceptions occur, the CPU transfers control to the place where the exception vector is located ( ref : D1.10.2). There are 4 types of exceptions, each of which contains 4 possible sources of exceptions. Those. total 16 exception vectors. Here are four types of exceptions:
svc
or brk
. Well, in general, for any events in which the programmer is guilty.Here are four interrupt sources:
SP = SP_EL0
SP = SP_ELx
From the description of the manual ( guide : 10.4):
When an exception occurs, the processor must execute the handler code that corresponds to the exception. The memory location where the handler [exceptions] is stored is called the exception vector. In the ARM architecture, exception vectors are stored in a table called the exception vector table. Each exception level has its own vector table, that is, for each of EL3, EL2, and EL1. The table contains instructions for execution, not a set of addresses [as in x86]. Each record in the vector table has a size of 16 instructions. Vectors for individual exceptions are arranged with fixed offsets from the beginning of the table. The virtual address of each table is based on the [special] vector address registersVBAR_EL3
,VBAR_EL2
andVBAR_EL1
.
These vectors are physically located in memory as follows:
Current exception level with SP = SP_EL0
Offset from VBAR_ELx | An exception |
---|---|
0x000 | Synchronized exception |
0x080 | IRQ |
0x100 | FIQ |
0x180 | Seror |
The current level of exceptions when SP = SP_ELx
Offset from VBAR_ELx | An exception |
---|---|
0x200 | Synchronized exception |
0x280 | IRQ |
0x300 | FIQ |
0x380 | Seror |
Lower level exceptions at which AArch64 is running
Offset from VBAR_ELx | An exception |
---|---|
0x400 | Synchronized exception |
0x480 | IRQ |
0x500 | FIQ |
0x580 | Seror |
Lower level exceptions at which AArch32 is running
Offset from VBAR_ELx | An exception |
---|---|
0x600 | Synchronized exception |
0x680 | IRQ |
0x700 | FIQ |
0x780 | Seror |
Currently, this is all we need to know about the ARMv8 architecture. Before continuing, try to answer these questions. For self test.
What are the aliases for registerx30
? [arm-x30]
If we write0xFFFF
to thex30
register, what other two names of this register can we use to retrieve this value?
')
How can I change the PC value to a specific address? [arm-pc]
How can I set the PC to addressA
using theret
instruction? How to set PC to addressA
usingeret
instruction? Specify which registers you will change in order to achieve this.
How can I determine the current level of exceptions? [arm-el]
What specific instructions would you follow to determine the current level of exclusion?
How would you change the stack pointer to return an exception? [arm-sp-el]
The stack pointer of the running program isA
at the time the exception occurred. After processing the exception, you want to go back to where the program was running, but you want to change the stack pointer toB
How do you do this?
Which vector is used for system calls from a lower EL? [arm-svc]
The user process is performed on EL0. This process callssvc
. What address will be transferred to management?
Which vector is used for interrupts from a lower EL? [arm-int]
The user process is performed on EL0. At this point, a timer interrupt occurs. What address will be transferred to management?
How can I enable IRQ exception handling? [arm-mask]
In which register, what values ​​need to be written in order to unlock IRQ interrupts?
How would you useeret
to enable AArch32 mode? [arm-aarch32]
The source of the exception is AArch64. The handler for this exception is also on AArch64. What values ​​in which registers would you change so that when returning from an exception viaeret
percent switches to the AArch32 execution mode?
Hint : Watch ( guide : 10.1)
In this subphase, we will explore the most basic commands from the ARMv8 command set. We will not write the code right now, but there are a couple of questions for self-testing.
ARMv8 is a set of RISC download / storage instructions (a computer with a reduced instruction set). The defining feature of such a set of commands can be called the small fact that memory access can only be done through clearly defined instructions. In particular, the memory can only be read by reading into the register by the loading instruction, and it can only be written by the save instruction.
There are many instructions for loading / unloading (load / store) in different variations (for the most part they are of the same type). Let's start with the simplest form:
ldr <ra>, [<rb>]
: loads the value from the <rb>
address into <ra>
.str <ra>, [<rb>]
: saves the value of <ra>
to an address from <rb>
.The <rb>
register is called the base register . For example, if r3 = 0x1234
, then:
ldr r0, [r3] // r0 = *r3 ( , r0 = *(0x1234)) str r0, [r3] // *r3 = r0 ( , *(0x1234) = r0)
In addition, you can add an offset from the interval [-256, 255]
:
ldr r0, [r3, #64] // r0 = *(r3 + 64) str r0, [r3, #-12] // *(r3 - 12) = r0
You can also specify a post index that changes the value in the base register after applying the load or saving:
ldr r0, [r3], #30 // r0 = *r3; r3 += 30 str r0, [r3], #-12 // *r3 = r0; r3 -= 12
Or pre-index, which will change the value in the base register before applying the load or save:
ldr r0, [r3, #30]! // r3 += 30; r0 = *r3 str r0, [r3, #-12]! // r3 -= 12; *r3 = r0
Offset, post-index and pre-index, they are known as addressing modes .
In addition, there is another command that can load / unload two registers at once. ldp
and stp
instructions (load pair, store pair). These instructions can be used with the same addressing modes as ldr
and str
.
// `x0` `x1` . : // // |------| <x ( SP) // | x1 | // |------| // | x0 | // |------| <- SP // stp x0, x1, [SP, #-16]! // `x0` `x1` . : // // |------| <- SP // | x1 | // |------| // | x0 | // |------| <x (original SP) // ldp x0, x1, [SP], #16 // , sub SP, SP, #16 stp x0, x1, [SP] ldp x0, x1, [SP] add SP, SP, #16 // , x0, x1, x2, x3. sub SP, SP, #32 stp x0, x1, [SP] stp x2, x3, [SP, #16] ldp x0, x1, [SP] ldp x2, x3, [SP, #16] add SP, SP, #32
The immediate (immediate) value is another name for an integer whose value is known without any computation. In order to load (for example) the 16-bit immediate into the register, optionally moving it a certain number of bits to the left, we need the command mov
(move). In order to load the same 16 bits with a shift, but without replacing the other bits, we need movk
(move / keep). Here is an example of using all of this:
mov x0, #0xABCD, LSL #32 // x0 = 0xABCD00000000 mov x0, #0x1234, LSL #16 // x0 = 0x12340000 mov x1, #0xBEEF // x1 = 0xBEEF movk x1, #0xDEAD, LSL #16 // x1 = 0xDEADBEEF movk x1, #0xF00D, LSL #32 // x1 = 0xF00DDEADBEEF movk x1, #0xFEED, LSL #48 // x1 = 0xFEEDF00DDEADBEEF
Please note that the values ​​themselves are prefixed with #
. LSL
at the same time means a shift to the left.
Only 16 bits can be loaded into the register with an optional offset. By the way, the assembler can in many cases determine the necessary shift itself. mov x12, #(1 << 21)
mov x12, 0x20, LSL #16
.
<label>:
:
add_30: add x1, x1, #10 add x1, x1, #20
, , adr
ldr
:
adr x0, add_30 // x0 = add_30 ldr x0, =add_30 // x0 = add_30
ldr
. adr
.
, , mov
:
mov x13, #23 // x13 = 23 mov sp, x13 // sp = 23, x13 = 23
ELR_EL1
/ mrs
msr
.
, - msr
:
msr ELR_EL1, x1 // ELR_EL1 = x1
- mrs
:
mrs x0, CurrentEL // x0 = CurrentEL
add
sub
:
add <dest> <a> <b> // dest = a + b sub <dest> <a> <b> // dest = a - b
For example:
mov x2, #24 mov x3, #36 add x1, x2, x3 // x1 = 24 + 36 = 60 sub x4, x3, x2 // x4 = 36 - 24 = 12
<b>
:
sub sp, sp, #120 // sp -= 120 add x3, x1, #120 // x3 = x1 + 120 add x3, x3, #88 // x3 += 88
and
orr
AND
OR
. add
sub
:
mov x1, 0b11001 mov x2, 0b10101 and x3, x1, x2 // x3 = x1 & x2 = 0b10001 orr x3, x1, x2 // x3 = x1 | x2 = 0b11101 orr x1, x1, x2 // x1 |= x2 and x2, x2, x1 // x2 &= x1 and x1, x1, #0b110 // x1 &= 0b110 orr x1, x1, #0b101 // x1 |= 0b101
(Branching) — . PC . , b
:
b label // jump to label
( lr
), bl
. ret
lr
:
my_function: add x0, x0, x1 ret mov x0, #4 mov x1, #30 bl my_function // lr = `mov x3, x0` mov x3, x0 // x3 = x0 = 4 + 30 = 34
br
blr
b
bl
, , :
ldr x0, =label blr x0 // bl label br x0 // b label
cmp
. , bne
(branch not equal), beq
(branch if equal), blt
(branch if less than) .. ( ref : C1.2.4)
// 1 x0 , x1, // `function_when_eq`, not_equal: add x0, x0, #1 cmp x0, x1 bne not_equal bl function_when_eq exit: ... // x0 == x1 function_when_eq: ret
:
cmp x1, #0 beq x1_is_eq_to_zero
: , .
ARMv8 . , . ( ref : C1.2.4). . ISA- Griffin Dietz. , :
memcpy
ARMv8? [arm-memcpy]
,x0
, ,x1
,x2
( 8 ).memcpy
? ,ret
: 6-7 .0xABCDE
ELR_EL1
? [arm-movk]
,EL1
,0xABCDE
ELR_EL1
ARMv8?
: .cbz
? [arm-cbz]cbz
( ref : C6.2.36). ? ?init.S
? [asm-init]os/kernel/ext/init.S
— , ._start
0x80000
. , EL1 .os/kernel/ext/init.S
context_save
. , , - , , . (“read cpu affinity”, “core affinity != 0”) - :MPIDR_EL1
( ref : D7.2.74) (Aff0
), , . —setup
.wfe
.
: / , .
EL2 EL1. os/kernel/ext/init.S
os/kernel/src/kmain.rs
. , .
aarch64
( os/kernel/src/aarch64.rs
), . sp()
. current_el()
, . , EL2 . , kmain()
. , current_el()
unsafe
. , , EL1.
, EL1. os/kernel/ext/init.S
:
// FIXME: Return to EL1 at `set_stack`.
:
mov x2, #0x3c5 msr SPSR_EL2, x2
, . , , SPSR_EL2
eret
.
, FIXME
. , EL1 CPU set_stack
, . . , — eret
. , current_el()
1
.
: PC ?
. , . , , brk #n
. kernel/ext/init.S
kernel/src/traps
.
, 16 , 16 . init.S
_vectors
. , 16 , handle_exception
Rust kernel/src/traps/mod.rs
. handle_exception
. , , .
handle_exception
, Rust, , . , , info
, esr
tf
, .
, ( 2 C Rust). , , , . — , :
r0
… r7
.r0
… r7
.r19
... r29
SP
— callee-saved . — caller-saved . , lr
( x30
) . SIMD/FP . , caller-saved .lr
, . ret
lr
.AArch64 ( guide : 9) procedure call standard .
Rust- handle_exception
, , .
Rust , ?
, . Rust . , Rust ,extern
.handle_exception
extern
, Rust .
, , with HANDLER(source, kind)
, . HANDLER(a, b)
"", , #define
. Those. :
_vectors: HANDLER(32, 39)
:
_vectors: .align 7 stp lr, x0, [SP, #-16]! mov x0, #32 movk x0, #39, LSL #16 bl context_save ldp lr, x0, [SP], #16 eret
lr
x0
x0
32- 16 source
16 kind
. context_save
, _vectors
. , , lr
x0
.
context_save
. ret
context_restore
. context_save
, Rust.
(, ), ( ESR_ELx
) ( ref : D1.10.4). kernel/src/traps/syndrome.rs
. Syndrome
-. , ESR_ELx
Rust esr
. Sydnrome::from(esr)
, , .
handle_exception
Info
. 16 : source
kind
. , 32- , HANDLE
x0
. , HANDLE
- , Info
.
. , — brk
, .. . , , .
unsafe { asm!("brk 2" :::: "volatile"); }
:
_vectors
HANDLE
. , Info
.handle_exception
context_save
.0
tf
. .SP
16 , /. , .VBAR
, : // FIXME: load `_vectors` addr into appropriate register (guide: 10.4)
handle_exception
, .handle_exception
info
esr
, , . . , , , aarch64::nop()
. , . .Syndrome::from()
Fault::from()
.brk 12
Syndrome::Brk(12)
, svc 77
Syndrome::Svc(77)
. , 32- , , .brk
.Syndrome::from()
handle_exception
, brk
. , . . , Syndrome::from()
. ESR_ELx
.exit
. exit
, . brk
. shell()
kmain
loop { }
, ., brk 2
kmain
Brk(2)
, source, CurrentSpElx
kind Synchronous
. . exit
, .
, , . , , svc 3
. , .
As soon as everything works as you expected, you are ready to move on to the next stage.
UPD : the next part
Source: https://habr.com/ru/post/353994/
All Articles