EOS Hints

INTRODUCTION

Here is some of the stuff that you'll need top know for your Kernel project, part 1. The first thing that you will need to do is to read Chapter 3 of your course notes: Software information. It tells you generalities that you will need to know for the assignment: about the processor registers, segment selectors (GDT) and interrupt vectors (IDT).

For more detailed information about the architecture and about processor instrucitons, you will need is access to a 486 (or 386+) microprocessor manual. There is one in the lab, but you may want your own or want to borrow a friend's. But note that the assembler that we are using ("gas") has a different syntax from the Intel format which is described in most Intel books. More on this in a minute.

One thing that you will want to do is to minimize the amount of assembly code that you write. You only require assembly code in two places: in your kernel-call "stubs" library to set up arguments/registers and execute the software-interrupt instruction ("int"), and in the kernel to handle the interrupt(s), establish the C-language environment, pass arguments, call a C function for higher-level processing, and return from the interrupt. A skeleton version of this "context switching" kernel code is shown in your course notes.

Another thing that you will want to do is to make the assembly code that you do have to write be as "safe" as possible. Making code "safe" means to make the fewest number of assumptions about the C-language environment. In the GCC optimizing-compiler environment, it's hard to be sure which processor registers are holding important values at what time, and whether code is going to be re-arranged or not, and what code will be produced to maintain the C-language stack environment. For this reason, the context-switching code purposely defines its own "_entryPoint", which you will set the interrupt vector to point to (not to the doNotCallMe() function name), since GCC will generate some C-stack-maintenance code between doNotCallMe() and the "asm" directive.

SEGMENTATION

i486 addresses are formed from a segment base address plus an offset. To compute an absolute memory address, the i486 figures out which segment register is being used and uses the value in that segment register as an index into the global descriptor table (GDT). The entry in the GDT tells (among other things) what the absolute address of the start of the segment is. The processor takes this base address and adds on the offset to come up with the final absolute address for an operation. You'll have to look in a 486 manual for more information about this or about the GDT organization.

Reasons: ease of relocation, enlargement of processor address space, upward compatibility, smaller instructions. The biggest advantage for us is ease of relocation.

i486 uses a modification of this idea - 6 segment registers instead of one:
  CS - added to address during instruction fetch (code segment)
  SS - added to address during stack access (stack segment)
  DS - added to address when accessing a memory operand that is not on
       the stack (data segment)
  ES, FS, GS - can be used as an extra segment register; also used in
       special instructions that span segments

Segments can start at any address, although it is usually desiriable to make them start on 16-byte ("paragraph") or 4096-byte ("page") boundaries. Segment sizes can have either a byte or a page granularity, for sizes from 1 byte to 1 MB (byte) or from 1 byte to 4 GB (page).

You should initialize the GDT how the course notes tell you: declare a global array of descriptors in your kernel code, and set the GDTR to point to it.

When you create a new process, you will have to allocate and initialize two GDT entries for it: one for the code segment and one for the data segment. The code segment will be inside the bindfile image and the data segment will be dynamically allocated by you.

To get the information about your posted modules, use the mbiaddr parameter passed in by Grub (via Multiboot). This address points to a structure of type multiboot_info_t. Module i is located at mods_addr[i] (using the multiboot info header).

To figure out how many bytes a process' data segment should be in size, you need to take some information from the "elf" file format that all programs are in. The size that you should allocate for the data segment of a process is dataSize + bssSize + stackSize. This also happens to be the initial ESP (stack pointer) value that you should use for the new process.

When you create a new process, you should set its code segment to be for the code-segment selector in the GDT for the new process, and you should set the DS, SS, ES, FS, and GS registers to be the segment selector for the data segment in the GDT for the new process.

About the absRead() function: it reads from absolute memory. The first argument is the absolute address (real memory) to start reading from, the second argument is the local address (offset) of the C variable to read the data into, and the third argument is the number of bytes to read (where there are, duh, four bytes in a 32-bit word). There is also a corresponding absWrite() function. The arguments are the same, but it writes instead of reading. These methods assume there is a GDT selector at 0x10 which gives them access to all 4GB of memory, starting at 0x0000 (the absolute start of memory).

Note: don't confuse absolute (real-memory) addresses with segment selector indices.

GAS ASSEMBLY LANGUAGE

To repeat what was stated above, the GNU Assembler (GAS) uses a different syntax from what you will likely find in any microprocessor reference manual, and the two-operand instructions have the source and destinations in the opposite order. Here are the types of the GAS instructions:
    opcode                    (e.g., pushal)
    opcode operand            (e.g., pushl %edx)
    opcode source,dest        (e.g., movl %edx,%eax) (e.g., addl %edx,%eax)

When there are two operands, the rightmost one is the destination. The leftmost one is the source - i.e., movl %edx,%eax means "move the contents of the edx register into the eax register", and addl %edx,%eax means "add the contents of the edx and eax registers, and place the sum in the eax register". This source,dest notation is a little confusing for the subtract and compare instructions, since the arguments will be in the unintuitive order (I think), but, really, you shouldn't be needing to do anything as complicated as subtraction.

Included in the syntactic differences between GAS and Intel assemblers is that all register names used as operands must be preceeded by a percent (%) sign, and instruction names usually end in either "l", "w", or "b", indiciating the size of the operands: long (32 bits), word (16 bits), or byte (8 bits), respectively. For our purposes, we will usually be using the "l" (long) suffix.

Another thing to note is the actual register names: you use different names for the registers depending on what size operands your instruction is using. For byte operands, you would name registers like %al or %ah to refer to the lowest byte or to the high byte (or a 16-bit register); for word arguments you would use register names like %ax; and for long arguments you would use register names like %eax. We will normally be using this latter form (except in the cases of using the segment registers, since they are 16-bit registers). The form of the register name must agree with the size suffix of the instruction.

There seems to be a lot of redundancy here, and there is; the GAS format was designed more to be efficient to assembler rather than to be convenient to program in, since almost all of the assembler code that it ever sees has been generated by a C or C++ compiler.

Here are the important processor registers:

    EAX,EBX,ECX,EDX - "general purpose", more or less interchangeable
                    - you can use only the lower 16 or 8 bits of these,
                      e.g., ax, al, ah

    EBP             - used to access data on stack
                    - when this register is used to specify an address, SS is
                      used implicitly

    ESI,EDI         - index registers, relative to DS,ES respectively

    SS,DS,CS,ES,FS,GS - segment registers
                      - (when Intel went from the 286 to the 386, they figured
                         that providing more segment registers would be more
                         useful to programmers than providing more general-
                         purpose registers... now, they have an essentially
                         RISC processor with only _FOUR_ GPRs!)
                      - these are all only 16 bits in size

    EIP            - program counter (instruction pointer), relative to CS

    ESP            - stack pointer, relative to SS

    EFLAGS         - condition codes, a.k.a. flags

Addressing modes (for operands):

    implied - used a lot, often in combination with other modes (i.e., one
              operand is implied, the other given explicitly)
            - movsb     (ds:esi, es:edi, ecx are all used by this instruction)

    accumulator - that is, can only use al or ax or eax
                - e.g., cbw   (byte-->word, sign extension)

    register    - can use EAX, EBX, ECX, EDX
                - (also ESI, EDI, EBP, ESP, I think --CSB)
                - e.g., movl %ecx,%eax
                -       push %ebx

    immediate   - data specified as part of instruction
                - GAS uses a dollar sign ($) to indicate this (other assemblers
                  may use a number sign or no symbol at all)
                - e.g., movl $3, %ecx  (ecx := 3)

    memory      - template:  segment:offset( base, index, scale )
                - <segment> can be any of the segment registers, to override
                     the default
                - <offset> is an integer constant or constant identifier
                - <base> is %eax, %ecx, %edx, %ebx, %esp, %ebp, %esi, or %edi
                - <index> is %eax, %ecx, %edx, %ebx, %ebp, %esi, or %edi
                - <scale> is either 1, 2, 4, or 8 == what to multiply the index
                     by before using it; this is used for accessing arrays
                - format examples (you can leave out some parts):
                    offset                               {movl %eax,5} {incl 5}
                    offset(base)                           {movl %eax,-8(%ebp)}
                    offset(,index)                         {movl 4(,%edi),%eax}
                    offset(base,index)                 {movl 4(%ebp,%esi),%ebx}
                    offset(base,index,scale)        {movl 16(%ebx,%edi,4),%eax}
                    %cs:4
                    
                   notes: - offset can be zero, negative or positive
                          - when %ebp is used, the %ess register is used to
                            generate the address instead of the %eds register
                            (unless overridden, see next point) (%esp too?)
                          - usually %eds is used to generate address

BE CAREFUL!!! You can't always use all addressing modes with all instructions, so, KEEP A BOOK HANDY!! There are more exceptions than rules (Yay, Intel!).

Segment registers are special:  can't do  movw <seg-reg>,<seg-reg>

                              but can do    movw <seg-reg>,memory
                                            movw memory,<seg-reg>
                                            movw <seg-reg>,<reg>
                                            movw <reg>,<seg-reg>
 note: if you {movw %ss,%ax}, then you should {xorl %eax,%eax} first to clear
        the high-order 16 bits of %eax, so you can work with long values.

COMMON/USEFUL INSTRUCTIONS

mov (especially with segment registers)
    - e.g.,:
        movw %es,%ax
        movl %cs:4,%esp
        movw _processControlBlock,%cs

    - note:     mov's do NOT set flags


cmp (usually followed by jump instruction)

   cmp s2,s1 performs s1-s2 and uses the results to set flags

   therefore
      cmp %edx,%eax
      jnz label1                 ;if eax <> edx goto label1
      cmp #max_processes,%eax
      jb label2                  ;if eax < max_processes goto label2


call  (jmps to piece of code, saves return address on stack)
         e.g., call _cFunction
ret   (returns from piece of code entered due to call instruction)
iretl (returns from piece of code entered due to hardware or software
       interrupt)


pushl, popl       - push/pop long
pushal, popal     - push/pop EAX,EBX,ECX,EDX,ESP,EBP,ESI,EDI

rep movsb
    set DS,ESI to source string (hence how ESI got its name)
    set ES,EDI to dest string (hence how EDI got its name)
    set ECX = length

            CLD    - clears flag used by movsb
            REP    - repeat MOVSB and decrement ECX till ECX = 0
            MOVSB  - mov ES:DI,DS:SI and increment SI,DI

add, sub
sti, cli

MIXING C AND ASSEMBLY LANGUAGE

The way to mix C and assembly language is outlined in your Course Notes. You use the "asm" directive. To access C-language variables from inside of assembly language, you simply use the C identifier name (prefixed with an underscore) as a memory operand:

unsigned long a1, r;
void junk( void )
{
   asm(
        "pushl %eax \n"
        "pushl %ebx \n"
        "movl $100,%eax \n"
        "movl a1,%ebx \n"
        "int $69 \n"
        "movl %eax,r \n"
        "popl %ebx \n"
        "popl %eax \n"
   );
}

This example puts a value of 100 into %eax, copies the value in global variable "a1" into %ebx, executes a software interrupt number 69, copies the value in %eax into the global variable "r" and then restores the contents of the temporary registers.

The example demonstrates the recommended "safe" way to communicate between C and assembler: with global variables. It is also possible to communicate using the stack (relative to %ebp: arguments are stored at positive offsets from %ebp and local variables are stored at negative offsets), and it may not be necessary to save and restore register contents, but this is hard to predict inside of the GCC hyper-code-optimization environment. Using the above method avoids all optimization pitfalls (except, maybe, that your code doesn't get executed at all if GCC thinks that it's a null function; dunno). The "safe" code is a little uglier and slower than optimized code, but our processors are way faster than they need to be for our purposes anyway.

The recommended way to code the interrupt-handling code in the kernel is shown in the doNotCallMe() example in the course notes: make sure that you enter the kernel code at an entryPoint that you define; otherwise, you don't know what GCC will stick in between the function declaration and the start of your code. With the following declaration:

   extern unsigned long entryPoint;
you should be able to fool C into letting your C code know where the entry point is by referring to (unsigned long)&entryPoint in order to initialize the interrupt vector.

KERNEL HINTS

Don't assume in your context-switching code that the values of DS, SS, and ES will always be the same. When you implement asynchronous interrupts (part #3), the values may be different when an interrupt occurs (from executing far-memory-accessing code), so you must save and restore each of these registers independently; if you restore them to all of the same values after an asynchrous interrupt, Bad Things(TM) could happen. You should also save and restore the FS and GS registers, too.

Try not to make the kernel use the user process' stack for its own processing. The compiler assumes that DS and SS will be the same for all code that it produces, so pointers, global variables and library functions in the kernel code may not work properly in some cases.