2013-11-23

2013-11-23 Constructors and Library Initialisation

The last few weeks have been spent researching initialisation functions, constructors, and how to write and call them.

Now that libkernel and libc are separate entities, they both have some initialisation code that needs to be run.  At the very least, these two are going to have to resolve the kernel system call functions so that they can make syscalls.  Libc can cheat at this because it provides the start() function that is called first when the program is loaded, so it can take this opportunity to set itself up (this is fairly common amongst C libraries).  Libkernel has a slightly tougher job.  I'd rather not have the program need to explicitly initialise it, and libc shouldn't assume that libkernel should be there, so how do we do it?

Well, GCC already has a way of doing this which it uses for C++ static constructors and for a few rare cases in C, but the details of it are spread around so let's try to make sense of it for our specific situation.

The first port of call is to RTFM, which in this case is http://gcc.gnu.org/onlinedocs/gccint/Initialization.html .  Unfortunately, this article is very heavy going, talks in very abstract terms and isn't very easy to follow.  Also, searching around for some of the terms used, especially the #defines in this article doesn't help either.

The few sources I was able to find with information on how this works generally suggest that the ELF file contains a section called .ctors which contains a list of addresses to constructor functions which are to be called at the beginning of the program (and a section called .dtors for deconstructors for the end of the program).  It makes sense that this section should be created by the compiler itself (as the linker won't know which functions need to be in this) so the linker must just combine together the pointers from all the different object files.

Step 1 : Compiling


Let's have a look at this first hand.  We know that C++ does for some cases, so let's try this:

main()
{
 asm("int $1");
}

volatile int RandomRoll()
{
 asm("int $2");
 return 4;
}

int mTest = RandomRoll();

Which can be compiled into an object file with:
g++ hello.cpp -nostdlib
(ignoring the warning about the undefined entry symbol.)

This file needs some initialisation in order to work properly, specifically that the program must call RandomRoll() to set the value of mTest before main() is called, so we expect to find some clues in here.  The assembly interrupts aren't expected to be run, they are there to help us identify the code when we start disassembling it.

Having a look at the object file with objdump allows us to see what's going on.

> objdump a.out -h

a.out:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000052  08048074  08048074  00000074  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .eh_frame     00000098  080480c8  080480c8  000000c8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .ctors        00000004  08049160  08049160  00000160  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  3 .bss          00000004  08049164  08049164  00000164  2**2
                  ALLOC
  4 .comment      00000011  00000000  00000000  00000164  2**0
                  CONTENTS, READONLY



Ok, that's good, we have a section called .ctors with a size of four bytes which contains data.  This is exactly the right amount to hold an address on a 32-bit platform.

Back to objdump to make certain we understand this (I've skipped the data from some of the sections as they weren't relevant):

> objdump a.out -sS

a.out:     file format elf32-i386

Contents of section .ctors:
 8049160 aa800408                             ....

Disassembly of section .text:

08048074 <main>:
 8048074:       55                      push   %ebp
 8048075:       89 e5                   mov    %esp,%ebp
 8048077:       cd 01                   int    $0x1
 8048079:       b8 00 00 00 00          mov    $0x0,%eax
 804807e:       5d                      pop    %ebp
 804807f:       c3                      ret

08048080 <_z10randomrollv>:
 8048080:       55                      push   %ebp
 8048081:       89 e5                   mov    %esp,%ebp
 8048083:       cd 02                   int    $0x2
 8048085:       b8 04 00 00 00          mov    $0x4,%eax
 804808a:       5d                      pop    %ebp
 804808b:       c3                      ret

0804808c <_z41__static_initialization_and_destruction_0ii>:
 804808c:       55                      push   %ebp
 804808d:       89 e5                   mov    %esp,%ebp
 804808f:       83 7d 08 01             cmpl   $0x1,0x8(%ebp)
 8048093:       75 13                   jne    80480a8 <_z41__static_initialization_and_destruction_0ii data-blogger-escaped-x1c="">
 8048095:       81 7d 0c ff ff 00 00    cmpl   $0xffff,0xc(%ebp)
 804809c:       75 0a                   jne    80480a8 <_z41__static_initialization_and_destruction_0ii data-blogger-escaped-x1c="">
 804809e:       e8 dd ff ff ff          call   8048080 <_z10randomrollv>
 80480a3:       a3 64 91 04 08          mov    %eax,0x8049164
 80480a8:       5d                      pop    %ebp
 80480a9:       c3                      ret

080480aa <_global__sub_i_main>:
 80480aa:       55                      push   %ebp
 80480ab:       89 e5                   mov    %esp,%ebp
 80480ad:       83 ec 08                sub    $0x8,%esp
 80480b0:       c7 44 24 04 ff ff 00    movl   $0xffff,0x4(%esp)
 80480b7:       00
 80480b8:       c7 04 24 01 00 00 00    movl   $0x1,(%esp)
 80480bf:       e8 c8 ff ff ff          call   804808c <_z41__static_initialization_and_destruction_0ii>
 80480c4:       c9                      leave
 80480c5:       c3                      ret

There's a lot going on here and I'm not sure I wrote all of it.  Let's have a look.

We can see our main() and RandomRoll() functions (not sure why the name is munged, not important right now) but we also have two new functions, _global__sub_i_main() and _z41__static_initialization_and_destruction_0ii().  _global__sub_i_main() seems to call _z41__static_init_blah_blah(), which in turn seems to call our RandomRoll() function so without getting into it too deeply, I'd be happy to assume that _global__sub_i_main() is our initialisation function.

At the top of the disassembly is the contents of the .ctors section which contains that one 32-bit value, which is (interpreting the little-endian encoding) 0x080480aa, which is exactly the memory address of the _global__sub_i_main().
\ o /

At this point, my Google searching failed me.  For the life of me I can't figure out how to coerce the C compiler to create a .ctors section and populate it in the way that C++ does (answers on a postcard), however, I have a plan.

At the end of the day, the value that C++ put into the .ctors section is just your average common-or-garden initialised pointer variable, but in a special place, so what about this:

void test_init()
{
asm("int $3");
}

void* __attribute__ ((section (".ctors"))) test_init_ref = test_init;

I have my function, and I have a global variable which points to my function.  I've used some GCC specific runes to say that I want my variable to live in a section called .ctors.  Let's build this and see if it gives us the desired result (some sections removed for brevity):

>gcc hello.c -nostdlib
>objdump a.out -sS

a.out:     file format elf32-i386

Contents of section .ctors:
 80490b4 74800408                             t...

Disassembly of section .text:

08048074 :
 8048074:       55                      push   %ebp
 8048075:       89 e5                   mov    %esp,%ebp
 8048077:       cc                      int3
 8048078:       5d                      pop    %ebp
 8048079:       c3                      ret

Success!  We can see our function, and we can see the entry in .ctors which points to it, just as we saw in C++.  It's not the prettiest of solutions, but it will work for now.

Step 2 : Running


While we've gotten that all working, that's only half of the battle,  we still need to actually call all these from our program.

For the most part, all our program needs to know is where the .ctors array is and how long it is.  It can then iterate all the constructors and run them.  (These are function pointers after all, so these will always be relocated as required by the loader.  If the relocation didn't update function pointers, the program has more to worry about than constructors.)

One of the other searches I was running while doing this was to investigate how other systems dealt with this problem.  While that search yielded nothing of note, it did give me the idea to check out the default GCC  link script.  Currently my kernel uses a custom link script, but the applications still use the default one, where I found something interesting:

  .ctors          :
  {
    /* gcc uses crtbegin.o to find the start of
       the constructors, so we make sure it is
       first.  Because this is a wildcard, it
       doesn't matter if the user does not
       actually link against crtbegin.o; the
       linker won't look for a file to match a
       wildcard.  The wildcard also means that it
       doesn't matter which directory crtbegin.o
       is in.  */
    KEEP (*crtbegin.o(.ctors))
    KEEP (*crtbegin?.o(.ctors))
    /* We don't want to include the .ctor section from
       the crtend.o file until after the sorted ctors.
       The .ctor section from the crtend file contains the
       end of ctors marker and it must be last */
    KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
    KEEP (*(SORT(.ctors.*)))
    KEEP (*(.ctors))
  }

So, my link-script-fu is not great, but I can understand enough of this to see that crtbegin.o and crtend.o have special handling when it comes to the .ctors section.  Anything for the .ctors section from crtbegin.o goes at the beginning of the output, and anything from crtend.o goes at the end, effectively book-ending the array.  (I'll admit I didn't know what crtbegin and crtend were for, but GCC complained that I didn't have them when I created libc, so I put in empty ones.)

This information, coupled with what we just did for shuffling variables into the .ctors section gives me an idea.

I can put a variable in crtend.c like this:
int __attribute__((section(".ctors"))) __libc_internal_ctor_end = 0;

... and then put this in crtbegin.c:
extern int __libc_internal_ctor_end;  // <<-- in crtend.o
int __attribute__((section(".ctors"))) __libc_internal_ctor_start = 0;

void call_ctors()
{
 // Loop between __libc_internal_ctor_start and __libc_internal_ctor_end
}

The linker will put these two variables at the beginning and end of the .ctors list, and then the call_ctors() function can use them to work out where all the other constructors are.  Note that the content of those two variables is completely irrelevant, only their locations in memory is useful.

Step 3 : Testing


This blog post is way longer than it was going to be, so I'll be brief and say what I usually say.  It failed a bunch of times because I made some stupid mistakes, but after much hammering, it all works.

crt0.s now performs calls:

  • the C Library initialisation function,
  • the constructors function in crtbegin.c, and
  • the program's main() function.
The really good article I found the other day ( https://blogs.oracle.com/ksplice/entry/hello_from_a_libc_free ) also helped me to confirm at least some of what this was supposed to do, but only after I know what I was looking for.

I also found out along the way that when a library (such as libkernel.a) is linked in, the linker only includes those object files which are actually needed.  This gave a minor testing headache because I expected my init() function and its .ctor reference to appear just because the library was linked, but it didn't.  That was easily fixed though.

2013-11-03

2013-11-03 Doom Continued, and printf upgrade.

With Doom reaching the main() function, it's time to start working through it making sure everything is implemented and working.

Bearing in mine that sound and video are a way off yet, the short term goal is getting Doom to display its opening text, loading what files it wants, then "running", then exiting.

So the next thing it wants is a WAD file.  Doom searches to see which .wad files are available, but for now I'll just hardcode the path to the .wad file.

I seem to getting a strange problem with printf().  It's printing text to the screen.  That may not sound that odd, except for the fact that I haven't written it yet.  It appears that GCC is optimising calls to printf() with a constant string to be calls to puts() instead (which is written) so that why some of them were working.  (I thought -O0 turned off optimisations?)

V_Init: allocate screens.
M_LoadDefaults: Load system defaults.
Z_Init: Init zone memory allocation daemon.
W_Init: Init WADfiles.

The next thing that Doom needs is a sprintf() function which I don't have.  Originally, my printf() routine was a simple routine written by someone else (one of the few pieces of code my OS has used that I didn't write).  It was designed for an embedded system and didn't support all the features and options of printf().

Not long ago I sat down and wrote my own version, adding a lot more support for the various tokens, but it would still only output to the screen.  This version of the function is still used in the kernel as kstdio_printf() which gets called with debug messages all over the place.

So today I've taken that function and extended it once more.  After researching variadic functions and how they are handled in C, I've written implementations for the eight different version of printf():

  • fprintf() - variadic arguments, outputting to a stream
  • printf() - variadic arguments, outputting to stdout (a special case of above).
  • sprintf() - variadic arguments, outputting to a buffer.
  • snprintf() - variadic arguments, outputting to a buffer with a limit.
  • vfprintf() - va_list arguments, outputting to a stream.
  • vprintf() - va_list arguments, outputting to stdout (a special case of above).
  • vsprintf() - va_list arguments, outputting to a buffer.
  • vsnprintf() - va_list arguments, outputting to a buffer with a limit.

All of these are actually serviced by a single internal printf function which is capable of performing them all which reduces code duplication.

With all of these in place, I think that's good place to leave it for today.


2013-11-02

2013-11-02 Doom!

I don't know if I mentioned, but a while ago I downloaded the source code to the Linux port of Doom with the intention of getting it working with my OS.

"Why Doom?" I hear you ask.

  • Doom is a complex C program with proven functionality.  While I'm making Doom work with my OS, it's going to require me to write a lot more of the basic kernel and C library functionality.  Also the code should work so if there's a problem, it's probably in my code.
  • Doom is well known.  When I mention to people that I'm writing an OS, I commonly get asked "Does it run World of Warcraft / Counter-Strike / Half-life 3 etc.?"  I like to think that they're trying to get an idea of the capabilities of my OS in terms that they understand, so by being able to say that it'll run Doom, they'll get exactly that.
  • Doom has modest system requirements.  Given that my test systems have at most maybe 128Mb and maybe a 1GHz processor, Doom should run nicely, even with the inevitable inefficiencies of my code.
  • Doom doesn't require multiple threads.  Doom is intended to run on a DOS like system, so has no inbuilt dependency on the operating system to provide multi-threading support.
  • Doom has modest graphical requirements.  Given that my OS is firmly seated in the world of 640x480x4 (16 colours) for now, having a program which doesn't need many colours or a large screen resolution is a good thing.
  • Doom is awesome.  I mean come on, it's DOOM!  (Remember when first-person shooters were called "Doom clones"?)


So all that aside, how do we do this?

Firstly, Doom has two files (i_video.c and i_sound.c) which handle ... well ... video and sound.  Given that both of these are going to need some special consideration and a lot of work to get going, we'll put these off until later, so let's strip out the content of the all the functions.  I've a lot of work before it will call any of these anyway.

It looks like Linux has two different interfaces for accessing files, the C library fopen() and fread() functions, and the open() and read() functions from fcntl.h and unistd.h.  Doom uses both of these in varying measure.  Rather than pollute my current libc with functions that don't belong there, I should probably create a libkernel to contain everything else.

... some time passes ...

Ok, so with a lot of reorganising of header files, adding a new library to the project, and writing a lot of stub functions (one of which being the scanf-type functions, that's not going to fun to write), the OctoDoom executable builds.

While I do have ELF loading code (used for the HelloWorld program), I still haven't wired it up to the file loading routines so that I can load an actual executable file (it's on my to-do list).  For now, I'll have to load the Doom executable the same why as I do the HelloWorld one, that is, using Bochs to load the binary into memory at some high address.

So the first test run of Doom wasn't very successful (when are they?).  I think that the problem might be that I'm loading a program that's 987Kb into a memory that was only 1Mb to start, probably not going to have a good time.  That and the ELF loader call to kmalloc wasn't sanity checking the return value for a valid pointer.  So I had run out of memory, malloc returned NULL and the ELF loader carried on trying to use that pointer, schoolboy error.

Oh yeah, now it's loading Doom and getting as far as the main() function ... this stuff just got real!

2013-10-31

2013-10-31 FAT and ATA drivers

So I've migrated the ATA driver from the DriveAccess kernel, it's nothing special, but it can read hard drives on an ATA interface (IDE) and also interpret standard partition tables to allow devices to specify the partition and resolve blocks relative to it.

I've also migrated much of the FAT file-system driver, but with modifications to implement the LookupFile() function which will (in future) allow the file locking system to work.

I did work out that with 32 (I think) FAT directory entries in a 512-byte block, using a uint32 to uniquely identify each directory on the FAT volume does actually limit me to partitions of 128Gb in size.  This is something I will have to address at some point.

I've created mountlists for the two hard drives that the emulated system has (one hardfile for Grub and a "hardfolder" for the kernel and other files) and I'm using these to mount the hard drives:

dos_mount
(
 "DH0:",
 &dvrata_driver,
 "PARTITION=0",
 &fat_driver,
 ""
);

dos_mount
(
 "DH1:",
 &dvrata_driver,
 "UNIT=1,PARTITION=0",
 &fat_driver,
 ""
);


I've fixed up a bug where the path splitting function was splitting on a semi-colon rather than a colon, and written file-system functions to open a file from a FileID (basically a pointer to the Directory Entry), to seek to a location within a file (with each seek reading and caching the current block), and to read a section of a file (using the seek function to actually perform the read).

The FAT Filesystem context contains the following data:

uint32 DirSectorNumber;
uint32 DirEntryNumber;
tFAT_DirectoryEntry DirectoryEntry;
uint32 FilePointer;
uint32 FirstCluster;
uint32 FileLength;
uint32 CurrentCluster;
uint32 CachedBlockNum;
uint8* CachedBlock;

and finally a small snippet of code which loads and prints a test text file to the output:

int32 lTestFile = dos_openFile( "DH1:test.txt", 0 );
uint32 lReadSize = 256;
uint8* lBuffer = kmalloc(lReadSize);
uint32 lDataRead;
while( ( lDataRead = dos_read( lTestFile, lBuffer, lReadSize ) ) == lReadSize )
{
 kernel_write( 1, lBuffer, lDataRead );
}
kfree( lBuffer );

All in all, it works.  It's mounting the device, reading the FAT parameters from the disk via the block device, looking up a file by path and opening it, and reading the content of the file.  It's a really big step forward to have the Octopus kernel able to load files, and with Bochs capable of taking a folder from the host OS and presenting it to the emulated OS as a FAT filesystem, it'll make some bits of testing a LOT easier.


2013-10-28

2013-10-28 - Keyboard Handler Complete

The kernel now has a keyboard driver, the driver is being loaded into the list of Dos Devices as KBD:, KBD: is being opened as file number 0 (which corresponds with the standard input device).  The C library file functions are wired up to the kernel functions (using the function lookup mechanism) and the CLI uses the C library functions for its input and output.

At some point, the CLI could easily be extracted and made into a program with minimal changes.

Now, just to carefully enable ... the ... IRQ ... handler ... and ... BANG!  :(  General Protection Fault.

Ok, there may have been a few last issues with the interrupt handler, but with those fixed, the console is receiving key-presses  from the keyboard via the Dos layer, file descriptor table, kernel API, and the C library.

One thing I still need to address is when in the process the system converts scan codes into ASCII codes.  In fact, the C library only deals with characters in the stream rather actual key-presses with key-down and key-up which some programs will need, so I guess I'll need to add some API for that.  We'll leave that for another day though.

2013-10-22

2013-10-22 Keyboard Handler Progress

Still working through the keyboard handler and updating it for Octopus.  I've settled on the basic functions that a character device needs to expose so the keyboard handler now looks like a character device.

I've also made use of the stub functions for process_lock() and process_unlock() to allow the keyboard read() function to block until some keyboard data is available.  Currently process_lock() just does a spinlock as I haven't migrated the multi-tasking code yet, but as the keyboard is an interrupt-based device it doesn't hang the kernel forever.

The DOS layer has its list of DOSDevices into which the keyboard is being registered, and the interrupt system has a list of interrupt handlers into which a handler for IRQ 1 is being added.

A logical consequence of the way that the object-orientated device drivers are now working, I've had to enhance the interrupt handler so that a pointer to some kind of context object (the object instance, the state, whatever you want to call it) gets passed in when the interrupt is registered, and then gets passed back to the interrupt handler when the interrupt occurs.  In the case of the keyboard handler, this is a "keyboard context" object that contains, amongst other things, the keyboard input buffer itself.

I can see how this interrupt context will be really useful for all the other interrupt handlers, and I'm kind of annoyed that I didn't think of it when writing the original DriveAccess kernel as it would have made life a lot simpler.

2013-10-09

2013-10-09 DOS Layer ... still

I'm still working through the DOS layer that I built for the DriveAccess kernel and updating it to be more complete.

Previously, the DOS layer only supported block devices and file systems so could only load and save files.  Now I'm adding character devices into it to support keyboards, mice, serial ports, parallel ports and whatnot.

Once it's all in, I can add the keyboard handler as a character device called "KBD:", then open it as stdin which will allow the console to work.

Once the character device for the keyboard is working, it'll be trivial to migrate the various file systems (FAT) and block device drivers (ATA and RAM) from DriveAccess into Octopus so that I can get files loading and saving via the C library.