460 Notes #5
CS460 NOTES #5 Kernel/User Modes
1. Process Image:
The execution image of a process consists of 3 (logical) segments:
-----------------------------
| CODE | DATA | STACK |
-----------------------------
| ^ | | ^
CS | DS SS |
pc sp
Figure 1. Process Image
In theory, the 3 segments can be independent; each can be at a different memory
location, as long as they are pointed by the segment registers of CPU.
In practice, some of the segments may coincide. For example, in the combined
I and D space model, all segments are the same, i.e. CS=DS=SS all point at the
same segment. In the separate I and D space model, CS points to the I (code)
segment, but DS and SS point to the same Data+Stack segment.
For simplicity, we shall use the combined I and D space model in MTX so that
a process image has only ONE segment. The segment size is (up to) 64KB.
2. Kmode and Umode Images:
From now on we shall assume that a process may execute in (either one)
of two different modes, Kernel mode or User mode, denoted by Kmode or Umode
for short. While in Kmode, all processes share the same Code and Data segments,
which are those of the Kernel, but each process has its own Kstack. The Umode images
are in general all different, i.e. each process has a separate Umode memory area
consisting of its own Ucode, Udata and Ustack. Also, for simplicity, we shall assume:
MTX Kernel at (segment) 0x1000
8 processes (tasks), P1 to P8;
Each process has a 64KB Umode image in the segment 0x1000*(i+1), i.e.
P1 at 0x2000, P2 at 0x3000, etc. (We can remove this "FIXED SEGMENT"
restriction later when we introduce memory management).
Now, assume that
P5 is running in Umode NOW, P2 is READY but not running.
************************* Helpful Reviews ***********************************
Try to answer these questions. They will help you understand the MTX Kernel:
If P2 is not running but READY, where is its PROC? (i.e. in which Kernel data
structure?) How can P2 run again? When P2 runs again, where does it "resume"
to?
*****************************************************************************
The memory map is shown below:
-------------------------------------------------------------------------
|Vector | Kernel | | UImage2 | | | UImage5 |
-------------------------------------------------------------------------
^ 0x1000 ^ 0x3000 ^ 0x6000
| | |
kCS,kDS,kSS uCS,uDS,uSS
At this moment, the CPU's CS,DS,SS registers all point at P5's Umode image
at 0x6000. P5's ustack is somewhere in the upper region of UImage5.
When P5 enters Kmode, it will execute the Kernel image at 0x1000. The CPU's
CS,DS,SS registers MUST be changed to point at 0x1000, as shown by the
kCS,kDS,kSS in the diagram. In order for P5 to execute in Kernel, the stack
pointer (sp) must also be changed to point at the Kstack of P5. Naturally, P5
must save its Umode uCS,uDS,uSS and usp in order for it to return to UImage5
later.
While in Kmode, P5 may switch to P2. P2 may return to its Umode image at
0x3000. If so, P2 must load CPU's CS,DS,SS with its own uCS,uDS,uSS, all of
which point at 0x3000. P2's ustack is in the upper region of UImage2.
Similarly, you should be able to deduce what would happen when P2 enters Kmode
and switches to another process, ..... etc.
3. Transition between Umode and Kmode.
A process migrates between Umode and Kmode MANY times during its life time.
Although every process begins (comes into existence) in Kmode, we shall assume
that a process is already executing in Umode. This sounds like another
chicken-egg problem, but we can handle it easily.
A process in Umode will enter Kmode if any of these events occurs:
=====================================================================
(1). traps : divideByZero, IllegalInstruction, InvalidAddress,...
(2). Interrupts : timer interrupts, device I/O completion, etc.
(3). syscalls : INT # (or equivalent instructions on other CPUs).
=====================================================================
Traps and Interrupts will be covered later in the course. Here, we shall
consider only syscalls.
syscall is a mechanism that allows a Umode process to enter Kmode to execute
Kernel functions. syscalls are not ordinary function calls because they involve
CPU operating in different modes (U to K) and accessing different address
spaces. Assume that there are N Kernel functions, each corresponds to a
Type = 0,1,2, ..., N-1, e.g.
Type KernelFunction
----- --------------
0 getpid
1 fork
2 wait
...................
99 exit
A Umode process may call
int syscall(type, arg1, arg2, .... ) int type, arg1, arg2, ....;
which acts as an interface between U mode "calls" to the corresponding
Kernel functions. The implementation of syscall(), assuming 4 arguments,
is shown below:
|========================================================================
| syscall(a,b,c,d) int a,b,c,d; issue INT 80 to enter Kerenl
|========================================================================
_syscall:
int 80 <==== This is the magic wand!
ret
On the Intel CPU, syscalls are implemented as INT #, where # is a (byte) value.
Although we may use different INT numbers to implement different syscall
fucntions, it suffices to use one number: INT 80 (because the parameter a
indicates the syscall type number). The choice of 80 is quite arbitrary. You
may choose any number, as long as it is not used as IRQ or by BIOS.
When CPU executes the instruction INT 80, it performs the following actions.
--------------------
PUSH: push uFlag
clear T-bit,I-bit of flag register to 0
(trace off, mask off interrupts)
push uCS
push uPC
------------------------------------------------
LOAD: load (PC, CS) with CONTENTs of (4*80, 4*80+2);
------------------------------------------------
HANDLER: continue execution from the loaded (PC, CS) ===>
CPU enters INT80 handler in kernel's Code segment.
------------------------------------------------
Details of the PUSH, LOAD and HANDLER actions follow.
(1). PUSH:
INT # causes the CPU to push current uFlag, uCS, uPC into stack. On most other
machines, INT # causes the CPU to enter a separate Kmode, and switches stack
from ustack to kstack automatically. However, we are using the Intel CPU in
UNPROTECTED mode, which does not have a separate Kmode. After an INT 80, all
it does is to switch execution point from (uPC,uCS) to (kPC,kCS), which changes
the code segment from UCode to KCode. All other segments (DS and SS) and CPU
registers are still those of the Umode. Thus, when CPU first enters the
INT80 handler, the stack is still ustack in the uSS segment. It contains
uFlag, uCS, uPC at the top:
--------------------------------
///////|uFlag| uCS | uPC |
--------------------------------
^
|
High usp Low
Corresponding to INT #, the instruction IRET pops 3 items off the current
stack into CPU's PC,CS,Flag registers, in that order. It is used to return from
interrupt handler (in Kmode) to Umode.
(2). LOAD
After an INT 80, CPU loads PC, CS from memory locations corresponding to the
vector number 80. Naturally, the vector 80 locations MUST be initialized
properly first, such as
int int80h(); /* handler function prototype */
set_vector(80, int80h); /* 80*4=kPC=int80h, 80*4+2=kCS=0x1000 */
(3). HANDLER:
The INT80 handler is shown below:
|=======================================================================
| int80h() is the entry point for INT 80 interrupts
|=======================================================================
_int80h:
! (1). SAVE Umode registers in ustack, save uSS,uSP in running task's PROC,
! Accordingly, we modify the proc structure as follows.
! typedef struct proc{
! struct proc *next;
! int *ksp; /* offset = 2 bytes */
! int uss, usp; /* ADD 2 items at offset 4,6 */
! int pid;
! int ppid;
! int status;
! int pri; /* scheduling priority */
! int event; /* sleep event */
! char name[32]; /* name string of task */
! int kstack[SSIZE]; // task's Kernel mode stack
}PROC;
! int procSzie = sizeof(PROC): // a global imported in assembly
! The added fields (uss, usp) are for saving (uSS, uSP).
! (2). Change to running task's kstack HIGH END (see how below)
! Then, call handler function in C
! (3). RETURN to Umode
! Details of these steps follow.
! **************** SAVE U mode registers *********************************
push ax ! save all Umode registers in ustack
push bx
push cx
push dx
push bp
push si
push di
push es
push ds
! ustack contains : flag,uCS,uPC | ax,bx,cx,dx,bp,si,di,ues,uds
! !
uSP
! ************** Change DS,SS,sp to K space *****************
! change DS to KDS in order to access data in K space
push cs ! push kCS (0x1000)
pop ds ! let DS=CS = 0x1000
USS = 4
USP = 6
! NOTE: All variables are now relative to DS=0x1000 of Kernel space
! save running task's U mode uSS and uSP into its PROC
mov si,_running ! ready to access running PROC in K space
mov USS(si),ss ! save uSS in proc.uss
mov USP(si),sp ! save uSP in proc.usp
! change ES SS to K space segment 0x1000
mov di,ds ! stupid CPU must mov segments this way!
mov es,di
mov ss,di ! SS is now KSS = 0x1000
! We are now ready to switch (running task's) stack from U space to K space.
mov sp,si ! sp points at running proc
add sp,_procSize ! sp -> HIGH END of running's kstack[]
! We are now completely in K space, and stack is running task's (empty) kstack
! ************* CALL handler in C ******************************
call _kcinth ! call kcinth() in C;
! ************* RETURN TO U Mode ********************************
_goUmode:
cli ! mask out interrupts
mov bx,_running ! bx -> proc
mov cx,USS(bx)
mov ss,cx ! restore uSS
mov sp,USP(bx) ! restore uSP
pop ds
pop es
pop di
pop si
pop bp
pop dx
pop cx
pop bx
pop ax ! NOTE: return value must be in AX in ustack
iret
=============================================================================
The assembly function goUmode() restores Umode stack, then restores Umode
registers, followed by an IRET, causing the process to return to Umode.
These assembly functions are keys to understanding Umode/Kmode transitions.
STUDY THEM CAREFULLY and make sure you know what they do.
*****************************************************************************
4. Handler Function in C
kcinth(){.............}
is the syscall handler function in C. The parameters used in
syscall(a,b,c,d)
are still in ustack, which contains
LOW High
| uds,ues,udi,usi,ubp,udx,ucx,ubx,uax |upc,ucs,uflag|retPC, a, b, c, d |...
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
usp
The segment of ustack is saved in proc.uss, and usp is saved in proc.usp
Use the get_byte()/put_byte() functions (given below) to implement YOUR own
get_word()/put_word() functions, where
int get_word(segment, offset) ushort segment, offset;
returns a 2-byte word from (segment, offset), and
int put_word(word, segment, offset) ushort word, segment, offset;
writes a 2-byte word to (segment, offset).
In both cases, (segment, offset) MUST be an even address
With these functions, you can easily copy bytes/words from/to U space.
In particular, you can get the syscall parameter from ustack AND modify
the ustack contents, e.g. the saved AX register for return value to U
mode.
5. Action Functions:
In syscall(a,b,c,d), the parameter a is the syscall number. Based on the
value of a, you can execute the desired action function, passing any needed
parameters b,c,d to it.
RETURN VALUE:
Each action function returns a value back to Umode. The return value is
carried in the AX register. Since goUmode() pops the saved AX from ustack,
you must fix the saved AX in ustack before goUmode().
!============================================================================
6. Utility Functions:
These functions provide Inter-space transportation.
get_byte() and put_byte() are already given BEFOE !
*===========================================================================*
!* get_byte *
!*===========================================================================*
! This routine is used to fetch a byte from anywhere in memory.
! The call is:
! c = get_byte(segmet, offset)
! where
! 'segment' is the segment value to put in es
! 'offset' is the offset from the segment in es
_get_byte:
push bp ! establish FP
mov bp,sp !
push es ! save es
push bx ! save bx
mov es,4(bp) ! load es with segment value
mov bx,6(bp) ! load bx with offset from segment
seg es ! go get the byte
movb al,(bx) ! al = byte
xorb ah,ah ! ax = byte
pop bx ! restore bx
pop es ! restore es
mov bp,sp
pop bp ! return
ret !
!=============================================================================
!*===========================================================================*
!* put_byte *
!*===========================================================================*
! This routine is used to put a word to anywhere in memory.
! The call is:
! put_byte(char,segment,offset)
! where
! char is a byte
! 'segment' is a segment
! 'offset' is the byte offset from the segment
_put_byte:
push bp ! establish FP
mov bp,sp !
push es ! save es
push bx
mov es,6(bp) ! load es with segment value
mov bx,8(bp) ! load bx with offset from segment
movb al,4(bp) ! load byte in aL
seg es ! go put the byte to (ES, BX)
movb (bx),al ! there it goes
pop bx
pop es ! restore es
mov bp,sp
pop bp !
ret ! return
7. Function for reading/writing disk blocks
!------------------------------------------------------------------
! diskr(cyl, head, sector, buf); MemoryAddress=(ES,buf)
! 4 6 8 10 NOTE: (cyl,head,sector) count from 0
!-----------------------------------------------------------------
_diskr:
push bp
mov bp, sp ! setup stack frame; bp is FP
movb dl, #0x00 ! drive 0=fd0
movb dh, 6(bp) ! head#
movb cl, 8(bp) ! sector#
incb cl ! inc sector by 1 to suit BIOS
movb ch, 4(bp) ! cyl#
mov ax, #0x0202 ! READ 2 sectors; 0x0302 for WRITE 2 sectors
mov bx, 10(bp) ! put buf value in BX ==> addr=(ES,BX)
int 0x13 ! call BIOS to read the block
jb error ! to error if CarryBit is on (read failed)
mov bp,sp
pop bp
ret ! return to caller
error: ! handle disk i/o error (reboot?)