Overview

In this session we look into running applications using the binary compatibility layer as well as understanding the inner workings of the system call shim layer.

One of the obstacles when trying to use Unikraft is the porting effort of new applications. This process can be made painless through the use of Unikraft’s binary compatibility layer. Binary compatibility is the possibility to take pre-built Linux ELF binaries and run them on top of Unikraft. This is done without any porting effort while maintaining the benefits of Unikraft: reduced memory footprint, high degree of configurability of library components.

For this, Unikraft must provide a similar ABI (Application Binary Interface) with the Linux kernel. This means that Unikraft has to provide a similar system call interface that Linux kernel provides, a POSIX compatible interface. For this, the system call shim layer (also called syscall shim) was created. The system call shim layer provides Linux-style mappings of system call numbers to actual system call handler functions.

01. The Process of Loading and Running an Application with Binary Compatibility

For Unikraft to achieve binary compatibility there are two main objectives that need to be met:

  1. The ability to pass the Linux ELF binary to Unikraft at boot time.
  2. The ability to load the passed ELF binary into memory and jump to its entry point.

The dominant format for executables is the Executable and Linkable File format (ELF), so, in order to run executables we need an ELF loader. The job of the ELF Loader is to load the executable into the main memory. It does so by reading the program headers located in the ELF formatted executable and acting accordingly.

As an overview of the whole process, when we want to run an application on Unikraft using binary compatibility, the first step is to pass the executable file to the unikernel as an initial ram disk. Once the unikernel gets the executable, it reads the executable segments and loads them accordingly. After the program is loaded, the last step is to jump to its entry point and start executing.

The unikernel image is the app-elfloader application. This application parses the ELF file and then loads it accordingly. It’s a custom application developed for Unikraft.

We require PIE (position-independent executable) ELFs. This is fine, as default Linux executables are built as PIE.

We have collected PIE executables in:

  • the dynamic-apps repository - storing dynamically-linked executables
  • the static-pie-apps repository - storing statically-linked executables

Summary

The binary compatibility layer is a very important part of the Unikraft unikernel. It helps us run applications that were not build for Unikraft while, at the same time, keeps the classic benefits of Unikraft: speed, security and small memory footprint.

Work Items

00. Setup

To easily setup, build and run Linux ELFs with app-elfloader, best way is to use the scripts repository. Clone the scripts repository on your machine to get started.

$ git clone https://github.com/unikraft-upb/scripts

$ cd scripts/

$ cd make-based/app-elfloader/

$ ./do.sh setup

$ ./do.sh run
'run' command requires target application as argument
Target applications: helloworld_static server_static helloworld_go_static server_go_static helloworld_cpp_static helloworld_rust_static_musl helloworld_rust_static_gnu nginx_static redis_static sqlite3 bc_static gzip_static helloworld server helloworld_go server_go helloworld_cpp helloworld_rust nginx redis sqlite3 bc gzip

$ ./do.sh run helloworld
[...]                       # many messages

$ ./do.sh run sqlite3
[...]                       # many messages

The last commands run the dynamic versions of helloworld and sqlite3 applications, the ones in the dynamic-apps repository. There is a lot of output because, by default, a pre-build version of app-elfloader is being used, with debugging enabled.

01. Run Binary Applications

Run as many executables as possible from the list of applications listed by the command:

$ ./do.sh run
'run' command requires target application as argument
Target applications: helloworld_static server_static helloworld_go_static server_go_static helloworld_cpp_static helloworld_rust_static_musl helloworld_rust_static_gnu nginx_static redis_static sqlite3 bc_static gzip_static helloworld server helloworld_go server_go helloworld_cpp helloworld_rust nginx redis sqlite3 bc gzip

02. Build app-elfloader

Using do.sh run we ran the prebuilt images from the run-app-elfloader repository. Let’s now also build app-elfloader.

Run the following commands:

$ ./do.sh clean

$ ./do.sh setup

$ ./do.sh build

$ ./do.sh run_built ...

In the last command, replace ... with the name of the application you need to run.

If you want to remove all the pesky debugging information, use setup_plain, such as the commands below:

$ ./do.sh clean

$ ./do.sh setup_plain

$ ./do.sh build

$ ./do.sh run_built ...

If, on the other side, you want to have more debugging information, use setup_debug, such as the commands below:

$ ./do.sh clean

$ ./do.sh setup_debug

$ ./do.sh build

$ ./do.sh run_built ...

03. Doing It Manually

Let’s see what happens behind the scenes. Enter the run-app-elfloader/ directory:

.../scripts/make-based/app-elfloader$ cd ../../workdir/apps/run-app-elfloader/

.../workdir/apps/run-app-elfloader$ ls
app-elfloader_kvm-x86_64*             app-elfloader_kvm-x86_64_full-debug.dbg*  debug.sh*  out/       rootfs/      run.sh*
app-elfloader_kvm-x86_64_full-debug*  app-elfloader_kvm-x86_64_plain*           defaults   README.md  run_app.sh*  utils/

Follow the instructions in the README.md file to run as many applications as possible directly. That means through the use of the run_app.sh and run.sh scripts.

04. Create your Own Application

Create your own application or get an existing application, build it and run it in binary compatability mode.

See the existing examples in the dynamic-apps repository or the static-pie-apps repository.

Further Reading

Elf Loaders, Libraries and Executables on Linux