Josh Triplett - Porting Python to run without an OS - PyCon 2015

By: PyCon 2015

167   0   9507

Uploaded on 04/11/2015

"Speaker: Josh Triplett

We've ported Python to run directly on hardware, without an OS, as a testing and exploration environment for firmware, ACPI, and UEFI. This talk will explore porting Python to a new platform, embedding Python, recreating enough of libc and POSIX to run Python without an OS, and binding to platform-specific services. Includes live demo of bare-metal Python, directly driving hardware.

Slides can be found at: and"

Comments (2):

By anonymous    2017-09-20

Runnable examples

Technically, a program that runs without an OS, is an OS. So let's see how to create and run some minuscule hello world OSes.

The code of all examples below is present on this GitHub repo. All was tested on Ubuntu 14.04 AMD64 QEMU and real hardware ThinkPad T430.

Boot sector

On x86, the simplest and lowest level thing you can do is to create a Master Boot Sector (MBR), which is a type of boot sector, and then install it to a disk.


printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img

main.img contains the following:

  • \364 in octal == 0xf4 in hex: the encoding for a hlt instruction, which tells the CPU to stop working.

    Therefore our program will not do anything: only start and stop.

    We use octal because \x hex numbers are not specified by POSIX.

    We could obtain this encoding easily with:

    echo hlt > a.asm
    nasm -f bin a.asm
    hd a

    but it is also present on the Intel manual of course.

  • %509s produce 509 spaces. Needed to fill in the file until byte 510.

  • \125\252 in octal == 0x55 followed by 0xaa: magic bytes required by the hardware. They must be bytes 511 and 512.

    If not present, the hardware will not treat this as a bootable disk.

Note that even without doing anything, a few characters are already printed on the screen. Those are printed by the firmware, and serve to identify the system.

Run on real hardware

Emulators are fun, but hardware is the real deal.

  • Burn the image to an USB stick (will destroy your data!):

    sudo dd if=main.img of=/dev/sdX
  • plug the USB on a computer

  • turn it on

  • tell it to boot from the USB.

    This means making the firmware pick USB before hard disk.

    If that is not the default behavior of you machine, keep hitting Enter, F12, ESC or other such weird keys after poweron until you get a boot menu where you can select to boot from the USB.

    It is often possible to configure the search order in those menus.

Hello world

Now that we have made a minimal program, let's move to a hello world.

The obvious question is: how to do IO? A few options:

  • BIOS
  • UEFI. BIOS successor, better standardized.
  • VGA: special memory region that gets printed to the screen if written to. Can be used on Protected mode.
  • write a driver. This is the "proper" way to do it: more powerful, but more complex.

Here we will do a BIOS example as it is simpler. But note that it is not the most robust method.

Here is the GAS code:

.global _start
    mov $msg, %si
    mov $0x0e, %ah
    or %al, %al
    jz halt
    int $0x10
    jmp loop
    .asciz "hello world"
.org 510
.word 0xaa55

Besides the standard userland assembly instructions, we have:

  • .code16: tells GAS to output 16-bit code

  • cli: disable software interrupts. Those could make the processor start running again after the hlt

  • int $0x10: does a BIOS call. This is what prints the characters one by one.

  • .org 510 and .word 0xaa55: place the magic bytes at the end

A quick and dirty way to compile this is with:

as -o main.o main.S
ld --oformat binary -o -Ttext 0x7C00 main.o

and run main.img as before.

There are two important flags here:

  • --oformat binary: output raw binary assembly code, don't warp it inside an ELF file as is the case for regular userland executables.

  • -Ttext 0x7C00: we need to tell the linker ld where the code will be placed so that it will be able to access the memory.

    In particular, this is used during the relocation phase. Read more about it here.

The better way of compiling is to use a clean linker script as this one. The linker script can also place the magic bytes for us.


In truth, your boot sector is not the first software that runs on the system's CPU.

What actually runs first is the so-called firmware, which is a software:

  • made by the hardware manufacturers
  • typically closed source but likely C-based
  • stored in read-only memory, and therefore harder / impossible to modify without the vendor's consent.

BIOS is an old all-present x86 firmware, and UEFI a newer replacement for it.

QEMU comes with its own BIOS firmware implementation.

The firmware does things like:

  • loop over each hard disk, USB, network, etc. until you find something bootable.

    When we run QEMU, -hda says that main.img is a hard disk connected to the hardware, and

    hda is the first one to be tried, and it is used.

  • load the first 512 bytes to RAM memory address 0x7c00, put the CPU's RIP there, and let it run

  • show things like the boot menu or BIOS print calls on the display

Firmware offers OS-like functionality on which OS-es depend.

Firmwares could even be considered OS-es themselves. E.g. a Python subset has been ported to run on BIOS / UEFI:

Initial state

Like many things in hardware, standardization is weak, and one of the things you should not rely on is the initial state of registers.

So do yourself a favor and use some initialization code like the following:

Registers like %ds and %es have important side effects, so you should zero them out even if you are not using them explicitly.

Note that some emulators are nicer than real hardware and give you a nice initial state. Then when you go run on real hardware, everything breaks.

GRUB Multiboot

Boot sectors are simple, but they are not very convenient:

  • you can only have one OS per disk
  • the load code has to be really small and fit into 512 bytes. This could be solved with the int 0x13 BIOS call.
  • you have to do a lot of startup yourself, like moving into protected mode

It is for those reasons that GRUB created a more convenient file format called multiboot.

Minimal working example:

If you prepare your OS as a multiboot file, GRUB is then able to find it inside a regular filesystem.

This is what most distros do, putting OS images under /boot.

Multiboot files are basically an ELF file with a special header. They are specified by GRUB at:

You can turn a multiboot file into a bootable disk with grub-mkrescue.

El Torito

Format that can be burnt to CDs:

It is also possible to produce a hybrid image that works on either ISO or USB. This is can be done with grub-mkrescue (example), and is also done by the Linux kernel on make isoimage using isohybrid.


ARM-land has it's own conventions (ore more lack of conventions, since ARM licenses IP to vendors that modify it), but the general ideas are the same:

  • you write code to a magic address in memory
  • you do IO with magic addresses

Some differences include:

  • IO is done by writing to magic addresses directly, there is no in and out instructions
  • you need to add magic compiled closed source blobs provided by vendors to your image. Those are somewhat like BIOS, and that is a good thing, as it makes updating that firmware more transparent.

Here is an example:


Original Thread

Submit Your Video

If you have some great dev videos to share, please fill out this form.