OS自作に役立つZen言語機能10選!

はじめに

本記事は自作OS Advent Calendar 2019の4日目として書きました。

とうとう公開されましたねZen言語!

zen-lang.org

私は組込み屋さんで、OS自作を嗜んでいますが、ベアメタルでプログラミングするにあたり、Zen言語は非常に使いやすい機能を兼ね備えています。 本記事では、Zen言語でOS自作を始めるにあたり、Zen言語の便利な機能を紹介し、いくつかのターゲット環境のブートストラップを提供します

間に合いませんでした!Cortex-Mのブートストラップだけで許して下さい!他のターゲットも順次情報公開します。

Zen言語のslackもあるので、ご興味のある方は、@LDScellまでご連絡下さい!

記事内容のうち半分程度はZen言語のフォーク元であるZig言語でも同様に使えます。

対象読者

過去に少しでも、OS自作したことがある方を想定しています。 バイナリ操作に対する解説はありませんので、ご了承下さい。

目次

テストフレームワーク

まず、何と言っても言語に組込みのテストフレームワークでしょう! スモールステップで少しずつプログラムを構築していくことで、原因不明のバグに悩む頻度を減らすことができます。

Zenでは、通常のコードと同じファイル内にテストを書くことができます。

const std = @import("std");
const testing = std.testing;

fn add(a: u32, b: u32) u32 {
    return a + b;
}

test "this is a test" {
    testing.equal(@is(u32, 3), add(1, 2));
}

OS自作では、テストを書くことが難しいコードがたくさんあります。 それでも、書けるテストは書くと良いです。 テストはホスト環境で実行します。

私がよく書くテストは、ペリフェラルバイスレジスタマップが合っているかどうか、を試すテストです。 例えば、ペリフェラルレジスタマップ構造体を次のように定義します。

pub const CLOCK = packed struct {
    pub const base: usize = 0x40000000;
    TASKS_HFCLKSTART: WriteOnly(u32) = WriteOnly(u32){},
    TASKS_HFCLKSTOP: WriteOnly(u32) = WriteOnly(u32){},
    TASKS_LFCLKSTART: WriteOnly(u32) = WriteOnly(u32){},
    TASKS_LFCLKSTOP: WriteOnly(u32) = WriteOnly(u32){},
    TASKS_CAL: WriteOnly(u32) = WriteOnly(u32){},
    TASKS_CTSTART: WriteOnly(u32) = WriteOnly(u32){},
    TASKS_CTSTOP: WriteOnly(u32) = WriteOnly(u32){},
    reserved28: Reserved(228) = Reserved(228){},
    EVENTS_HFCLKSTARTED: ReadWrite(u32) = ReadWrite(u32){},
    EVENTS_LFCLKSTARTED: ReadWrite(u32) = ReadWrite(u32){},
    reserved264: Reserved(4) = Reserved(4){},
    EVENTS_DONE: ReadWrite(u32) = ReadWrite(u32){},
    // ...
};

こういったレジスタマップを作って、どこかでバイト数がずれていると悲惨です。 EVENTS_HFCLKSTARTEDレジスタを読み書きしているつもりで、全然違うレジスタを読み書きしてしまっているかもしれません! これは、意外と気づきにくいバグになります。

データシートを参照して、確実にレジスタのアドレスが合っているように、テストしましょう! @byteOffsetOfは、その構造体の先頭から、何バイト目の位置にあるか、を取得するための言語組込み関数です。

test "clock reg" {
    testing.equal(usize(0x000), @byteOffsetOf(CLOCK, "TASKS_HFCLKSTART"));
    testing.equal(usize(0x004), @byteOffsetOf(CLOCK, "TASKS_HFCLKSTOP"));
    testing.equal(usize(0x008), @byteOffsetOf(CLOCK, "TASKS_LFCLKSTART"));
    testing.equal(usize(0x00C), @byteOffsetOf(CLOCK, "TASKS_LFCLKSTOP"));
    testing.equal(usize(0x010), @byteOffsetOf(CLOCK, "TASKS_CAL"));
    testing.equal(usize(0x014), @byteOffsetOf(CLOCK, "TASKS_CTSTART"));
    testing.equal(usize(0x018), @byteOffsetOf(CLOCK, "TASKS_CTSTOP"));
    testing.equal(usize(0x100), @byteOffsetOf(CLOCK, "EVENTS_HFCLKSTARTED"));
    testing.equal(usize(0x104), @byteOffsetOf(CLOCK, "EVENTS_LFCLKSTARTED"));
    testing.equal(usize(0x10C), @byteOffsetOf(CLOCK, "EVENTS_DONE"));
    // ...
}

もう1つ、ハードウェアの動作はともかく、ソフトウェア的に正しい値が書き込めているかどうか、を試すテストも書きます(意外と大事ですよ!)。 本来は、registerはメモリマップドなペリフェラルバイスのアドレスを指すのですが、一度インスタンスを作成し、ドライバがそのインスタンスのどこに何を書いたか、をテストします。

test "call SPIM init" {
    // snip
    var register = reg.SPIM0{};

    const spim_test = SpiM{
        .nvic = &nvic_dummy,
        // これは本当は、ペリフェラルデバイスのメモリアドレスになる
        .register = @ptrCast(*volatile reg.SPIM0, &register),
        .irqn = irqn,
        .sck = ck,
        .mosi = mo,
        .miso = mi,
        .csn = cs,
        .context = &context,
    };

    // `init()`関数でどこに何が書き込まれたか、をテストしたい
    spim_test.init();

    // `init()`後に`register`のどこに何が書き込まれたか、をテストする
    testing.equal(@is(u32, 1), register.PSEL_SCK.inner);
    testing.equal(@is(u32, 2), register.PSEL_MOSI.inner);
    testing.equal(@is(u32, 3), register.PSEL_MISO.inner);
    testing.equal(@is(u32, 4), register.PSEL_CSN.inner);
    testing.equal(@is(u32, 0x0200_0000), register.FREQUENCY.inner);
    testing.equal(@is(u32, 7), register.ENABLE.inner);
}

少し複雑なビットマスクやビット演算を行うと、意外とミスっていることも多いです。 これをやるだけでも、問題発生時、ソフトかハードか、の切り分けが楽になります。

ここだけの話ですが、ベアメタル環境でもZenのテストフレームワークは動作します(が、現状はコンパイラの修正が少し必要です)。 ベアメタル環境でのテストは近い将来、皆さんに届けたいですね。

UEFI

私は組込みOSが主戦場ですが、汎用OSに興味がある人のほうが多数派ですね。仕方ないですね! 汎用OSを作る上での強い味方、UEFIさんですが、Zenでもサポートしています。

通常、UEFIアプリケーションはEfiMainから書き始めると思いますが、Zenの標準ライブラリ内にUEFI用のブートストラップがあります。 UEFIをターゲット環境として指定した場合、Zen標準ライブラリ (std/special/start.zen) 内のEfiMainがリンクされ、EfiMainからはpub fn mainが呼び出されるようになっています。

つまりどういうことかと言うと、pub fn mainでおもむろにmain関数を書き始めるだけでUEFIアプリケーションがビルドできます。

pub fn main() void {
    // ゆーいーえふあいなアプリケーションを書く
}
$ zen build-exe src/main.zen -target x86_64-uefi-msvc

はい、これでOKです。UEFIアプリケーションの出来上がりです。

さらに、Zenの標準ライブラリにはUEFIプロトコルのbinding APIが存在しています。 std.os.uefiに実装があるので、UEFIアプリケーションを気軽に書くことができます!

任意ビット数の整数型

任意ビット数の整数型が利用できます。 u8, u16, u32といった8ビット単位の整数だけでなく、u1, u5, u13といった任意のビット幅を指定することができます。

これらの整数型に対して、厳密に値のチェックが行われます。 例えば、u1には01しか代入できませんし、u5には0から31までの数値しか代入することができません。

また、Zenのenumは整数値にタグをつけるわけですが、ここにも任意幅の整数値を指定することができます。 つまり、次のようなことができます。

const Parity = packed enum(u3) {
    excluded = 0b000,
    included = 0b111,
};

このようなことができて、何が嬉しいのでしょうか? それは、デバイスドライバを書く時に、最高に重宝するのです!

例えば、UARTデバイスの設定を行うメモリマップドレジスタは、次のように定義できます。

const HardwareControlFlow = packed enum(u1) {
    disabled = 0,
    enabled = 1,
};

const Parity = packed enum(u3) {
    excluded = 0b000,
    included = 0b111,
};

const StopBits = packed enum(u1) {
    one = 0,
    two = 1,
};

pub const UartConfig = packed struct {
    stop: StopBits,
    parity: Parity,
    hardware_control_flow: HardwareControlFlow,
    reserved0: u3 = 0,
    reserved1: u24 = 0,
};

これでUartConfigは32bitのデータを持つ構造体になります。 stop, parity, hadware_control_flowフィールドにはそれぞれ、定義されたenumの値しか代入できないので、安全です!

クロスビルド

ZenのバックエンドはLLVMのため、非常に簡単にクロスビルドできます。 リリースされているZenコンパイラ以外は何も必要ありません。

リンカスクリプトが必要な場合はあります!

-targetの後にターゲットトリプルを指定するだけです。

$ zen build-exe -target <target triple> src/main.zen

例えば、wasm32ターゲットであれば、次の通りです。

$ zen build-exe -target wasm32-freestanding-musl src/main.zen

クロスビルド可能なターゲット環境は、次のコマンドで確認できます。

$ zen targets

ターゲット依存コードをコンパイル時にswitch

自作したOS、1つのプラットフォームでしか動かないのは、寂しいですね。 色んなターゲットで動かしたいですね。 当然ですよね!?

そんな時の強い味方builtinを活用しましょう!

例えば、プロセッサを割り込み待ちにする場合、x86ではhlt命令を使用します。 これには、通常インラインアセンブリか、アセンブリソースか、を利用すると思います。

Zenのインラインアセンブリを使って、次のように書くことができます。

|> hlt.zen

export fn hlt() void {
    asm volatile("hlt");
}

かるーくバイナリを覗いてみます。

$ zen build-obj hlt.zen
$ llvm-objdump -D hlt.o | less

hltで検索すると、ちゃんとできていますね!

0000000000000270 <hlt>:
     270:       55                      push   %rbp
     271:       48 89 e5                mov    %rsp,%rbp
     274:       f4                      hlt    
     275:       5d                      pop    %rbp
     276:       c3                      retq

さて、話はマルチプラットフォーム対応の話でした。 では、このコードをARM Cortex-Mをターゲットにビルドしてみましょう。

$ zen build-obj hlt.zen -target armv7m-freestanding-eabihf
<inline asm>:1:2: error: invalid instruction
        hlt
        ^

あらあら、そんな命令はない、と怒られてしまいましたね!

ARM Cortex-M系では、割り込み待ち状態に入る時はwfi (wait for interrupt) 命令を使用します。 x86とARM Cortex-M系両方で使えるhlt関数はどうやって作れば良いのでしょうか!

答えはこうです。

const builtin = @import("builtin");

export fn hlt() void {
    switch (builtin.arch) {
        .i386, .x86_64 => asm volatile("hlt"),
        .arm => asm volatile("wfi"),
        else => @compileError("unimplemented!"),
    }
}

外部ビルドシステム (Makefileなど) でターゲットプラットフォームに合わせて、マクロ定義を追加したり、コンパイルするアセンブリソースを切り替えたりする必要は、一切ありません。

一度、ビルドしてみましょう。

$ zen build-obj hlt.zen -target armv7m-freestanding-eabihf

中身がどうなっているのか、知りたくてたまりませんね?

とりあえず落ち着いて、ARMのオブジェクトファイルかどうか、確認してみましょう。

$ file hlt.o
hlt.o: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), with debug_info, not stripped

間違いなく、ARMのオブジェクトファイルです。

hlt関数を見てみましょう。

$ llvm-objdump -D hlt.o  --triple armv7m-none-eabifh | less
0000000a hlt:
       a: 30 bf                         wfi
       c: 70 47                         bx      lr

完全にARMだこれ!

ちなみに、x86用のコードは、先ほどと同じ出力結果になります。 試しにやってみて下さい。

$ zen build-obj hlt.zen
$ llvm-objdump -D hlt.o | less

Zenコンパイラ組込みのbuiltinにはビルドターゲットプラットフォームの情報 (Zenコンパイラ呼び出し時の-targetに指定したプラットフォーム) が格納されています。 Zenのコードではこのbuiltin情報を使って、ターゲットプラットフォーム専用のコードだけをビルドすることができます。

ここでポイントとなるのが、builtinは、コンパイラが持っているコンパイル時定数であることです。 Zenでは、ifswitchの条件式がcomptime-known (コンパイル時計算可能) であれば、その分岐はコンパイルに展開されます。

下のswitch式を例にします。

    switch (builtin.arch) {
        .i386, .x86_64 => asm volatile("hlt"),
        .arm => asm volatile("wfi"),
        else => @compileError("unimplemented!"),
    }

x86がターゲット、つまりbuiltin.arch == .i386 or builtin.arch = .x86_64であれば、コンパイル時にswitch式が展開され、コンパイルされるのは次のコードだけなります。

    asm volatile("hlt")

このようにZenでは、外部のビルドシステムに頼ることなく、マルチプラットフォーム対応が可能になっています。

再利用性を高めるinterface

マルチプラットフォームに対応しだすと、抽象化のレイヤーを設けて、上位ソフトウェアを再利用をしたくなりますね! だってプログラマだもの!

Zenのインターフェースは (多分) 構造的部分型と呼ばれるものです。 Javaのようにインタフェースを明示的に継承するのではなく、その型が持つメソッドフィールドによって派生関係が決まります。

Zenドキュメントの例を見てみましょう。

zen-lang.org

下はinterfaceの定義です。このインタフェースは、data() u32シグネチャを持つメソッドの実装を要求しています。

const Sensor = interface {
    fn data() u32;
};

このinterfaceを実装する構造体は次の通りです。

const SensorA = struct {
    fn data(self: SensorA) u32 {
        return 42;
    }
};

Zenのインタフェースはコンパイル時に働きます。 関数呼び出しの引数として、インタフェース型を受け取ることで、渡されたインスタンスがインタフェースを満足するかどうか、を検査します。

// `Sensor`インタフェースのゲートウェイを果たす関数
fn data(sensor: Sensor) u32 {
    return sensor.data();
}

fn useSensor() void {
    const sensor_a = SensorA {};
    // `SensorA`でーす。`Sensor`インタフェースを満たしていまーす。ちょっと通りますよ。
    const result = data(sensor_a);
    // use `result` ...
}

イメージとしてはRustのtraitやC++のConceptに近いです。

上記の例では、Sensorインタフェースを使って上位ソフトウェアを実装しておけば、後々Sensorインタフェースを満たすようにSensorB, SensorCと作っていけば良いわけです。 便利ですね!

動的ポリモーフィズムを実現するvtable

さて、Zenのインタフェースはコンパイル時に働く、と書きました。 では、動的ポリモーフィズムはどのように実現されるのでしょうか?

例えば、SensorASensorBがそれぞれSensorインタフェースを実装していたとしても、次のようなコードはコンパイルできません。

fn useSensors() void {
    var sensor: Sensor = SensorA{};
    // ダメ!`sensor`は`SensorB`のインスタンスから再代入できない!
    sensor = SensorB{};

    // これもダメ!
    const sensors = [_]Sensor {
        SensorA{},
        SensorB{},
    };
}

これでは不便ですね! そのための*vtable機能があります!

fn useSensors() void {
    var sensor: *vtable Sensor = &SensorA{};
    // `vtable`は別型のインスタンスで書き換え可能!
    sensor = &SensorB{};

    // これもOK!
    var sensor_a = SensorA{},
    var sensor_b = SensorB{},
    const sensors = [_]*vtable Sensor {
        &sensor_a,
        &sensor_b,
    };
}

このようにinterfacevtableを組み合わせることで、再利用性の高いソフトウェアを、型システムの力を借りながら、実装できるわけですね! 素晴らしいですね!

メモリアロケータ

ベアメタルでの開発も少しステージが進むと、LinkedListや可変長配列のような動的なデータ構造を扱いたくなります。 そのためには、メモリアロケータが必要です(C言語で言うところのmallocなどですね)。 Zenでは一定の状況下では、容易にこのような動的なデータ構造を扱うことができます。

Zenにはグローバルアロケータが存在しません。 Allocatorというインタフェースを実装するアロケータインスタンスから、明示的にメモリの確保、解放を行います。

さて、ベアメタルでも簡単に利用できるアロケータとして、FixedBufferAllocatorがあります。 このアロケータは、固定長のバッファ領域しか割り当てられませんが、その固定長のバッファ領域はどこに用意されていても良いという特徴があります。

例えば、バッファ領域はスタック領域でもかまいません。

fn use_allocator() Allocator.Error!void {
    var buffer: [1024]u8 = [_]{0} ** 1024;
    var allocator = heap.FixedBufferAllocator { .buffer = &buffer };

    // `u32`の領域を確保する
    const allocated = heap.create(&allocator, u32);
    defer heap.destroy(&allocator, allocated);
}

スタティック領域にバッファを割り当てることもできます。

var buffer: [1024]u8 = [_]{0} ** 1024;

fn use_allocator() Allocator.Error!void {
    var buffer: [1024]u8 = [_]{0} ** 1024;
    var allocator = heap.FixedBufferAllocator { .buffer = &buffer };

    // `u32`の領域を確保する
    const allocated = heap.create(&allocator, u32);
    defer heap.destroy(&allocator, allocated);
}

このFixedBufferAllocatorを使うことで、LinkedListや可変長配列をすぐに利用することができます。 将来的にもっと良いメモリアロケータを作った場合には、FixedBufferAllocatorの代わりに、新しいメモリアロケータのインスタンスAllocatorインタフェース経由で使用すれば良いです。

ただしFixedBufferAllocatorフラグメンテーションをケアしていないので、確保と解放を繰り返すような使い方はできません。

例えば、次のような新しいプロセスを動的確保する例を考えます(LinkedListに追加するような場合でも同様です)。 この関数は、heap.Allocatorインタフェースを実装した何らかのアロケータインスタンスを使用し、新しいプロセスを生成します。

fn createNewProcess(allocator: heap.Allocator) Allocator.Error!*Process {
    var process = heap.create(allocator, Process);
    process.init();
    return process;
}

createNewProcessを呼び出す側は、FixedBufferAllocatorインスタンスを引数に渡しても良いですし、自作のアロケータインスタンスを引数に渡しても良いです。 このように、利用するアロケータを簡単に変更することができます。

format print

自作OSをやる上で、一刻も早く手にしたいのは、いわゆるprintfデバッグができる環境です。 Zenでは標準ライブラリを利用することで、容易にフォーマットされた文字列出力が可能です。

Zenのformat printはstd.debug.warnに代表される、次のようなものです。

    std.debug.warn("hello {}\n", "world");

Zenでフォーマット文字列を出力するのに必要なことは、std.io.OutStreamインタフェースを実装する構造体を書くだけです。 OutStreamはユーザー定義の書き込みエラー型 (WriteError) を関連する型として持ちます。

pub fn OutStream(comptime WriteError: type) type {
    return interface {
        fn write(buf: []const u8) WriteError!void;
    };
}

細かい理屈はさておき、やることは、次の1文につきます。

  • UARTなどコンソール出力できるドライバにfn write(self: Self, buf: []const u8) WriteError!voidなメソッドを実装します。

ここでWriteErrorは自由に定義できます。とりあえず、空のエラーでもOKです (error {})。

今、UartDriverという構造体があり、writeByte()関数で1文字を出力可能であると仮定しましょう。

pub const UartDriver = struct {
    // snip

    pub fn writeByte(self: UartDriver, byte: u8) void {
        // UARTデバイスに1バイト書き込む
    }
};

追加するコードは、下だけです。

pub const UartDriver = struct {
    // snip

    pub const WriteError = error {};
    pub fn write(self: UartDriver, buf: []const u8) WriteError!void {
        for (buf) |byte| {
            self.writeByte(byte);
        }
    }
};

後は、UartDriverインスタンスを作成し、io.write構造体経由で利用するだけです。 下のような感じです。

    const serial = UartDriver{};
    const write = io.write(UartDriver.WriteError);
    try write.print(&serial, "hello {}", "world");

panic

Zen言語には実行時エラーを安全に取り扱うためのパニック機構が存在します。 ここで実行時エラーとは、整数演算のオーバーフローや配列の範囲外へのインデックスアクセスなど、C言語で言うところの「未定義動作」に近いものです。

例えば次のコードは、要素数5の配列に対して、10のインデックスでインデックスアクセスするため、index out of boundsの実行時エラーになります。

pub fn main() anyerror!void {
    var array = [_]u8 {0} ** 5;
    var index: usize = 10;
    array[index] = 0;
}

OSにホストされた環境で実行時エラーが発生すると、バックトレースを出力し、プロセスが終了されます。

$ zen build-exe main.zen
$ ./main 
index out of bounds
src/main.zen:6:10: 0x22d624 in main (main)
    array[index] = 0;
         ^

// snip

zen_0.8.0/lib/zen/std/special/start.zen:106:5: 0x22c2ff in std.special._start (main)
    @noInlineCall(posixCallMainAndExit);
    ^
Aborted (core dumped)

この動作は、実はパニックハンドラ (panic 関数) に定義された動作です。 OSにホストされた環境では、デフォルトパニックハンドラは、上述の動作をします。

実装が気になる場合、std.special.panic.zenソースコードを参照して下さい。

それ以外の環境では、組込み関数の @Trap を呼び出すようになっています。 例えば、Cortex-Mでは、 @Trap 組込み関数は、次のアセンブリになります。

0000000e trap:
       e: fe de                         trap

トラップハンドラをきちんと実装していれば、これでも問題ないかもしれません。 しかし、もう少しカジュアルにパニック発生時の状況を把握したいですよね。

そこで、パニックハンドラを上書きすることができます。 ルートソースファイルpanic関数を書くことで、そのpanic関数がパニックハンドラとして使われるようになります。

例えば、main関数を実装しているmain.zenに次のようなパニックハンドラを書いてみます。

pub fn panic(msg: []const u8, _: ?*builtin.StackTrace) noreturn {
    std.debug.warn("my panic handler!\n");
    std.debug.warn("panic address: {x}\n", @returnAddress());
    std.os.abort();
}

わざとパニックを起こすmain関数を書いてみましょう。

pub fn main() anyerror!void {
    var array = [_]u8 {0} ** 5;
    var index: usize = 10;
    array[index] = 0;  // ここでパニック
}

実行すると、パニック時の挙動が次のようになります。

$ ./main 
my panic handler!
panic address: 22d085
Aborted (core dumped)

パニック時の挙動が変わっています! さらに、@returnAddress組込み関数を使用し、パニック関数の戻りアドレスを取得することで、パニックを起こした命令 (の次の命令のアドレス) を取得することができます。

22d085のアドレスを見てみると、

$ objdump -S -D main | less
   array[index] = 0;  // ここでパニック
  22d064:       48 8b 55 f0             mov    -0x10(%rbp),%rdx
  22d068:       48 83 fa 05             cmp    $0x5,%rdx
  22d06c:       48 89 55 e8             mov    %rdx,-0x18(%rbp)
  22d070:       72 13                   jb     22d085 <main+0x45>
  22d072:       31 c0                   xor    %eax,%eax
  22d074:       89 c6                   mov    %eax,%esi
  22d076:       48 bf 48 19 20 00 00    movabs $0x201948,%rdi
  22d07d:       00 00 00 
  22d080:       e8 7b 7f fd ff          callq  205000 <panic>
  22d085:       31 c0                   xor    %eax,%eax  // ★@returnAddress()で得られたアドレス

どこでパニックが発生したか、一目瞭然 (?) ですね♪

ちなみに、Zenの標準ライブラリstd/debug.zenにはDWARF形式からスタックトレースを出力するためのコードがあるため、デバッグ情報ごとターゲット環境に書き込む余裕があれば、OSでホストされている状態と同じように、パニック時のデバッグ情報を出力することができます。

ビルドスクリプト

めっちゃ便利なので書きたかったのですが、時間切れでした…。

Cortex-Mブートストラップ

これは拙著「Zenbedded〜Zen言語で作る組込みシステム CPUリセットからUART編〜」に掲載しているコードです。 本ではもう少しちゃんと解説しています!

tomo-wait-for-it-yuki.hatenablog.com

ターゲットはQEMUCortex-M3 (lm3s6965evb) です。

Cortex-Mは神なので、アセンブリを書かずにブートすることができます。

boot.zen

export fn reset() noreturn {
  while (true) {}
}

export const RESET_VECTOR: extern fn() noreturn
  linksection(".vector_table.reset_vector") = reset;

注目すべきはlinksection(".vector_table.reset_vector")ですね。 Zenでは任意の変数、関数をリンクセクションを指定して配置することができます。

linker.ld

/* Memory layout of the LM3S6965 microcontroller */
/* 1K = 1 KiBi = 1024 bytes */
MEMORY
{
  FLASH : ORIGIN = 0x00000000, LENGTH = 256K
  RAM : ORIGIN = 0x20000000, LENGTH = 64K
}

/* エントリポイントはリセットハンドラです */
ENTRY(reset);

EXTERN(RESET_VECTOR);

SECTIONS
{
  .vector_table ORIGIN(FLASH) :
  {
    /* 1つ目のエントリは、スタックポインタの初期値です */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

    /* 2つ目のエントリはリセットベクタです */
    KEEP(*(.vector_table.reset_vector));
  } > FLASH

  .text :
  {
    *(.text .text.*);
  } > FLASH

  .rodata :
  {
    *(.rodata .rodata.*);
    *(.data.rel.ro);
  } > FLASH

  .ARM.exidx :
  {
    *(.ARM.exidx.*);
  } > RAM
}

ビルド

リンカスクリプトを指定して、ターゲット向けにクロスビルドします。

$ zen build-exe boot.zen  --linker-script linker.ld -target thumbv7m-freestanding-eabi

実行

$ qemu-system-arm -machine lm3s6965evb -kernel boot -s -S -nographic