CS452 - Real-Time Programming - Fall 2011
Lecture 6 - ARM Architecture
Pubilc Service Annoucements
Context Switch
Kernel Structure
The kernel is just a function called main( ), which runs
forever. It looks like this
main( ) {
initialize( ); // includes starting the first user task
FOREVER {
active = schedule( )
request = activate( active );
handle( request );
}
}
Where is the OS?
- requests come from running user tasks
- one type of request creates a task
- There needs to be a first task that gets everything going
All the interesting stuff inside done by the kernel is hidden inside
activate( active ).
schedule( ) returns the ready task of the highest priority or
nil.
- Task states: Ready, Active, Zombie
- Three possible state transitions
| Initial state |
Final state |
Trigger |
| Ready |
Active |
schedule( ) |
| Active |
Ready |
Pass( ) |
| Active |
Zombie |
Exit( ) |
All the interesting stuff inside done by the kernel is hidden inside
activate( active )?
- transfer of control to the active task
- execution to completion of the active task
- `to completion' means until the active task sends a request to the
kernel
- transfer of control back to the kernel
- getting the request
The hard part to get right is `transfer of control'
- which we call a context switch
Context Switch
The context switch is an examnple of the NOP design pattern, which is
extremely widespread in actual code. A common, and relevant, example occurs
when we call a function
Function Call (gcc calling conventions)
; In calling code
; store values of r0-r3
; load arguments into r0-r3
bl <entry point> ; this treats the pc and lr specially
; lr <- pc, pc <- <entry point>
; r0 has the return value
; r1-r3 have useless junk
; In called code
entry point:
mov ip, sp
stmdb sp!, {..., fp, ip, lr}
; fp stands in for all registers except scratch,
; determined by the registers the function uses
...
ldmia sp, {..., fp, sp, pc}
; exact inverse of bl, mov and stmdb
Note the role of the index pointer (ip), link register (lr) and stack
pointer (sp).
Software Interrupt
The above code sets up function execution within the context of the
calling task. When we want to run code that is in a different context we use
the software interrupt instruction
What happens when it's executed?
- r14_svc <- address of the following instruction. This is where the
kernel will return to.
- SPSR_svc <- CPSR. This saves the mode, condition codes, etc.
- CPSR[0:4] <- 0b10011. Supervisor mode.
- CPSR[5] <- 0. ARM (not Thumb) state.
- CPSR[7] <- 1. Normal interrupts disabled.
- PC <- 0x08
The CPU ignores the 24-bit immediate value, which can be used by the
programmer as an argument identifying the system call, for example.
; In calling code
; Store r0-r3
; Put arguments into r0-r3
; the kernel entry point must be in 0x028
swi n ; n identifies which system call you are calling
; retrieve return value from r0
; r1-r3 have even more useless junk
; In kernel
kernel entry:
; Change to system mode
; Save user state on user stack
; Return to supervisor mode
ldr r4, [lr, #-4] ; gets the request type
; at this point you can get the arguments
; Where are they?
; Retrieve kernel state from kernel stack
; Do kernel work
Questions:
- What is above kernel entry?
- If you put swi in a wrapper or stub what happens before and after
it?
- If the request had arguments, how would you get them into the kernel?
Hint. How does gcc pass arguments into a function?
- It might be important that there are two link registers. Which two link
registers? Why?
- In practice it isn't important. Why not?
- This is not the only way to do a context switch. But remember that
every correct implementation of a content switch contains either a
software interrupt (
swi ) or a hardware interrupt.
Suggestions:
- Try this first on paper drawing the stack, registers, etc after each
instruction
- Try coding in baby steps, which is usually a good idea in assembly
language.
Try reading this, which uses the NOP
design pattern repeatedly to create a context switch step-by-step, with every
step testable.
After the Software Interrupt
In the kernel
The order matters
kernel entry:
- State on entry
- supervisor mode
- interrupts off
- spsr = cpsr_usr
- arguments in r0-r3
- caller context in registers r4-r12
- caller local variables indexed off fp
- kernel stack pointer in r13
- address of instruction following swi in r14, i.e., lr_srv = return
address
- kernel entry in r15
- Change to system state
- Save the user state
- on its stack
- This might include scratch registers (arguments), which you may or
may not need later.
- Put
sp_usr in a scratch register, say r2
- Return to supervisor mode
- Get the request into a scratch register
ldr r3, [lr, #-4]
- Retrieve the kernel state, which should not include the scratch
registers
- You now have the kernel frame pointer
- You can use it to put stuff in kernel memory
- Put what you need to in the active task's TD
active is indexed off the kernel's frame pointer
active is a pointer to the TD of the requester
- Some where above you must have picked up the arguments
- must be done after 5. Why?
- must be done before 9. Why?
- Return from
getNextRequest( active ) and get to work
- Don't forget to store the return value in the TD when you're
finished
There is more than one way to do almost everything in this list, and I
have chosen this way of describing what is to be done because it's simplest
to describe, not because it's best!.
Before the Software Interrupt
After a while it's time to leave the kernel
- Schedule the next task to run
- i.e. get the value of
active
- Call
GetNextRequest( active )
Inside GetNextRequest
- Save kernel state on kernel stack
- From TD
- set spsr_svc = cpsr_usr
- set lr_svc = pc for execuation in user mode
- get sp_usr
- Set return value by overwriting r0 on user stack
- Switch to system mode
- Load registers from user stack
- Return to supervisor mode
- Let it go
movs pc, lr
The instruction after this one is normally the kernel entry.
Making the Stub that Wraps swi
For each kernel primitive there must be a function available in usr code:
the kernel's API.
- e.g.
int Create( int priority, void ( *code ) ( ) );
What gcc does for you
Before calling Create
- gcc saves the scratch registers to memory.
- gcc puts the arguments into the scratch registers, and possibly on the
stack.
While calling Create
bl to the entry point of Create
While executing Create
- gcc saves the registers that it thinks will be altered
during execution of the function.
- gcc thinks wrong, because only the assembler knows that swi is in
the instruction stream
- your code gets executed
- gcc restores the registers it saved, and only those registers.
Exiting from Create
- mov pc, lr, or equivalent, is executed, returning the execution to the
instruction following bl
After calling Create
- gcc stores register r0, the return value, in the variable, to which the
result of Create is assigned.
What the code you write does
- Moves the arguments from gcc's locations to whatever convention you
choose for your kernel
- Does swi n, where n is the code for Create.
- Moves the return value from your kernel's conventional location to
r0.
Creating a Task
In creating a task you have to do two things
- Get and initialize resources needed by the task
- Make the task look as if it had just entered the kernel
- it's ready to execute when it's scheduled
Things you need to do
Get an unused TD and memory for its stack
- memory could be associated with TD during initialization
- actually a form of constant time memory allocation
- unless you implement Destroy
Mostly filling in fields in the TD.
- task id
- stack pointer
- SPSR
- link register
- parent tid
- return value
- dummy
- different return value for the active task, which goes in its
TD
- state
- install in the ready queues
Must also initialize the stack
The Create Function
You also need a int Create( int priority, void (*code) ( ) )
function to call from user tasks.
Although it's no more than a wrapper there are a few problems to solve.
- Passing arguments
- On entry the arguments are somewhere, usually r0 & r1
- You have to put them where the kernel can find them.
- gcc's function extry code immediately puts them on the stack.
- In assembly you can find them using the frame pointer.
- Jumping into the kernel
- Getting the return value from the kernel and returning it.
- You find it where the kernel put it
- gcc's function exit code expects it to be indexed off the frame
pointer
- from where it does into r0
What follows just seems to say the same thing again. But we might as well
leave it here because some student might find it useful.
How do we implement the API for user code?
- This requires a bit of assembly language.
- In assembly language all arguments & return values are words.
- usually pointers
- stored in registers
- What happens when there are too many arguments?
- What happens when
Create is called?
- Two arguments
- stored in
r0 and r1
- which is which?
- Immediately placed on the stack, referred to by
fp
- Create puts them in the special place you choose
swi switches context into the kernel.
What happens when Create returns?
- When the kernel is finished manipulating
TDs and
PQs
- it knows the tid to be returned
- it places the tid in the caller's return value in its
TD
When the caller is next activated,
- as part of activation the return value is put in your special place,
usually
r0
user code has to get it and put it in the compiler's special place, r0 for
gcc.
Return to: