Writing Linux Modules in Ada - Part 1

Posted on Sun 23 October 2016 in misc View Comments

View on Github Star on Github

In the following series of blog posts I will document my attempts to write Linux modules using the Ada language.

I am not a professional and my knowledge of the Linux kernel and gnat is not comprehensive. Please pardon me for any inaccuracies I make.

Introduction and Motivation

Linux module is a binary that can be dynamically loaded and linked into the Kernel. One major use case is device drivers. Traditionally Linux modules are written in C and built using kbuild, an elaborate build system that is also used to build the kernel itself.

Code written for a module runs in the kernel with the privileges of the kernel, meaning that a programming error can cause anomalies such a system reboot, memory corruption of applications, data corruption of the hard disk and more.

Ada is a language that was designed specifically for embedded safety critical applications which makes it more suitable for writing device drivers.

Preparations

First, I investigated the current process of writing and building modules. An example tutorial can be found here.

In an nutshell, it is required to create a file that looks like this:

/*  
 *  hello-1.c - The simplest kernel module.
 */
#include <linux/module.h>   /* Needed by all modules */
#include <linux/kernel.h>   /* Needed for KERN_INFO */

int init_module(void)
{
    printk(KERN_INFO "Hello world 1.\n");

    /* 
     * A non 0 return means init_module failed; module can't be loaded. 
     */
    return 0;
}

void cleanup_module(void)
{
    printk(KERN_INFO "Goodbye world 1.\n");
}

Additionally, a mkefile that looks like this:

obj-m += hello-1.o

all:
    make  -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
    make  -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Calling make will produce a .ko file which is the actual loadable module. It can be loaded and removed using the insmodule and rmodule.

The difference between a .ko and .o file is the additional memory sections added by the linker.

The Strategy

It is very tempting to abolish the cumbersome kbuild system and use a gpr file for the whole compilation and linkage. Unfortunately the makefiles that kbuild is made of are too complex for me to understand, and I was able to imitate everything it does to produce the ".ko" file. Additionally, it is not safe to work around the kbuild system as the internal details might change it future versions of the kernel.

So instead, I envision the following strategy. All the "logic" of the module will reside in Ada functions that will be compiled into a static library. The main file will be written in C and will consist of wrappers (eg init_module, cleanup_module) that will call the Ada functions. We will tell kbuild about the existence of the Ada library, so it will link it into the module, and everyone will be happy.

Hello World with Ada

The Code

The Ada files contain a function which always return a constant 42. pragma Export is used to export the ada_foo symbol.

package Ada_Foo_Pack is

   function Ada_Foo return Integer;

private
   pragma Export
      (Convention    => C,
       Entity        => Ada_Foo,
       External_Name => "ada_foo");
end Ada_Foo_Pack;

:::ada
package body Ada_Foo_Pack is

   Ultimate_Unswer : Integer := 0;

   function Ada_Foo return Integer is
   begin

      return Ultimate_Unswer;

   end Ada_Foo;

begin

   --  This line of code will run during elaboration
   --
   Ultimate_Unswer := 42;

end Ada_Foo_Pack;

The C file performs the elaboration of Ada libraries by calling adakernelmoduleinit, and then calls prink with the value returned by the Ada function.

#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_INFO */

extern void adakernelmoduleinit- (void);
extern int ada_foo(void);

int init_module(void)
{
    adakernelmoduleinit();
    printk(KERN_ERR "Hello Ada %d.\n", ada_foo());

    return 0;
}


void cleanup_module(void)
{
    printk(KERN_ERR "Goodbye Ada.\n");
}

So far nothing too complicated. However when building the module, kbuild complaints about many missing symbols. These missing symbols are part of the "Run Time". The calls to these methods are either produced by a compiler (for example delay 0.1; will implicitly call functions from the calendar package) or produced by the binder (initialization of the run time).

Normally, these functions can be found in libgnat.so, but this dynamic library is not available in the kernel space.

I also tried to tell kbuild to statically link with libgnat.a, but it turns out that libgnat.a is referencing other symbols which are supposed to be located in further libraries which complicates everything.

On top of this, linking the standard run time to the kernel does not make sense as it uses system calls (for example to open a file) which are available only in user space.

The Run Time

The solution is to build a custom, degraded run time. Such run times are also known as "Zero Footprint".

To achieve this I followed "Ada Bare Bones" tutorial that was written by Luke A. Guest.

To summarize, for building a run time, you will need adainclude and adalib directories. The first one will contain a copy of some of the files from the original run time, as well as system.ads, gnat.ads and additional custom packages. The second directory will contain the compiled library libgnat.a.

When building the kernel module library, every relevant tool needs to be called with the "--RTS=" flag that specify the path to the directory that contains the above two.

Deprating a little bit from Luke's tutorial, I did some modifications to the directory structure, the compiler flags, removed the console package and changed the last chance handler into a null function.

The final directory structure would look like the following diagram. I have uploaded the complete project to githhub, and you can find this version of the project by looking for the blog-post-pt-1 git tag.

Download Pt-1

.
├── gnat.adc
├── kernel_module_lib.gpr
├── lib
├── main.c
├── Makefile
├── obj
├── rts
│   ├── adainclude
│   │   ├── ada.ads
│   │   ├── a-unccon.ads
│   │   ├── a-uncdea.ads
│   │   ├── gnat.ads
│   │   ├── g-souinf.ads
│   │   ├── interfac.ads
│   │   ├── last_chance_handler.adb
│   │   ├── last_chance_handler.ads
│   │   ├── s-atacco.adb
│   │   ├── s-atacco.ads
│   │   ├── s-maccod.ads
│   │   ├── s-stoele.adb
│   │   ├── s-stoele.ads
│   │   └── system.ads
│   ├── adalib
│   ├── gnat.gpr
│   └── obj
└── src
    ├── ada_foo_pack.adb
    └── ada_foo_pack.ads

Conclusion

The example module I have written is very simple proof of concept. The Ada part does not even use any of symbols exported by the kernel. In future articles I will make an attempt to write a usefull kernel module that actually does stuff.