Our Goal

In this tutorial, Skelix has the ability of loading programs(a shell if you like) from disk and execute them, the user programs can invoke kernel functions via system calls.

Download source code


System Call

After last 8 tutorials, we almost set up everything we should set up for a kernel, em, roughly, quite roughly of course, now it is the time for shell to take over the control, actually it is getty() function prints a login prompt and waits for user to login, certainly, I'm not going to write a shell, neither a getty(). However, in this tutorial, I'm going to give Skelix the ability of loading a file from file system and executing it, in practice, it give the general OS kernel the ability of loading and executing tasks resident on disks(init file on *nix-like systems or shells).

Because the virtual memory part of Skelix is still undergoing, so all tasks share the same memory space, so for simplicity, the program from disk will be loaded to a fixed address 0x100000, and the program will be stored right under the directory /.

Before that, there is another thing we have to be aware of. In reality, the user task can not access kernel memory space, so those functions like kprintf, print_c etc which we used before we can not use them anymore (actually, at this moment we still can, because I still have not worked out the virtual memory part, but we just pretend we can not use them), In reality, all OS kernel provides APIs or system calls or whatever you call them. So we are going to let our program uses the system call to perform some operation, because it is just for giving an example, so I just write one system call,

09/isr.s

sys_print:

        pushl    %esi            # bg color

        pushl    %edi            # fg color

        pushl    %ebx            # character to be printed

        cli

Does not like interrupt gate, the trap gate do not set the IF flag in EFLAGS automatically, so we call cli explicitly.

        call     print_c

        sti

        addl     $12,    %esp

        ret

Quite simple, right? This system call accepts three arguments: ESI stores the background color and EDI stores the foreground color, EBX stores the character we are going to print. So eventually, this system call just print a given character, use given colors. It is nothing more than a wrapper to print_c function.

Theoretically, user can not access print_c function, actually they do not even know the existence of this function. The only way for user tasks talking to kernel is via system calls, so we have to make this system call visible to users.

DOS provides int 21 for it's system services, and Linux provides int 0x80 to provides system calls, we are going to use an entry in IDT to enable users use system calls by instruction int 0x80.

We only used 34 entries in IDT, rest of them all are available, but I am going to use 0x80 entry. So we have to set up an 386 trap gate in IDT. The format of 386 trap gate is quite similar to interrupt gate, just the type field is E instead of 8, and we have to set it's DPL field to 3 to let normal users use it.

09/init.c

static void

sys_call_install(void) {

        unsigned long long sys_call_entry = 0x0000ef0000080000ULL |

                        ((unsigned long long)CODE_SEL<<16);

        sys_call_entry |= ((unsigned long long)sys_call<<32) & 

                0xffff000000000000ULL;

        sys_call_entry |= ((unsigned long long)sys_call) & 0xffff;

        idt[SYS_CALL] = sys_call_entry;

}

The macro SYS_CALL has been defined to 0x80. We can see the sys_call we used as a wrapper of all system calls.

09/isr.s

sys_call:

                cmpl    $1,     %eax

                jb              1f

                iret

1:

                pushal

                call    *sys_call_table(, %eax, 4)

                popal

                iret

I keep it as simple as I can, it just checks the system call number which stores in EAX, then call the corresponding function in array sys_call_table, the sys_call_table is an array of all system call function pointers.

09/syscall.c

void (*sys_call_table[VALID_SYSCALL])(void) = {sys_print};


Color

Now let's check out the program we are going to store it on disk and will be loaded and executed in memory.

09/color.c

void

color(void) {

    int i, j;

    for (i=0; i<16; ++i)

        for (j=0; j<16; ++j)

            __asm__ ("int    $0x80"::"S"(i),"D"(j),"b"('X'),"a"(0));

    for (;;)

        ;

}

Simple, right? We let the entry of this program to be color, just like the normal entry for ordinary C file is main. This program will print letter 'X' using different combinations of background and foreground color.

However, here is the problem, how can we store it on disk, we do not have a shell, we can not just type cp blablabla blablalb, so I decide to write the program on disk directly after the creation of file system.

First, I wrote a program to print the content of a file in array format, certainly you have to compile 09/ghex/ghex.c and 09/color.c at first. In Makefile, the entry for color should be written like this,

09/Makefile

all: final.img color

 

color: color.o

    ${LD} --oformat binary -N -e color -Ttext 0x100000 -o color $<

Let it know it will be loaded at offset 0x100000.

ghex

Then copy those hex number to source code like following, we are going to write the color program to disk.

09/fs.c

void

install_color(void) {

        struct SUPER_BLOCK sb;

        char sect[512] = {0};

        struct DIR_ENTRY *de = NULL;

        int inode = -1;

        struct INODE clnode;

        unsigned int blk = 0;

        unsigned char color[] = {0x57,0x56,0x53,0x83,0xec,0x08,0xc7,0x44,

        0x24,0x04,0x00,0x00,0x00,0x00,0x83,0x7c,0x24,0x04,0x0f,0x7f,

        0x2e,0xc7,0x04,0x24,0x00,0x00,0x00,0x00,0x83,0x3c,0x24,0x0f,

        0x7f,0x19,0x8b,0x74,0x24,0x04,0x8b,0x3c,0x24,0xbb,0x58,0x00,

        0x00,0x00,0xb8,0x00,0x00,0x00,0x00,0xcd,0x80,0x89,0xe0,0xff,

        0x00,0xeb,0xe1,0x8d,0x44,0x24,0x04,0xff,0x00,0xeb,0xcb,0xeb,

        0xfe};

 

        sb.sb_start = *(unsigned int *)(HD0_ADDR);

        hd_rw(ABS_SUPER_BLK(sb), HD_READ, 1, sect);

        memcpy(&sb, sect, sizeof(struct SUPER_BLOCK));

 

        inode = alloc_inode(&sb);

        assert(inode > 0);

        blk = alloc_blk(&sb);

        assert(blk != 0);

        clnode.i_block[0] = blk;

        hd_rw(blk, HD_WRITE, 1, color);

        clnode.i_mode = FT_NML;

        clnode.i_size = sizeof color;

        iput(&sb, &clnode, inode);

Allocates new inode and block for color program and set the correct information in its inode and write the inode back to disk.

        hd_rw(iroot.i_block[0], HD_READ, 1, sect);

        de = &((struct DIR_ENTRY *)sect)[2];

        strcpy(de->de_name, "color");

        de->de_inode = inode;

        hd_rw(iroot.i_block[0], HD_WRITE, 1, sect);

Add the new entry about color to directory /.

        iget(&sb, &iroot, 0);

        iroot.i_size = 3*sizeof(struct DIR_ENTRY);

        iput(&sb, &iroot, 0);

}

One new file color has been created under directory /, so, the inode of directory / has been changed, write it back to disk.

Now we have color file on disk, we have to load it to address 0x100000.

void

load_color(void) {

        struct INODE inode;

        struct SUPER_BLOCK sb;

        char sect[512] = {0};

 

        sb.sb_start = *(unsigned int *)(HD0_ADDR);

        hd_rw(ABS_SUPER_BLK(sb), HD_READ, 1, sect);

        memcpy(&sb, sect, sizeof(struct SUPER_BLOCK));

        iget(&sb, &inode, 1);

 

        /* for simplicity, just load the first sector of color to 0x100000 */

        hd_rw(inode.i_block[0], HD_READ, 1, (void *)0x100000);

}

Very simple, we just load the first sector of color file, and copy it to address 0x100000.

Still remember task1_run and task2_run? It's been so boring for them just flipping letters on screen, so I'm going to let task1 load and execute color.

09/init.c

void

do_task1(void) {

        __asm__ ("incb 0xb8000+160*24+2");

        load_color();

        __asm__ ("jmp 0x100000");

}

Now we put them together in check_root.

        if (! testb(sect, 0)) {

                kprintf(KPL_DUMP, "/ has not been created, creating....\t\t\t\t\t  ");

                if (alloc_inode(&sb) != 0) {

                        kprintf(KPL_PANIC, "\n/ must be inode 0!!!\n");

                        halt();

                }

                iroot.i_block[0] = alloc_blk(&sb);

                iput(&sb, &iroot, 0);

                

                hd_rw(iroot.i_block[0], HD_READ, 1, sect);

                de = (struct DIR_ENTRY *)sect;

                strcpy(de->de_name, ".");

                de->de_inode = 0;

                ++de;

                strcpy(de->de_name, "..");

                de->de_inode = -1;

                hd_rw(iroot.i_block[0], HD_WRITE, 1, sect);

                kprintf(KPL_DUMP, "[DONE]");

                if (iroot.i_size == 2*sizeof(struct DIR_ENTRY))

                        install_color();

        }

we save file color in directory /, and we restat the content of directory /, we shoule see a new file color was found.

Now make it with last Makefile,

making process of tutorial09 possible colors


Feel free to use my code. Please contact me if you have any questions.