Ada Bare Bones: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
Content added Content deleted
No edit summary
m (Bot: Replace deprecated source tag with syntaxhighlight)
 
(45 intermediate revisions by 11 users not shown)
Line 1: Line 1:
''The aim of this tutorial is to produce a small Multiboot kernel written in the Ada programming language.''
{{Rating|2}}{{Template:Kernel designs}}


==Preface==
''In this tutorial we will compile a simple Ada kernel and boot it.''


One of the first things people ask on the Ada IRC channel on Freenode is ''"Can Ada be used for OS development?"'' to which the answer is a resounding '''yes'''.
<big><b>WAIT! Have you read [[Getting Started]], [[Beginner Mistakes]], and some of the related [[:Category:OS theory|OS theory]]?</b></big>


=== Prerequisites ===
==Preface==


The following section details the prerequisite software necessary for successfully building the kernel.
This tutorial is based on my multiboot kernel which I developed some time ago and placed on my site [http://www.archeia.com/an-ada95-hello-world-style-kernel.html] and will also be the basis for my own kernel [https://github.com/Lucretia/tamp TAMP].


==== Cross-Compiler ====
One of the first things people ask on the Ada IRC channel on Freenode is "Can Ada be used for OS development?" to which the answer is a resounding '''yes'''. But there are 2 problems:


{{Main|GNAT Cross-Compiler}}
# The people asking this question are new to Ada, and
# GNAT is not the easiest compiler to build.


This tutorial requires the use of a [[GCC_Cross-Compiler|cross-compiler]] with Ada support targeting the <tt>i686-elf</tt> architecture. Theoretically, any Ada compiler capable of producing bare-metal ELF binaries for this architecture is suitable. The recommended compiler is [https://www.gnu.org/software/gnat/ GNAT], which is part of the GNU Compiler Collection maintained by the Free Software Foundation. Historically it was difficult to create cross-compiler builds for GNAT, but recent versions of GCC have made this much simpler. For a tutorial on how to set up a GNAT cross-compiler, refer to [[GNAT Cross-Compiler|this article]].
Therefore these users don't understand what it takes to get the compiler into a useable state.


==== GPRbuild ====
As you may have seen from other bare bones tutorials on this site, they state that you must have a compiler built which can handle [[ELF]] files, the usual way is by building GCC which targets i386-elf or some other similar architecture. The problem here is that GNAT will not build for these targets out of the box without messing with it's makefile. You have to do this as the makefile builds the RTS and then the gnat tools (gnatmake, gnatbind, et al) which must all be built to have a working compiler - even though we will be replacing the RTS with our own cut down version.


This tutorial will require the use of the [https://docs.adacore.com/gprbuild-docs/html/gprbuild_ug.html GPRBuild] utility to build GNAT project files. GPRbuild is a generic build tool designed for the construction of multi-language systems. It is included with AdaCore's distribution of their proprietary GNAT compiler: [https://www.adacore.com/download] or can be built from source files obtained from AdaCore's [https://github.com/AdaCore/gprbuild github profile]. Older versions of GNAT distributed by the Free Software Foundation were able to build GNAT project files using the <tt>gnatmake</tt> utility, however newer versions do not support all the required features.
For this tutorial, we will use the system GNAT compiler to build for a PC i386. GNAT is part of GCC. Later I will show how to build an arm-elf compiler and tools. My OS is Debian testing 64-bit with GNAT 4.6.


The cross-compiler mentioned above will need to be properly integrated with GPRbuild and its associated tools. This can be accomplished by following the guide detailed [[GNAT_Cross-Compiler#GPRbuild_Integration|here]].
==GNAT and the Ada runtime system (RTS)==


== Ada run-time library ==
For this kernel we will be configuring a zero footprint RTS profile. This basically means, we have a compiler, tools and not much else.
{{Main|Ada Runtime Library}}


The Ada run-time library is responsible for the implementation of the standard library as defined in the [http://ada-auth.org/standards/12rm/html/RM-TOC.html Ada Language Reference Manual]. Not all features outlined in the reference manual need to be implemented for every platform. Since the kernel will be running in a freestanding environment without the support of an underlying operating system, it will be built with what is known as a ''a zero-footprint run-time system'' (commonly abbreviated as 'ZFP'). A zero-footprint RTS will typically provide only the bare-minimum functionality for the target architecture.
===Directory structure===


The following steps will detail how to create an initial zero-footprint runtime library suitable for building the kernel.
We need a place to structure this kernel,

<source lang="bash">
The first step is to set up the directory structure to hold the runtime library. GNAT expects the runtime library to conform to the a set directory structure. It requires the presence of two directories: <tt>adalib</tt> and <tt>adainclude</tt>. <tt>adainclude</tt> contains the runtime package specifications and source files, which are used analogously to include files in C. <tt>adalib</tt> contains the compiled binary artifacts of the runtime library.

The following code demonstrates setting up a directory structure for the RTS:

<syntaxhighlight lang="bash">
mkdir -p bare_bones/src/pc
mkdir -p bare_bones/src/pc
cd bare_bones
cd bare_bones
Line 34: Line 39:
mkdir -p rts/src
mkdir -p rts/src
mkdir -p obj
mkdir -p obj
</syntaxhighlight>
</source>


The best way to begin creating a new run-time library is to base it upon the existing source files obtained from the system GNAT installation, modifying them where necessary.
===RTS files to copy===


You will need to copy the following files from your compiler's RTS directory into rts/src and then create symbolic links from them to rts/boards/<arch>/adainclude where your ''arch'' is i386 or arm, etc.
The following code demonstrates copying the required files from the host system's GNAT installation's run-time library into <tt>rts/src</tt> and then creating symbolic links to <tt>rts/boards/${arch}/adainclude</tt>. Where <tt>${arch}</tt> is <tt>i386</tt>, <tt>armv6</tt>, etc. The source location will need to be modified to reflect the location of the system compiler.


<syntaxhighlight lang="bash">
'''N.B:''' You need to modify the location where these files are copied from, I've just used the location from my machine, which is most likely different to yours.
system_compiler_dir="/usr/lib/gcc/x86_64-linux-gnu/8"
rts_dir="${PWD}/rts"


for f in "ada.ads" "a-unccon.ads" "a-uncdea.ads" "gnat.ads" "g-souinf.ads" \
<source lang="bash">
for f in "ada.ads" "a-unccon.ads" "a-uncdea.ads" "gnat.ads" "g-souinf.ads" "interfac.ads" "s-atacco.adb" "s-atacco.ads" "s-maccod.ads" "s-stoele.adb" "s-stoele.ads"
"interfac.ads" "s-atacco.adb" "s-atacco.ads" "s-maccod.ads" "s-stoele.adb" \
"s-stoele.ads"
do
do
cp /usr/lib/gcc/x86_64-linux-gnu/4.6/adainclude/$f rts/src/
cp "${system_compiler_dir}/adainclude/$f" "${rts_dir}/src/"
ln -s `pwd`/rts/src/$f `pwd`/rts/boards/i386/adainclude/$f
ln -s "${rts_dir}/src/$f" "${rts_dir}/rts/boards/i386/adainclude/$f"
done
done
</syntaxhighlight>
</source>


==Files==
==Files==
Line 54: Line 62:
===gnat.adc===
===gnat.adc===


During compilation the GNAT compiler will search for a file named <tt>gnat.adc</tt> within the current working directory. This file is expected to contain one or more configuration pragmas that will be applied to the current compilation. These pragma directives instruct the compiler what features of the runtime should be restricted. This is of particular importance to kernel development since many of Ada's features are not supported by a freestanding, bare-metal environment.
This file in the root directory of the build tells GNAT there are some configuration pragmas to apply to the build. These pragmas can also be placed at the start of your custom sytem.ads (see below), but we'll place them here for now.


These pragma directives can alternatively be placed at the start of the runtime library's <tt>system.ads</tt> file (see below), however convention dictates using the <tt>gnat.adc</tt> file for this purpose.
What these do is to tell GNAT how much of the RTS we can use in our kernel, which is not a lot really.


Additionally, it is possible to apply additional global configuration files by appending the following line to the <tt>Builder</tt> package in a GNAT project file:
<source lang="ada">

<syntaxhighlight lang="ada">
package Builder is
-- ...
for Global_Configuration_Pragmas use "kernel.adc";
end Builder;
</syntaxhighlight>

It is also possible to instruct the compiler to apply additional files containing configuration pragmas to the current compilation using the switch <tt>-gnatec=path</tt> on the command line. These configuration pragmas are applied in addition to those found in <tt>gnat.adc</tt>, if it is present.
More information on configuration files can be found in the GNAT documentation: [https://gcc.gnu.org/onlinedocs/gnat_ugn/Handling-of-Configuration-Pragmas.html]

The GNAT Reference Manual and the Ada Reference Manual provide information on the various configuration pragmas.
See below for a list of restriction pragmas useful for a bare bones kernel and runtime:

<syntaxhighlight lang="ada">
pragma Discard_Names;
pragma Discard_Names;
pragma Restrictions (No_Enumeration_Maps);
pragma Restrictions (No_Enumeration_Maps);
pragma Normalize_Scalars;
pragma Normalize_Scalars;
pragma Restrictions (No_Exception_Handlers);
pragma Restrictions (No_Exception_Propagation);
pragma Restrictions (No_Finalization);
pragma Restrictions (No_Finalization);
pragma Restrictions (No_Tasking);
pragma Restrictions (No_Tasking);
Line 72: Line 95:
pragma Restrictions (No_Implicit_Dynamic_Code);
pragma Restrictions (No_Implicit_Dynamic_Code);
pragma Restrictions (No_Secondary_Stack);
pragma Restrictions (No_Secondary_Stack);
</syntaxhighlight>
</source>


'''Note:''' ''Do not use <tt>pragma No_Run_Time</tt>. It is obsolete.''
By passing the '''-r''' flag to the binder (inside the bare_bones.gpr file), the binder wil list further restrictions you can apply to enforce further checks.

<pre>
package Binder is
for Default_Switches ("Ada") use ("-r");
end Binder;
</pre>


Below is an explanation of these configuration pragmas:
====Discard_Names====


{| {{wikitable}}
In Ada, the compiler generates strings for various data types, e.g. enumerations, these strings can then be used in I/O.
! Restriction
! Purpose
|-
| Discard_Names || The Ada compiler automatically generates strings containing the names of enumerated data types, among others. These strings can be used in I/O.


<source lang="ada">
<syntaxhighlight lang="ada">
type Fruit is (Orange, Banana, Apple);
type Fruit is (Orange, Banana, Apple);
-- Ada defines the following strings, "Orange", "Banana" and "Apple" in an array.
-- Ada defines the following strings, "Orange", "Banana" and "Apple" in an array.
Line 93: Line 114:
Put (Fruit'Image (Orange));
Put (Fruit'Image (Orange));
-- Prints "Orange" to the console.
-- Prints "Orange" to the console.
</syntaxhighlight>
</source>


This pragma tells the compiler not to generate these tables.
This directive instructs the compiler not to generate these strings.
|-
| Normalize_Scalars || Forces all scalars to be initialised. Refer to [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/Pragma-Normalize_005fScalars.html#Pragma-Normalize_005fScalars GNAT RM:Normalize_Scalars] for more information.
|-
| No_Exception_Propagation || This directive forces the compiler to disallow any attempt to raise an exception over a subprogram boundary. Refer to [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fException_005fPropagation.html#No_005fException_005fPropagation GNAT RM:No_Exception_Propagation] for more information.


Note: The [http://docs.adacore.com/gnat-hie-docs/html/gnathie_ug_3.html#SEC8 GNAT High Integrity Edition] documentation states the following:
====Normalize_Scalars====


<blockquote>
Forces all scalars to be initialised, see the latest [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/Pragma-Normalize_005fScalars.html#Pragma-Normalize_005fScalars GNAT RM:Normalize_Scalars] for more information.
Exception declarations and raise statements are still permitted under this restriction. A raise statement is compiled into a call of <tt>__gnat_last_chance_handler</tt>.
</blockquote>


All exceptions that are not handled with an explicit exception handler within its subprogram will be caught with the <tt>Last_Chance_Handler</tt> subprogram. This will cause a warning to be issued at compile time.
====No_Exception_Propagation====
|-
| No_Exception_Registration || Ensures no stream operations are performed on types declared in Ada.Exceptions. See [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fException_005fRegistration.html#No_005fException_005fRegistration GNAT RM:No_Exception_Registration] for more information.
|-
| No_Finalization
| Restricts the use of controlled types. Refer to [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fFinalization.html#No_005fFinalization GNAT RM:No_Finalization] for more information.
|-
| No_Tasking
| This directive restricts all features related to tasking, including the use of protected objects. Refer to [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fTasking.html#No_005fTasking GNAT RM:No_Tasking] for more information.
|-
| No_Protected_Types
| This reinforces the above restriction. Refer to [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fProtected_005fTypes.html#No_005fProtected_005fTypes GNAT RM:No_Protected_Types] for more information.
|-
| No_Delay
| Restricts the use of <tt>delay</tt> statements or the calendar package. Refer to [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fDelay.html#No_005fDelay GNAT RM:No_Delay] for more information.
|-
| No_Recursion
| Restricts the use of recursion. Refer to [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fRecursion.html#No_005fRecursion GNAT RM:No_Recursion] for more information.
|-
| No_Allocators
| Restricts the use of dynamic memory. This is essential for a bare-metal application, since there is no underlying facility for allocation of dynamic memory. Refer to [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fAllocators.html#No_005fAllocators GNAT RM:No_Allocators] for more information.
|-
| No_Dispatch
| Disallows calling a subprogram using Ada's object-orientated mechanism. Refer to [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fDispatch.html#No_005fDispatch GNAT RM:No_Dispatch] for more information.
|-
| No_Implicit_Dynamic_Code
| Disallows nested subprograms or any other features that generate trampolines on the stack. Refer to [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fImplicit_005fDynamic_005fCode.html#No_005fImplicit_005fDynamic_005fCode GNAT RM:No_Implicit_Dynamic_Code] for more information.
|-
| No_Secondary_Stack
| Ada uses a ''secondary stack'' to return unconstrained types, such as arbitrary length strings or variant records. This directive instructs the compiler that there is no secondary stack present, and to restrict the use of code that requires one. Refer to [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fSecondary_005fStack.html#No_005fSecondary_005fStack GNAT RM:No_Secondary_Stack] for more information.
|}


Passing the <tt>-r</tt> flag to the binder instructs it to emit a list of further restrictions that are possible to apply to the project.
This forces the compiler to disallow any attempt to raise an exception over a subprogram boundary. All exceptions are caught with the Last_Chance_Handler subprogram. See [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fException_005fPropagation.html#No_005fException_005fPropagation GNAT RM:No_Exception_Propagation] for more information.
<syntaxhighlight lang="ada">

package Binder is
====No_Finalization====
for Default_Switches ("Ada") use ("-r");

end Binder;
Controlled types cannot be used, see [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fFinalization.html#No_005fFinalization GNAT RM:No_Finalization] for more information.
</syntaxhighlight>

====No_Tasking====

Turns off tasking, so you cannot define tasks or protected objects or do anything related to tasking, see [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fTasking.html#No_005fTasking GNAT RM:No_Tasking] for more information.

====No_Protected_Types====

This is pretty much here for reinforcement of the above restriction. See [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fProtected_005fTypes.html#No_005fProtected_005fTypes GNAT RM:No_Protected_Types] for more information.

====No_Delay====

You cannot use delay statements or the calendar package, see [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fDelay.html#No_005fDelay GNAT RM:No_Delay] for more information.

====No_Recursion====

Should be self evident, see [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fRecursion.html#No_005fRecursion GNAT RM:No_Recursion] for more information.

====No_Allocators====

You cannor use dynamic memory, see [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fAllocators.html#No_005fAllocators GNAT RM:No_Allocators] for more information.

====No_Dispatch====

You cannot call a subprogram using Ada's object-orientated mechanism, see [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fDispatch.html#No_005fDispatch GNAT RM:No_Dispatch] for more information.

====No_Implicit_Dynamic_Code====

You cannot use nested subprograms or any other features that generate trampolines on the stack, see [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fImplicit_005fDynamic_005fCode.html#No_005fImplicit_005fDynamic_005fCode GNAT RM:No_Implicit_Dynamic_Code] for more information.

====No_Secondary_Stack====

Without a secondary stack, you cannot return unconstrained types, such as strings or variant records, see [http://gcc.gnu.org/onlinedocs/gcc-4.7.0/gnat_rm/No_005fSecondary_005fStack.html#No_005fSecondary_005fStack GNAT RM:No_Secondary_Stack] for more information.

What this also means is you cannot use the runtime features 'Image and 'Val on any types, this would be useful for sending debugging info to the console, i.e. means you don't have to write your own code for converting strings to/from numbers.

I believe that it would be a good idea to have a small secondary stack defined in the assembler startup code, but define your own System.Secondary_Stack (s-secsta.ad[sb]) package which provides the correct API. Inside this package in it's executable part, you could then import the secondary stack from the assembly code, this would then be executed on elaboration of the package at the start of the kernel's execution.


===system.ads===
===system.ads===
Line 149: Line 171:
Every Ada program must have access to the System package, this essentially tells the compiler what kind of hardware we are building for, therefore there will be 1 system.ads file per architecture your kernel supports.
Every Ada program must have access to the System package, this essentially tells the compiler what kind of hardware we are building for, therefore there will be 1 system.ads file per architecture your kernel supports.


Copy a system.ads from GCC that matches the target you are working with, in our case this is gcc-<version>/gcc/ada/system-linux-x86.ads, name it system.ads and place it into rts/boards/i386/ we need to edit this a bit.
Copy a system.ads from GCC that matches the target you are working with, in our case this is gcc-<version>/gcc/ada/system-linux-x86.ads, name it system.ads and place it into rts/boards/i386/adainclude/ we need to edit this a bit.


We don't need to change anything from the top as all the integer sizes should be correct. Go to the private part of the spec and change the following values:
We don't need to change anything from the top as all the integer sizes should be correct. Go to the private part of the spec and change the following values:
Line 162: Line 184:


For more information on these options, see gcc-<version>/gcc/ada/targparm.ads.
For more information on these options, see gcc-<version>/gcc/ada/targparm.ads.

Also, add the following line in the private part of the package:

<syntaxhighlight lang="ada">
private

Run_Time_Name : constant String := "Bare Bones Run Time";
</syntaxhighlight>

According to targparm.ads, it must be the first thing after the private keyword. It should also show up in error messages in parentheses, but I've not managed to get it to show up thus far. This is useful as it should show you which RTS you are currently using, just in case you configure your build incorrectly.


===Last chance handler===
===Last chance handler===


The Ada runtime requires the presence of a <tt>Last_Chance_Handler</tt> subprogram. This subprogram is used as a handler for any exceptions that are not explicitly handled within their subprogram. These calls to the <tt>Last_Chance_Handler</tt> procedure in the case of unhandled exceptions will be automatically generated by the compiler.
When you start to write and compile Ada using this custom environment, the compiler will automatically place calls from the runtime into your final binary (this is what the compiler normally does, but we've restricted it a lot). One of these calls is to '''Last_Chance_Handler''' so create 2 new files and place into ''rts/boards/'''<arch>'''/adainclude'', as follows.

In order to facilitate this the last chance handler procedure must be defined somewhere within the program. Typically this is defined within the runtime. The last chance handler procedure may have any name, however the compiler will search for a procedure with external linkage with the name <tt>__gnat_last_chance_handler</tt>.

Create the following files in the <tt>rts/boards/${arch}/adainclude</tt>:


====last_chance_handler.ads====
====last_chance_handler.ads====


<source lang="ada">
<syntaxhighlight lang="ada">
with System;
with System;


Line 175: Line 211:
(Source_Location : System.Address; Line : Integer);
(Source_Location : System.Address; Line : Integer);
pragma Export (C, Last_Chance_Handler, "__gnat_last_chance_handler");
pragma Export (C, Last_Chance_Handler, "__gnat_last_chance_handler");
</syntaxhighlight>
</source>


====last_chance_handler.adb====
====last_chance_handler.adb====


<source lang="ada">
<syntaxhighlight lang="ada">
procedure Last_Chance_Handler
procedure Last_Chance_Handler
(Source_Location : System.Address; Line : Integer) is
(Source_Location : System.Address; Line : Integer) is
pragma Unreferenced (Source_Location, Line);
pragma Unreferenced (Source_Location, Line);
begin
begin
-- TODO: Add in code to dump the info to serial/screen which
-- TODO: Add in board-specific code to dump exception information to serial/screen.
-- is obviously board specific.
loop
loop
null;
null;
end loop;
end loop;
end Last_Chance_Handler;
end Last_Chance_Handler;
</syntaxhighlight>
</source>


The contents of the <tt>Last_Chance_Handler</tt> procedure will need to be tailored to the specific platform of the kernel. Typically this procedure will dump information regarding the exception to output media such as a [[Serial_Ports|serial port]].
As you can see, the meat of the handler is actualy a null loop at the moment, this is something you need to complete for your OS kernel and also, per platform.


===Compiling the runtime===
===Compiling the runtime===


Create a file called gnat.gpr in the root directory and copy the following into it:
Create a file called <tt>gnat.gpr</tt> in the root directory with the following contents:


<syntaxhighlight lang="ada">
<pre>
library project gnat is
library project gnat is
type Arch_Name is ("i386", "arm");
type Arch_Name is ("i386", "arm");
Line 217: Line 252:


package Builder is
package Builder is
Basic_Switches := ("-gnat2005", "-g", "-x", "-a", "-gnatg");
Basic_Switches := ("-gnat2005", "-g", "-x", "-a", "-gnatg",
"-gnatec=../gnat.adc");


case Board is
case Board is
Line 238: Line 274:
for Library_Dir use "rts/boards/" & Arch & "/adalib";
for Library_Dir use "rts/boards/" & Arch & "/adalib";
end gnat;
end gnat;
</syntaxhighlight>
</pre>


Now compile with the following command:
Now compile with the following command:


<source lang="bash">
<syntaxhighlight lang="bash">
gnatmake -XBoard=pc -Pgnat.gpr
gprbuild -XBoard=pc -Pgnat.gpr
</syntaxhighlight>
</source>


Inside rts/boards/i386/adainclude/ you should have the RTS sources symbolically linked along with the custom last_chance_hander and system files. Inside rts/boards/i386/adalib/ you should have the libgnat-4.6.a and also *.ali matching the source which are required by GNAT.
Inside <tt>rts/boards/i386/adainclude/</tt> the RTS sources should be symbolically linked along with the custom <tt>last_chance_hander</tt> and system files. Inside <tt>rts/boards/i386/adalib/</tt> there should be the <tt>libgnat-4.6.a</tt> file as well as <tt>*.ali</tt> matching the source files, which are required by GNAT.

''Note: Please note that the above lib might need it's name changed as some OSes might have built libgnat with a version number different to the one used here. The system libnat version may be <tt>libgnat-4.4.a</tt> and GNAT will expect to find that, so change the name in the GPR file accordingly.''


===startup.s===
===startup.s===


This is PC specific so place this in the src/pc directory.
This is the main system bootstrapping code. This version is PC specific, so place this in the <tt>src/pc</tt> directory.


''Note: This source file is written using GAS syntax.''
====GAS====


<source lang="asm">
<syntaxhighlight lang="asm">
.global startup # making entry point visible to linker
.global startup # Make the startup entry point symbol visible to the linker


# setting up the Multiboot header - see GRUB docs for details
# Set up the Multiboot header (see GRUB docs for details)
.set ALIGN, 1<<0 # align loaded modules on page boundaries
.set ALIGN, 1<<0 # Align loaded modules on page boundaries
.set MEMINFO, 1<<1 # provide memory map
.set MEMINFO, 1<<1 # Provide memory map
.set FLAGS, ALIGN | MEMINFO # this is the Multiboot 'flag' field
.set FLAGS, ALIGN | MEMINFO # This is the Multiboot 'flag' field
.set MAGIC, 0x1BADB002 # 'magic number' lets bootloader find the header
.set MAGIC, 0x1BADB002 # 'magic number' lets your bootloader find the header
.set CHECKSUM, -(MAGIC + FLAGS) # checksum required
.set CHECKSUM, -(MAGIC + FLAGS) # Checksum required


header: # Must be in the first 8kb of the kernel file
.align 4
.align 4, 0x90 # Pad the location counter (in the current subsection) to a 4-byte
# storage boundary. The way alignment is specified can vary with
# host system architecture.
.long MAGIC
.long MAGIC
.long FLAGS
.long FLAGS
.long CHECKSUM
.long CHECKSUM


# reserve initial kernel stack space
# Reserve initial kernel stack space
.set STACKSIZE, 0x4000 # that is, 16k.
.set STACKSIZE, 0x4000 # 0x4000 being 16k.
.lcomm stack, STACKSIZE # Reserve 16k stack on a32-bit boundary
# On my binutils the following line didn't work as the .lcomm instruction takes 2 parameters.
#.lcomm stack, STACKSIZE, 32 # reserve 16k stack on a doubleword boundary
.comm mbd, 4 # Declare common symbol mbd, allocate it 4-bytes of
.lcomm stack, STACKSIZE # reserve 16k stack
# uninitialized memory.
.comm mbd, 4 # we will use this in kmain
.comm magic, 4 # Declare common symbol magic, allocate it 4-bytes of
.comm magic, 4 # we will use this in kmain
# uninitialized memory.


startup:
startup:
movl $(stack + STACKSIZE), %esp # set up the stack
movl $(stack + STACKSIZE), %esp # Set up the stack

movl %eax, magic # Multiboot magic number
# The following saves the contents of the registers as they will likely be
movl %ebx, mbd # Multiboot data structure
# overwritten because main is not our actual entry point, Bare_Bones is. We
# will use these 2 symbols inside Bare_Bones.

movl %eax, magic # EAX indicates to the OS that it was loaded by a Multiboot-compliant boot
# loader
movl %ebx, mbd # EBX must contain the physical address of the Multiboot information
# structure

call main # Call main (created by gnatbind)


call main # call main created by gnatbind
cli # Disable interrupts. then intentionally hang the system.
# CLI only affects the interrupt flag for the processor on which it is
# executed.


cli
hang:
hang:
hlt # halt machine should kernel return
hlt # Because the HLT instruction halts until an interrupt occurs, the
# combination of a CLI followed by a HLT is used to intentionally hang
# the computer if the kernel returns.
# HLT is in a loop just in case a single HLT instruction fails to execute
# for some reason, (such as in the case of an NMI).
jmp hang
jmp hang
</syntaxhighlight>
</source>


===multiboot.ads===
===multiboot.ads===


I won't show the source to this package as I have made it more Ada-like and it's quite large, so I will link to the current versions, [https://github.com/Lucretia/bare_bones/blob/master/src/pc/multiboot.ads multiboot.ads] and [https://github.com/Lucretia/bare_bones/blob/master/src/pc/multiboot.adb multiboot.adb].
<source lang="ada">
-- TODO
</source>


===Console===
===Console===
Line 304: Line 357:
====console.ads====
====console.ads====


<source lang="ada">
<syntaxhighlight lang="ada">
with System;
with System;


Line 423: Line 476:
procedure Clear (Background : in Background_Colour := Black);
procedure Clear (Background : in Background_Colour := Black);
end Console;
end Console;
</syntaxhighlight>
</source>


====console.adb====
====console.adb====


<source lang="ada">
<syntaxhighlight lang="ada">
package body Console is
package body Console is
procedure Put
procedure Put
Line 466: Line 519:
end Clear;
end Clear;
end Console;
end Console;
</syntaxhighlight>
</source>


===bare_bones.adb===
===bare_bones.adb===
Line 472: Line 525:
This is platform independent and therefore goes into the src directory.
This is platform independent and therefore goes into the src directory.


<source lang="ada">
<syntaxhighlight lang="ada">
with Console; use Console;
with Console; use Console;


Line 484: Line 537:
end Bare_Bones;
end Bare_Bones;
pragma No_Return (Bare_Bones);
pragma No_Return (Bare_Bones);
</syntaxhighlight>
</source>


===linker.ld===
===linker.ld===
Line 532: Line 585:
Place this file in the root directory.
Place this file in the root directory.


<syntaxhighlight lang="make">
<pre>
ARCH = i386
ARCH = i386
RTS_DIR = `pwd`/rts/boards/$(ARCH)
RTS_DIR = `pwd`/rts/boards/$(ARCH)


ifeq ($(ARCH),i386)
ifeq ($(ARCH),i386)
GPRBUILD = gprbuild
GNATMAKE = gnatmake
AS = as
AS = as
ASFLAGS = --32 -march=i386
ASFLAGS = --32 -march=i386
Line 551: Line 604:


bare_bones: $(OBJS) src/bare_bones.adb
bare_bones: $(OBJS) src/bare_bones.adb
$(GNATMAKE) --RTS=$(RTS_DIR) -XBoard=$(BOARD) -Pbare_bones.gpr
$(GPRBUILD) --RTS=$(RTS_DIR) -XBoard=$(BOARD) -Pbare_bones.gpr


obj/startup.o: src/$(BOARD)/startup.s
obj/startup.o: src/$(BOARD)/startup.s
Line 560: Line 613:
clean:
clean:
-rm obj/* *~ bare_bones
-rm obj/* *~ bare_bones
</syntaxhighlight>
</pre>


===bare_bones.gpr===
===bare_bones.gpr===
Line 566: Line 619:
Place this file in the root directory.
Place this file in the root directory.


<syntaxhighlight lang="ada">
<pre>
project Bare_Bones is
project Bare_Bones is
type Arch_Name is ("i386", "arm");
type Arch_Name is ("i386", "arm");
Line 621: Line 674:
end Linker;
end Linker;
end Bare_Bones;
end Bare_Bones;
</syntaxhighlight>
</pre>

==Raspberry Pi==

===Boot process===

As stated in [http://www.raspberrypi.org/phpBB3/viewtopic.php?f=63&t=6685], the RPi boot proces is as follows:

# Power on starts the stage 1 boot loader which is on the SoC, which loads the stage 2 boot loader (bootcode.bin) into L2 cache (thus turning it on).
# bootcode.bin enables SDRAM and loads the stage 3 boot loader (loader.bin).
# loader.bin loads and executes the VideoCore firmware (start.elf).
# start.elf loads config.txt, cmdline.txt and kernel.img.

The config.txt file can contain aline "kernel=<name>" where you can name the kernel image anything you like.

Ideally for development we would use an emulator or some kind of netbooting facility so we don't have to keep switching the SD Card from the Pi to the PC and vice versa, this would get tedious really fast.

===U-Boot===

Seems you need serial access to the board to do this, so I won't be atempting this yet.

By following the information starting on the [http://kernelnomicon.org/?p=92 FreeBSD] porting page, we can build u-boot for RPi.


<syntaxhighlight lang="bash">
git clone git://github.com/gonzoua/u-boot-pi.git

cd u-boot-pi
make rpi_b_config
</syntaxhighlight>


==Testing==
==Testing==
Line 627: Line 709:
Make sure you have built the RTS above before this next stage otherwise you won't be able to compile the kernel.
Make sure you have built the RTS above before this next stage otherwise you won't be able to compile the kernel.


<source lang="bash">
<syntaxhighlight lang="bash">
make
make qemu
</syntaxhighlight>

qemu -kernel bare_bones
</source>


On the QEMU window, it should clear the screen, the the cursor won't move so it will be in the middle of the screen, in the top-left corner will be the message "Hello, bare bones in Ada."
On the QEMU window, it should clear the screen, the the cursor won't move so it will be in the middle of the screen, in the top-left corner will be the message "Hello, bare bones in Ada."


==Future==
==Source access==

I have created a Git repository on [https://github.com/Lucretia/bare_bones GitHub] containing the source above so you don't have to do it by hand if you don't want to.

In fact there have ben a lot of changes since I started this project and it is a better idea to grab the source from GitHub as it will be more up to date. I will leave the documents above so you can see how it's evolved and also how it works, maybe a bit clearer that it is now.

==Next Steps==


A useful next step for further developing the bare bones kernel outlined in this article is implementing capability for using the <tt>'Image</tt> attributes on scalar types. This facilitates the printing of integers in string form, which is extremely useful for kernel debugging. A simple guide on how to accomplish this is detailed [[Ada_Runtime_Library#Image_attributes|here]].
# Implement a Raspberry Pi version.
# Implement a secondary stack so we can use 'Image attributes.
# Implement a (U)EFI version.


[[Category:Bare bones tutorials]]
[[Category:Bare bones tutorials]]

Latest revision as of 05:19, 9 June 2024

The aim of this tutorial is to produce a small Multiboot kernel written in the Ada programming language.

Preface

One of the first things people ask on the Ada IRC channel on Freenode is "Can Ada be used for OS development?" to which the answer is a resounding yes.

Prerequisites

The following section details the prerequisite software necessary for successfully building the kernel.

Cross-Compiler

Main article: GNAT Cross-Compiler

This tutorial requires the use of a cross-compiler with Ada support targeting the i686-elf architecture. Theoretically, any Ada compiler capable of producing bare-metal ELF binaries for this architecture is suitable. The recommended compiler is GNAT, which is part of the GNU Compiler Collection maintained by the Free Software Foundation. Historically it was difficult to create cross-compiler builds for GNAT, but recent versions of GCC have made this much simpler. For a tutorial on how to set up a GNAT cross-compiler, refer to this article.

GPRbuild

This tutorial will require the use of the GPRBuild utility to build GNAT project files. GPRbuild is a generic build tool designed for the construction of multi-language systems. It is included with AdaCore's distribution of their proprietary GNAT compiler: [1] or can be built from source files obtained from AdaCore's github profile. Older versions of GNAT distributed by the Free Software Foundation were able to build GNAT project files using the gnatmake utility, however newer versions do not support all the required features.

The cross-compiler mentioned above will need to be properly integrated with GPRbuild and its associated tools. This can be accomplished by following the guide detailed here.

Ada run-time library

Main article: Ada Runtime Library

The Ada run-time library is responsible for the implementation of the standard library as defined in the Ada Language Reference Manual. Not all features outlined in the reference manual need to be implemented for every platform. Since the kernel will be running in a freestanding environment without the support of an underlying operating system, it will be built with what is known as a a zero-footprint run-time system (commonly abbreviated as 'ZFP'). A zero-footprint RTS will typically provide only the bare-minimum functionality for the target architecture.

The following steps will detail how to create an initial zero-footprint runtime library suitable for building the kernel.

The first step is to set up the directory structure to hold the runtime library. GNAT expects the runtime library to conform to the a set directory structure. It requires the presence of two directories: adalib and adainclude. adainclude contains the runtime package specifications and source files, which are used analogously to include files in C. adalib contains the compiled binary artifacts of the runtime library.

The following code demonstrates setting up a directory structure for the RTS:

mkdir -p bare_bones/src/pc
cd bare_bones
mkdir -p rts/boards/i386/adalib
mkdir -p rts/boards/i386/adainclude
mkdir -p rts/src
mkdir -p obj

The best way to begin creating a new run-time library is to base it upon the existing source files obtained from the system GNAT installation, modifying them where necessary.

The following code demonstrates copying the required files from the host system's GNAT installation's run-time library into rts/src and then creating symbolic links to rts/boards/${arch}/adainclude. Where ${arch} is i386, armv6, etc. The source location will need to be modified to reflect the location of the system compiler.

system_compiler_dir="/usr/lib/gcc/x86_64-linux-gnu/8"
rts_dir="${PWD}/rts"

for f in "ada.ads" "a-unccon.ads" "a-uncdea.ads" "gnat.ads" "g-souinf.ads" \
"interfac.ads" "s-atacco.adb" "s-atacco.ads" "s-maccod.ads" "s-stoele.adb" \
"s-stoele.ads"
do
	cp "${system_compiler_dir}/adainclude/$f" "${rts_dir}/src/"
	ln -s "${rts_dir}/src/$f" "${rts_dir}/rts/boards/i386/adainclude/$f"
done

Files

gnat.adc

During compilation the GNAT compiler will search for a file named gnat.adc within the current working directory. This file is expected to contain one or more configuration pragmas that will be applied to the current compilation. These pragma directives instruct the compiler what features of the runtime should be restricted. This is of particular importance to kernel development since many of Ada's features are not supported by a freestanding, bare-metal environment.

These pragma directives can alternatively be placed at the start of the runtime library's system.ads file (see below), however convention dictates using the gnat.adc file for this purpose.

Additionally, it is possible to apply additional global configuration files by appending the following line to the Builder package in a GNAT project file:

   package Builder is
      --  ...
      for Global_Configuration_Pragmas use "kernel.adc";
   end Builder;

It is also possible to instruct the compiler to apply additional files containing configuration pragmas to the current compilation using the switch -gnatec=path on the command line. These configuration pragmas are applied in addition to those found in gnat.adc, if it is present. More information on configuration files can be found in the GNAT documentation: [2]

The GNAT Reference Manual and the Ada Reference Manual provide information on the various configuration pragmas. See below for a list of restriction pragmas useful for a bare bones kernel and runtime:

pragma Discard_Names;
pragma Restrictions (No_Enumeration_Maps);
pragma Normalize_Scalars;
pragma Restrictions (No_Exception_Propagation);
pragma Restrictions (No_Finalization);
pragma Restrictions (No_Tasking);
pragma Restrictions (No_Protected_Types);
pragma Restrictions (No_Delay);
pragma Restrictions (No_Recursion);
pragma Restrictions (No_Allocators);
pragma Restrictions (No_Dispatch);
pragma Restrictions (No_Implicit_Dynamic_Code);
pragma Restrictions (No_Secondary_Stack);

Note: Do not use pragma No_Run_Time. It is obsolete.

Below is an explanation of these configuration pragmas:

Restriction Purpose
Discard_Names The Ada compiler automatically generates strings containing the names of enumerated data types, among others. These strings can be used in I/O.
type Fruit is (Orange, Banana, Apple);
--  Ada defines the following strings, "Orange", "Banana" and "Apple" in an array.

--  These strings can be accessed using the 'Image attribute, as in
Put (Fruit'Image (Orange));
--  Prints "Orange" to the console.

This directive instructs the compiler not to generate these strings.

Normalize_Scalars Forces all scalars to be initialised. Refer to GNAT RM:Normalize_Scalars for more information.
No_Exception_Propagation This directive forces the compiler to disallow any attempt to raise an exception over a subprogram boundary. Refer to GNAT RM:No_Exception_Propagation for more information.

Note: The GNAT High Integrity Edition documentation states the following:

Exception declarations and raise statements are still permitted under this restriction. A raise statement is compiled into a call of __gnat_last_chance_handler.

All exceptions that are not handled with an explicit exception handler within its subprogram will be caught with the Last_Chance_Handler subprogram. This will cause a warning to be issued at compile time.

No_Exception_Registration Ensures no stream operations are performed on types declared in Ada.Exceptions. See GNAT RM:No_Exception_Registration for more information.
No_Finalization Restricts the use of controlled types. Refer to GNAT RM:No_Finalization for more information.
No_Tasking This directive restricts all features related to tasking, including the use of protected objects. Refer to GNAT RM:No_Tasking for more information.
No_Protected_Types This reinforces the above restriction. Refer to GNAT RM:No_Protected_Types for more information.
No_Delay Restricts the use of delay statements or the calendar package. Refer to GNAT RM:No_Delay for more information.
No_Recursion Restricts the use of recursion. Refer to GNAT RM:No_Recursion for more information.
No_Allocators Restricts the use of dynamic memory. This is essential for a bare-metal application, since there is no underlying facility for allocation of dynamic memory. Refer to GNAT RM:No_Allocators for more information.
No_Dispatch Disallows calling a subprogram using Ada's object-orientated mechanism. Refer to GNAT RM:No_Dispatch for more information.
No_Implicit_Dynamic_Code Disallows nested subprograms or any other features that generate trampolines on the stack. Refer to GNAT RM:No_Implicit_Dynamic_Code for more information.
No_Secondary_Stack Ada uses a secondary stack to return unconstrained types, such as arbitrary length strings or variant records. This directive instructs the compiler that there is no secondary stack present, and to restrict the use of code that requires one. Refer to GNAT RM:No_Secondary_Stack for more information.

Passing the -r flag to the binder instructs it to emit a list of further restrictions that are possible to apply to the project.

   package Binder is
      for Default_Switches ("Ada") use ("-r");
   end Binder;

system.ads

Every Ada program must have access to the System package, this essentially tells the compiler what kind of hardware we are building for, therefore there will be 1 system.ads file per architecture your kernel supports.

Copy a system.ads from GCC that matches the target you are working with, in our case this is gcc-<version>/gcc/ada/system-linux-x86.ads, name it system.ads and place it into rts/boards/i386/adainclude/ we need to edit this a bit.

We don't need to change anything from the top as all the integer sizes should be correct. Go to the private part of the spec and change the following values:

  1. Command_Line_Args => False
  2. Configurable_Run_Time => True
  3. Exit_Status_Supported => False
  4. Stack_Check_Probes => False
  5. Suppress_Standard_Library => True
  6. ZCX_By_Default => False
  7. GCC_ZCX_Support => False

For more information on these options, see gcc-<version>/gcc/ada/targparm.ads.

Also, add the following line in the private part of the package:

private

   Run_Time_Name : constant String := "Bare Bones Run Time";

According to targparm.ads, it must be the first thing after the private keyword. It should also show up in error messages in parentheses, but I've not managed to get it to show up thus far. This is useful as it should show you which RTS you are currently using, just in case you configure your build incorrectly.

Last chance handler

The Ada runtime requires the presence of a Last_Chance_Handler subprogram. This subprogram is used as a handler for any exceptions that are not explicitly handled within their subprogram. These calls to the Last_Chance_Handler procedure in the case of unhandled exceptions will be automatically generated by the compiler.

In order to facilitate this the last chance handler procedure must be defined somewhere within the program. Typically this is defined within the runtime. The last chance handler procedure may have any name, however the compiler will search for a procedure with external linkage with the name __gnat_last_chance_handler.

Create the following files in the rts/boards/${arch}/adainclude:

last_chance_handler.ads

with System;

procedure Last_Chance_Handler
  (Source_Location : System.Address; Line : Integer);
pragma Export (C, Last_Chance_Handler, "__gnat_last_chance_handler");

last_chance_handler.adb

procedure Last_Chance_Handler
  (Source_Location : System.Address; Line : Integer) is
   pragma Unreferenced (Source_Location, Line);
begin
   --  TODO: Add in board-specific code to dump exception information to serial/screen.
   loop
      null;
   end loop;
end Last_Chance_Handler;

The contents of the Last_Chance_Handler procedure will need to be tailored to the specific platform of the kernel. Typically this procedure will dump information regarding the exception to output media such as a serial port.

Compiling the runtime

Create a file called gnat.gpr in the root directory with the following contents:

library project gnat is
   type Arch_Name is ("i386", "arm");
   type Board_Name is ("pc", "rpi");

   Arch  : Arch_Name  := "i386";
   Board : Board_Name := external ("Board");

   case Board is
      when "pc" =>
         Arch := "i386";
      when "rpi" =>
         Arch  := "arm";
   end case;

   for Source_Dirs use ("rts/boards/" & Arch & "/adainclude");
   for Object_Dir use "obj"; --"rts/boards/" & Arch & "/adalib";

   package Builder is
      Basic_Switches := ("-gnat2005", "-g", "-x", "-a", "-gnatg",
			 "-gnatec=../gnat.adc");

      case Board is
         when "pc" =>
            for Default_Switches ("Ada") use Basic_Switches &
               ("-m32", "-march=i386");
         when "rpi" =>
            for Default_Switches ("Ada") use Basic_Switches &
               ("-march=armv6zk", "-mfpu=vfp", "-mfloat-abi=hard", "-marm",
                "-mcpu=arm1176jzf-s", "-mtune=arm1176jzf-s");
      end case;
   end Builder;

   package Compiler is
      for Default_Switches ("Ada") use ("-O2", "-ffunction-sections", "-fdata-sections");
   end Compiler;

   for Library_Kind use "static";
   for Library_Name use "gnat-4.6";
   for Library_Dir use "rts/boards/" & Arch & "/adalib";
end gnat;

Now compile with the following command:

gprbuild -XBoard=pc -Pgnat.gpr

Inside rts/boards/i386/adainclude/ the RTS sources should be symbolically linked along with the custom last_chance_hander and system files. Inside rts/boards/i386/adalib/ there should be the libgnat-4.6.a file as well as *.ali matching the source files, which are required by GNAT.

Note: Please note that the above lib might need it's name changed as some OSes might have built libgnat with a version number different to the one used here. The system libnat version may be libgnat-4.4.a and GNAT will expect to find that, so change the name in the GPR file accordingly.

startup.s

This is the main system bootstrapping code. This version is PC specific, so place this in the src/pc directory.

Note: This source file is written using GAS syntax.

.global startup                         # Make the startup entry point symbol visible to the linker

# Set up the Multiboot header (see GRUB docs for details)
.set ALIGN,    1<<0                     # Align loaded modules on page boundaries
.set MEMINFO,  1<<1                     # Provide memory map
.set FLAGS,    ALIGN | MEMINFO          # This is the Multiboot 'flag' field
.set MAGIC,    0x1BADB002               # 'magic number' lets your bootloader find the header
.set CHECKSUM, -(MAGIC + FLAGS)         # Checksum required

header:                                 # Must be in the first 8kb of the kernel file
.align 4, 0x90                          # Pad the location counter (in the current subsection) to a 4-byte
                                        # storage boundary. The way alignment is specified can vary with
                                        # host system architecture.
.long MAGIC
.long FLAGS
.long CHECKSUM

# Reserve initial kernel stack space
.set STACKSIZE, 0x4000                  # 0x4000 being 16k.
.lcomm stack, STACKSIZE                 # Reserve 16k stack on a32-bit boundary
.comm  mbd, 4                           # Declare common symbol mbd, allocate it 4-bytes of
                                        # uninitialized memory.
.comm  magic, 4                         # Declare common symbol magic, allocate it 4-bytes of
                                        # uninitialized memory.

startup:
    movl  $(stack + STACKSIZE), %esp    # Set up the stack

# The following saves the contents of the registers as they will likely be
# overwritten because main is not our actual entry point, Bare_Bones is. We
# will use these 2 symbols inside Bare_Bones.

    movl  %eax, magic                   # EAX indicates to the OS that it was loaded by a Multiboot-compliant boot
                                        # loader
    movl  %ebx, mbd                     # EBX must contain the physical address of the Multiboot information
                                        # structure

    call  main                          # Call main (created by gnatbind)

    cli                                 # Disable interrupts. then intentionally hang the system.
                                        # CLI only affects the interrupt flag for the processor on which it is
                                        # executed.

hang:
    hlt                                 # Because the HLT instruction halts until an interrupt occurs, the
                                        # combination of a CLI followed by a HLT is used to intentionally hang
                                        # the computer if the kernel returns.
                                        # HLT is in a loop just in case a single HLT instruction fails to execute
                                        # for some reason, (such as in the case of an NMI).
    jmp   hang

multiboot.ads

I won't show the source to this package as I have made it more Ada-like and it's quite large, so I will link to the current versions, multiboot.ads and multiboot.adb.

Console

Disclaimer: I wrote this package a long time ago and have reformatted it using my current Ada programming style. I have not gone too far into the code, so it may not be the best implementation of an console.

The following 2 files give you access to the VGA console at 80x25 characters. As they are PC specific, they go into the src/pc directory.

console.ads

with System;

package Console is
   pragma Preelaborate (Console);

   type Background_Colour is
     (Black,
      Blue,
      Green,
      Cyan,
      Red,
      Magenta,
      Brown,
      Light_Grey);

   for Background_Colour use
     (Black      => 16#0#,
      Blue       => 16#1#,
      Green      => 16#2#,
      Cyan       => 16#3#,
      Red        => 16#4#,
      Magenta    => 16#5#,
      Brown      => 16#6#,
      Light_Grey => 16#7#);

   for Background_Colour'Size use 4;

   type Foreground_Colour is
     (Black,
      Blue,
      Green,
      Cyan,
      Red,
      Magenta,
      Brown,
      Light_Grey,
      Dark_Grey,
      Light_Blue,
      Light_Green,
      Light_Cyan,
      Light_Red,
      Light_Magenta,
      Yellow,
      White);

   for Foreground_Colour use
     (Black         => 16#0#,
      Blue          => 16#1#,
      Green         => 16#2#,
      Cyan          => 16#3#,
      Red           => 16#4#,
      Magenta       => 16#5#,
      Brown         => 16#6#,
      Light_Grey    => 16#7#,
      Dark_Grey     => 16#8#,
      Light_Blue    => 16#9#,
      Light_Green   => 16#A#,
      Light_Cyan    => 16#B#,
      Light_Red     => 16#C#,
      Light_Magenta => 16#D#,
      Yellow        => 16#E#,
      White         => 16#F#);

   for Foreground_Colour'Size use 4;

   type Cell_Colour is
      record
         Foreground : Foreground_Colour;
         Background : Background_Colour;
      end record;

   for Cell_Colour use
      record
         Foreground at 0 range 0 .. 3;
         Background at 0 range 4 .. 7;
      end record;

   for Cell_Colour'Size use 8;

   type Cell is
      record
         Char   : Character;
         Colour : Cell_Colour;
      end record;

   for Cell'Size use 16;

   Screen_Width  : constant Natural := 80;
   Screen_Height : constant Natural := 25;

   subtype Screen_Width_Range  is Natural range 1 .. Screen_Width;
   subtype Screen_Height_Range is Natural range 1 .. Screen_Height;

   type Row    is array (Screen_Width_Range)  of Cell;
   type Screen is array (Screen_Height_Range) of Row;

   Video_Memory : Screen;

   for Video_Memory'Address use System'To_Address (16#000B_8000#);

   pragma Import (Ada, Video_Memory);

   procedure Put
     (Char       : in Character;
      X          : in Screen_Width_Range;
      Y          : in Screen_Height_Range;
      Foreground : in Foreground_Colour := White;
      Background : in Background_Colour := Black);

   procedure Put
     (Str        : in String;
      X          : in Screen_Width_Range;
      Y          : in Screen_Height_Range;
      Foreground : in Foreground_Colour := White;
      Background : in Background_Colour := Black);

   procedure Clear (Background : in Background_Colour := Black);
end Console;

console.adb

package body Console is
   procedure Put
     (Char       : in Character;
      X          : in Screen_Width_Range;
      Y          : in Screen_Height_Range;
      Foreground : in Foreground_Colour := White;
      Background : in Background_Colour := Black) is
   begin
      Video_Memory (Y)(X).Char              := Char;
      Video_Memory (Y)(X).Colour.Foreground := Foreground;
      Video_Memory (Y)(X).Colour.Background := Background;
   end Put;

   procedure Put
      (Str        : in String;
       X          : in Screen_Width_Range;
       Y          : in Screen_Height_Range;
       Foreground : in Foreground_Colour := White;
       Background : in Background_Colour := Black) is
   begin
      for Index in Str'First .. Str'Last loop
         Put (Str (Index),
              X + Screen_Width_Range (Index) - 1,
              Y,
              Foreground,
              Background);
      end loop;
   end Put;

   procedure Clear (Background : in Background_Colour := Black) is
   begin
      for X in Screen_Width_Range'First .. Screen_Width_Range'Last loop
         for Y in Screen_Height_Range'First .. Screen_Height_Range'Last loop
            Put (' ', X, Y, Background => Background);
         end loop;
      end loop;
   end Clear;
end Console;

bare_bones.adb

This is platform independent and therefore goes into the src directory.

with Console; use Console;

procedure Bare_Bones is
begin
   Clear;

   Put ("Hello, bare bones in Ada.",
        Screen_Width_Range'First,
        Screen_Height_Range'First);
end Bare_Bones;
pragma No_Return (Bare_Bones);

linker.ld

This is a PC specific script so goes into the src/pc directory.

OUTPUT_FORMAT(elf32-i386)

/* Tell the linker which startup code to use, we do this as there is no way to do this (well not easily) from the GNAT tools. */
STARTUP(startup.o)

ENTRY (startup)

SECTIONS
{
    . = 0x00100000;

    .text :{
        code = .; _code = .; __code = .;
        *(.text)
        *(.rodata)
    }

    .rodata ALIGN (0x1000) : {
        *(.rodata)
    }

    .data ALIGN (0x1000) : {
        data = .; _data = .; __data = .;
        *(.data)
    }

    .bss : {
        sbss = .;
        bss = .; _bss = .; __bss = .;
        *(COMMON)
        *(.bss)
        ebss = .;
    }
    end = .; _end = .; __end = .;
}

makefile

Place this file in the root directory.

ARCH		=	i386
RTS_DIR		=	`pwd`/rts/boards/$(ARCH)

ifeq ($(ARCH),i386)
GPRBUILD	=	gprbuild
AS		=	as
ASFLAGS		=	--32 -march=i386

OBJS		=	obj/startup.o obj/multiboot.o obj/console.o
BOARD		=	pc

.PHONY: obj/multiboot.o obj/console.o

endif

all: bare_bones

bare_bones: $(OBJS) src/bare_bones.adb
	$(GPRBUILD) --RTS=$(RTS_DIR) -XBoard=$(BOARD) -Pbare_bones.gpr

obj/startup.o: src/$(BOARD)/startup.s
	$(AS) $(ASFLAGS) src/$(BOARD)/startup.s -o obj/startup.o

.PHONY: clean

clean:
	-rm obj/* *~ bare_bones

bare_bones.gpr

Place this file in the root directory.

project Bare_Bones is
   type Arch_Name is ("i386", "arm");
   type Board_Name is ("pc", "rpi");

   Arch  : Arch_Name  := "i386";
   Board : Board_Name := external ("Board");

   -- TODO: Add in a case statement that adds an arch dir to source.

   case Board is
      when "pc" =>
         for Source_Dirs use ("src", "src/pc");
      when "rpi" =>
         for Source_Dirs use ("src", "src/rpi");
   end case;

   for Object_Dir use "obj";
   for Exec_Dir use ".";
   for Main use ("bare_bones.adb");

   package Builder is
      Basic_Switches := ("-gnat2005", "-g", "-x", "-a", "-gnatg",
                         "-gnatec=../gnat.adc", "-gnaty-I", "-gnaty+d");

      case Board is
         when "pc" =>
            for Default_Switches ("Ada") use Basic_Switches &
               ("-m32", "-march=i386");
         when "rpi" =>
            for Default_Switches ("Ada") use Basic_Switches &
               ("-march=armv6zk", "-mfpu=vfp", "-mfloat-abi=hard", "-marm",
                "-mcpu=arm1176jzf-s", "-mtune=arm1176jzf-s");
      end case;
   end Builder;

   package Compiler is
      case Board is
         when "pc" =>
            for Default_Switches ("Ada") use
               ("-O0", "-g", "-ggdb", "-ffunction-sections", "-fdata-sections");
         when "rpi" =>
            for Default_Switches ("Ada") use
               ("-O0", "-g", "-ggdb", "-ffunction-sections", "-fdata-sections");

      end case;
   end Compiler;

-- To reduce size of final binary.
   package Linker is
      for Default_Switches ("Ada") use
         ("-Wl,--gc-sections", "-static", "-nostartfiles", "-nodefaultlibs",
          "-T../src/" & Board & "/linker.ld", "-v");
   end Linker;
end Bare_Bones;

Raspberry Pi

Boot process

As stated in [3], the RPi boot proces is as follows:

  1. Power on starts the stage 1 boot loader which is on the SoC, which loads the stage 2 boot loader (bootcode.bin) into L2 cache (thus turning it on).
  2. bootcode.bin enables SDRAM and loads the stage 3 boot loader (loader.bin).
  3. loader.bin loads and executes the VideoCore firmware (start.elf).
  4. start.elf loads config.txt, cmdline.txt and kernel.img.

The config.txt file can contain aline "kernel=<name>" where you can name the kernel image anything you like.

Ideally for development we would use an emulator or some kind of netbooting facility so we don't have to keep switching the SD Card from the Pi to the PC and vice versa, this would get tedious really fast.

U-Boot

Seems you need serial access to the board to do this, so I won't be atempting this yet.

By following the information starting on the FreeBSD porting page, we can build u-boot for RPi.


git clone git://github.com/gonzoua/u-boot-pi.git

cd u-boot-pi
make rpi_b_config

Testing

Make sure you have built the RTS above before this next stage otherwise you won't be able to compile the kernel.

make qemu

On the QEMU window, it should clear the screen, the the cursor won't move so it will be in the middle of the screen, in the top-left corner will be the message "Hello, bare bones in Ada."

Source access

I have created a Git repository on GitHub containing the source above so you don't have to do it by hand if you don't want to.

In fact there have ben a lot of changes since I started this project and it is a better idea to grab the source from GitHub as it will be more up to date. I will leave the documents above so you can see how it's evolved and also how it works, maybe a bit clearer that it is now.

Next Steps

A useful next step for further developing the bare bones kernel outlined in this article is implementing capability for using the 'Image attributes on scalar types. This facilitates the printing of integers in string form, which is extremely useful for kernel debugging. A simple guide on how to accomplish this is detailed here.