ARMv8-A AArch64 ベタメタルプログラミング ブート編

執筆者 : 原田 秀一


はじめに

現在ArmアーキテクチャのCPUは採用も多く情報も溢れていますが、改めてARMv8-Aアーキテクチャでの64bit環境(AArch64)でのブートプログラム作成についてまとめます。

ブート処理の内容は過去記事 RISC-V OSを作ろう (1) ~ブート処理 - VA Linux エンジニアブログ と同様にOSを動かす土台とできるようにします。

環境の準備

ツールチェーンはArmが提供しているものを使用しました。 https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads

実行環境はQEMUを使用します。

専用の実験環境を作るためDockerfileを用意しました。

Dockerfile

FROM ubuntu
RUN apt-get update && apt-get install -y curl xz-utils libncursesw5 libpython2.7 libexpat1 qemu-system-arm make \
  && curl -sL https://developer.arm.com/-/media/Files/downloads/gnu-a/10.3-2021.07/binrel/gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf.tar.xz \
  | tar xJ  -C /usr
ENV PATH="$PATH:/usr/gcc-arm-10.3-2021.07-x86_64-aarch64-none-elf/bin"
ARG UID=1000
ARG GID=1000
ARG WORK_DIR='/work'
RUN useradd -m -u ${UID} docker && mkdir -p $WORK_DIR && chown -R $UID:$GID $WORK_DIR

USER ${UID}
ENTRYPOINT ["/bin/bash"]

今回は以下のバージョンとなっています。

名称 バージョン
Arm GNU Toolchain 10.3-2021.07
qemu-system-arm 6.2.0
  • Arm GNU Toolchainは現在さらに新しいバーンジョンが提供されているようです。
  • gdbはArm GNU Toolchainに含まれています。

ブートプログラム

RISC-Vの記事と同様に、今回もテキストやデータもすべてRAM上に配置して動作させることにします。

ブート処理では

  • ベクタテーブルの設定
  • AArch64ステートの設定
  • 例外レベルの変更
  • スタックポインタ初期化
  • bssセクション初期化

を行います。

例外レベル

最初に抑えておくべき仕様として例外レベルがあります。 ARMv8-Aでは例外レベル(Exception Level)としてEL0,EL1,EL2,EL3が定義されています。 EL3が最も権限が強くEL0が最低となりアクセス可能なシステムレジスタ等が異なってきます。

各例外レベルは以下の用途が想定されています。

例外レベル 用途
EL3 セキュアモニタ
EL2 ハイパーバイザ
EL1 OS
EL0 ユーザー

EL3, EL2に対応しているかはハードウェア実装依存、EL1, EL0は必須実装となっていますが、多くの場合は全レベル実装されてるようです。

リセット時は実装されている最高権限で実行開始されます。

今回はEL3, EL2が実装された環境を想定してEL3で実行開始された状態からOS動作のためにEL1に遷移するようにします。

EL1に遷移する前にしか行えない設定処理があるため遷移方法は後ほど扱います。

例外動作の概要

例外動作の概要を以下の図に示します。

例外動作概要

通常処理を実行中に例外発生することで例外処理の実行が開始されます。 このとき、元々実行していた通常処理に復帰するためのレジスタとして、 PCがELR_ELxに、 PSTATEがSPSR_ELxに保存されます。

例外処理はeret命令の実行により終了します。 このとき、ELR_ELxがPCに、SPSR_ELxがPSTATEに復帰され例外発生した箇所から通常処理が再開されます。

ELR_ELx, SPSR_ELxのxは例外処理が実行される例外レベルとなります。 例えば、EL0で通常処理実行中に例外が発生しEL1に遷移する場合は それぞれELR_EL1, SPSR_EL1となります。

EL0 - EL1遷移

また、例外要因や設定によって例外レベルの遷移がない場合もあります。 以下の例はEL1で例外処理実行中に例外発生しEL1で例外処理が実行される場合を示します。

例外レベルの遷移がない場合

このような場合、ELR_EL1, SPSR_EL1が例外発生毎に上書きされるためプログラム側で対策しておく必要があります。 次回予定の割り込み処理でこの辺りのことは扱います。

TrustZone

Armのセキュリティ関連機能としてTrustZoneというものがあります。 概念的にSecure World / Non-Secure Worldに分離が行われ、Non-Secure WorldからはSecure Worldのリソースにアクセスできなくするものです。

今回はNon-Secure Worldで動作させます。 この設定にはシステムレジスタSCR_EL3のNSビットを1に設定します。

ベクタテーブルの設定

例外発生時に参照されるベクタテーブルは、EL3, EL2, EL1毎にベースアドレスの設定を行う必要があり、 それぞれ VBAR_EL3, VBAR_EL2, VBAR_EL1 レジスタを設定します。

ベクタテーブルの中身は、例外タイプと例外レベル遷移の有無等でオフセットアドレスが決まっています。

AArch64 exception vector table - AArch64 Exception and Interrupt Handling

例外タイプは、同期例外、割り込み(IRQ)、割り込み(FIQ)、システムエラーの4つとなっています。

例外レベル遷移の有無等 は、

  • 同じ例外レベルで例外が発生しSP0を使用する場合
  • 同じ例外レベルで例外が発生しSPxを使用する場合
  • 下位の例外レベルがaarch64動作していた場合
  • 下位の例外レベルがaarch32動作していた場合

の4つの条件があります。

SP0, SPxは、使用するスタックポインタ状態を意味します。 SPは例外レベル毎に存在し、EL0以外ではSPとしてSP_EL0を使用するか現在の例外レベルのSP_ELxを使用するか選択できます。 (PSTATE.SPビットで設定)

今回は割り込みは使用しませんが意図せず例外が発生した場合にはセルフジャンプになるようベクタテーブルを定義しておきます。 (ファイルはvector.Sとします)

    .section ".entry.text", "ax"

    .global default_vector_table

    .balign 0x800
default_vector_table:
// Current EL with SP0
    .balign 0x80
curr_el_sp0_sync:       // Synchronous
    b .

    .balign 0x80
curr_el_sp0_irq:        // IRQ/vIRQ
    b .

    .balign 0x80
curr_el_sp0_fiq:        // FIQ/vFIQ
    b .

    .balign 0x80
curr_el_sp0_serr:       // SError/vSError
    b .

// Current EL with SPx
    .balign 0x80
curr_el_spx_sync:       // Synchronous
    b .

    .balign 0x80
curr_el_spx_irq:        // IRQ/vIRQ
    b .

    .balign 0x80
curr_el_spx_fiq:        // FIQ/vFIQ
    b .

    .balign 0x80
curr_el_spx_serr:       // SError/vSError
    b .

// Lower EL using AArch64
    .balign 0x80
lower_el_a64_sync:       // Synchronous
    b .

    .balign 0x80
lower_el_a64_irq:        // IRQ/vIRQ
    b .

    .balign 0x80
lower_el_a64_fiq:        // FIQ/vFIQ
    b .

    .balign 0x80
lower_el_a64_serr:       // SError/vSError
    b .

// Lower EL using AArch32
    .balign 0x80
lower_el_a32_sync:       // Synchronous
    b .

    .balign 0x80
lower_el_a32_irq:        // IRQ/vIRQ
    b .

    .balign 0x80
lower_el_a32_fiq:        // FIQ/vFIQ
    b .

    .balign 0x80
lower_el_a32_serr:       // SError/vSError
    b .

ブート処理では上記で定義したベクタテーブルを例外レベル毎のVBAR_ELxに設定します。

    ldr     x5, =default_vector_table
    msr     VBAR_EL3, x5
    msr     VBAR_EL2, x5
    msr     VBAR_EL1, x5

AArch64ステートの設定

リセット後初期状態はAArch64ステートで実行されますが、EL2,EL1,EL0に対しては実行ステートを設定する必要があります。 今回はすべての例外レベルでAArch64ステートで実行するために以下の設定を行います。

  • EL2をAArch64に設定 (SCR_EL3.RW = 1)
  • EL1をAArch64に設定 (HCR_EL2.RW = 1)

EL2は使用しませんが上位の例外レベルがAArch32で下位のレベルがAArch64という組み合わせはできないため、EL2もAArch64に設定しておく必要があります。 またNon-Secureにするための設定もSCR_EL3を使用するためここで同時に行っています(SRC_EL3.NS= 1 )。

    // Non-Secure
    // set EL2 to AArch64
    mrs     x0, SCR_EL3
    mov     x1,  SCR_EL3_RW | SCR_EL3_NS
    orr     x0, x0, x1
    msr     SCR_EL3, x0

    // set EL1 to AArch64
    mov     x2, #HCR_EL2_RW
    msr     HCR_EL2, x2

スタックポインタ設定

今回ブート処理ではスタックは使用していませんがスタックポインタを設定しておきます。

    // set stack pointer
    ldr     x4, =_stack_end
    mov     sp, x4

例外レベルの遷移

下位の例外レベルに遷移するには例外を抜ける動作を行います。 例外からの復帰先アドレスや状態の設定を行った後に復帰命令eret実行します。

  • 復帰先アドレスをELR_ELR3に設定

    今回はEL1_startを復帰先として設定しています。

  • 復帰時のPSR(PSTATE)値をSPSR_EL3に設定

    ここではA, I, Fビットを設定することで復帰時にSError, IRQ, FIQをマスクするようにしています。 またEL1に遷移しSP_EL1を使用するようにモード設定しています。

    レジスタ仕様はこちら SPSR_EL3

    モードは例外レベルとSP選択の組み合わせとなっておりEL1h, EL1tのように記載されています。 このh, tは、それぞれhandler mode, thread modeに由来するようです。

  • SP_EL1を設定

    今回は現在のSPをそのままEL1時のSPとして使用しています。

  • eret命令の実行

    // tansition (return) to EL1
EL3_TO_EL1:
   // ELR_EL3
    ldr     x0, =EL1_start
    msr    ELR_EL3, x0            // set up the return data
    // SPSR_EL3
    mov     x0, PSR_A_BIT | PSR_I_BIT | PSR_F_BIT | PSR_MODE_EL1h
    msr     SPSR_EL3, x0
   
    mov     x24, sp
    msr     SP_EL1, x24
    eret

EL1での動作

EL1に遷移した後、main関数を呼び出します。 main関数が終了してきた場合はセルフジャンプするようにしています。

EL1_start:
    bl      main
    b       .

ここまでのboot処理をstart.Sとしてまとめます。 システムレジスタのビットを定義したC言語ヘッダファイルをarmv8def.hとして作成しインクルードしています。 アセンブラソースに対してもCプリプロセッサを適用するためstart.Sと拡張子は大文字になっています。

start.S

#include "armv8def.h"
.section .reset,"ax",@progbits
.global _start
_start:

    // vector table
    ldr     x5, =default_vector_table
    msr     VBAR_EL3, x5
    msr     VBAR_EL2, x5
    msr     VBAR_EL1, x5

    // Non-Secure
    // set EL2 to AArch64
    mrs     x0, SCR_EL3
    mov     x1,  SCR_EL3_RW | SCR_EL3_NS
    orr     x0, x0, x1
    msr     SCR_EL3, x0

    // set EL1 to AArch64
    mov     x2, #HCR_EL2_RW
    msr     HCR_EL2, x2

    // set stack pointer
    ldr     x4, =_stack_end
    mov     sp, x4

    // tansition (return) to EL1
EL3_TO_EL1:
   // ELR_EL3
    ldr     x0, =EL1_start
    msr    ELR_EL3, x0            // set up the return data
    // SPSR_EL3
    mov     x0, PSR_A_BIT | PSR_I_BIT | PSR_F_BIT | PSR_MODE_EL1h
    msr     SPSR_EL3, x0
   
    mov     x24, sp
    msr     SP_EL1, x24
    eret

EL1_start:
    bl      main
    b       .

armv8def.h

#ifndef _ARMV8DEF_H_
#define _ARMV8DEF_H_

/*
 * PSR bits
 */
#define PSR_MODE_EL0t  0x00000000
#define PSR_MODE_EL1t  0x00000004
#define PSR_MODE_EL1h  0x00000005
#define PSR_MODE_EL2t  0x00000008
#define PSR_MODE_EL2h  0x00000009
#define PSR_MODE_EL3t  0x0000000c
#define PSR_MODE_EL3h  0x0000000d
#define PSR_MODE_MASK  0x0000000f

/* AArch64 SPSR bits */
#define PSR_F_BIT      0x00000040
#define PSR_I_BIT      0x00000080
#define PSR_A_BIT      0x00000100

/* SCR_EL3 bits */
#define SCR_EL3_RW         0x00000400
#define SCR_EL3_NS      0x00000001


/* HCR_EL2 bits  */
#define HCR_EL2_RW      0x80000000

#endif  /* _ARMV8DEF_H_ */

今回main関数ではbssセクションの初期化のみ行っています。 後述のリンカディレクティブファイルで定義しているbssセクション中に定義されたシンボルbss_start ~ bss_end間をゼロクリアします。

main.c

static void clearbss(void)
{
    unsigned long long *p;
    extern unsigned long long _bss_start[];
    extern unsigned long long _bss_end[];

    for (p = _bss_start; p < _bss_end; p++) {
        *p = 0LL;
    }
}

void main(void) {
     clearbss();
}

メモリ配置

qemu仮想マシンタイプはvirtを使用します。

メモリマップが記載されてるドキュメントがわからなかったのでソースで確認しました。

https://github.com/qemu/qemu/blob/master/hw/arm/virt.c のうちVIRT_MEMの部分が該当します。

static const MemMapEntry base_memmap[] = {
    /* Space up to 0x8000000 is reserved for a boot ROM */
    [VIRT_FLASH] =              {          0, 0x08000000 },
    [VIRT_CPUPERIPHS] =         { 0x08000000, 0x00020000 },
    /* GIC distributor and CPU interfaces sit inside the CPU peripheral space */
    [VIRT_GIC_DIST] =           { 0x08000000, 0x00010000 },
    [VIRT_GIC_CPU] =            { 0x08010000, 0x00010000 },
    [VIRT_GIC_V2M] =            { 0x08020000, 0x00001000 },
    [VIRT_GIC_HYP] =            { 0x08030000, 0x00010000 },
    [VIRT_GIC_VCPU] =           { 0x08040000, 0x00010000 },
    /* The space in between here is reserved for GICv3 CPU/vCPU/HYP */
    [VIRT_GIC_ITS] =            { 0x08080000, 0x00020000 },
    /* This redistributor space allows up to 2*64kB*123 CPUs */
    [VIRT_GIC_REDIST] =         { 0x080A0000, 0x00F60000 },
    [VIRT_UART] =               { 0x09000000, 0x00001000 },
    [VIRT_RTC] =                { 0x09010000, 0x00001000 },
    [VIRT_FW_CFG] =             { 0x09020000, 0x00000018 },
    [VIRT_GPIO] =               { 0x09030000, 0x00001000 },
    [VIRT_SECURE_UART] =        { 0x09040000, 0x00001000 },
    [VIRT_SMMU] =               { 0x09050000, 0x00020000 },
    [VIRT_PCDIMM_ACPI] =        { 0x09070000, MEMORY_HOTPLUG_IO_LEN },
    [VIRT_ACPI_GED] =           { 0x09080000, ACPI_GED_EVT_SEL_LEN },
    [VIRT_NVDIMM_ACPI] =        { 0x09090000, NVDIMM_ACPI_IO_LEN},
    [VIRT_PVTIME] =             { 0x090a0000, 0x00010000 },
    [VIRT_SECURE_GPIO] =        { 0x090b0000, 0x00001000 },
    [VIRT_MMIO] =               { 0x0a000000, 0x00000200 },
    /* ...repeating for a total of NUM_VIRTIO_TRANSPORTS, each of that size */
    [VIRT_PLATFORM_BUS] =       { 0x0c000000, 0x02000000 },
    [VIRT_SECURE_MEM] =         { 0x0e000000, 0x01000000 },
    [VIRT_PCIE_MMIO] =          { 0x10000000, 0x2eff0000 },
    [VIRT_PCIE_PIO] =           { 0x3eff0000, 0x00010000 },
    [VIRT_PCIE_ECAM] =          { 0x3f000000, 0x01000000 },
    /* Actual RAM size depends on initial RAM and device memory settings */
    [VIRT_MEM] =                { GiB, LEGACY_RAMLIMIT_BYTES },
};

VIRT_MEMがRAMの定義で、開始アドレスは 'GiB' となっています。

GiBhttps://github.com/qemu/qemu/blob/master/include/qemu/units.h に以下の定義となっています。

#define GiB     (INT64_C(1) << 30)

これよりRAMは0x40000000番地~となっています。

リンカディレクティブファイルはこれに合わせてRAM領域を定義しています。 セクション配置は単純にRAM上に順にtext, rodata, data, bss, stackセクションを定義しています。

aarch64_virt.ld

OUTPUT_ARCH( "aarch64" )
ENTRY( _start )
MEMORY
{
  ram (wxa!ri) : ORIGIN = 0x40000000, LENGTH = 128M
}
SECTIONS
{
  .text : {
    PROVIDE(_text_start = .);
    *(.reset)
    *(.text .text.*)
    PROVIDE(_text_end = .);
  } >ram AT>ram
  .rodata : {
    PROVIDE(_rodata_start = .);
    *(.rodata .rodata.*)
    *(.note.* )
    PROVIDE(_rodata_end = .);
  } >ram AT>ram
  .data : {
    . = ALIGN(4096);
    PROVIDE(_data_start = .);
    *(.data .data.*)
  } >ram AT>ram
  .bss :{
    . = ALIGN(16);
    PROVIDE(_bss_start = .);
    *(.bss .bss.*)
    . = ALIGN(16);
    PROVIDE(_bss_end = .);
  } >ram AT>ram
  .stack :{
    . = ALIGN(16);
    PROVIDE(_stack_start = .);
    . = . + 4096;
    PROVIDE(_stack_end = .);
  } >ram AT>ram
}

コンパイル

aarch64用クロスコンパイラでコンパイルします。

標準ライブラリがない環境を指定する-ffreestandingを指定しています。 今回の環境では、-ffreestanding指定をしていない場合、clearbss関数でイブラリのmemset関数を使用するコードが生成されリンクエラーが発生します。

-ffreestandingのオプション仕様はこちらから確認できます。 C Dialect Options (Using the GNU Compiler Collection (GCC))

aarch64-none-elf-gcc -c -O2 -ffreestanding -g main.c start.S vectors.S

aarch64-none-elf-ld -T aarch64_virt.ld main.o start.o vectors.o

生成された命令列を確認してみます。

$ aarch64-none-elf-objdump -d a.out

a.out:     file format elf64-littleaarch64


Disassembly of section .text:

0000000040000000 <_start>:
    40000000:   580002c5    ldr x5, 40000058 <EL1_start+0xc>
    40000004:   d51ec005    msr vbar_el3, x5
    40000008:   d51cc005    msr vbar_el2, x5
    4000000c:   d518c005    msr vbar_el1, x5
    40000010:   d53e1100    mrs x0, scr_el3
    40000014:   d2808021    mov x1, #0x401                  // #1025
    40000018:   aa010000    orr x0, x0, x1
    4000001c:   d51e1100    msr scr_el3, x0
    40000020:   d2b00002    mov x2, #0x80000000             // #2147483648
    40000024:   d51c1102    msr hcr_el2, x2
    40000028:   580001c4    ldr x4, 40000060 <EL1_start+0x14>
    4000002c:   9100009f    mov sp, x4

0000000040000030 <EL3_TO_EL1>:
    40000030:   580001c0    ldr x0, 40000068 <EL1_start+0x1c>
    40000034:   d51e4020    msr elr_el3, x0
    40000038:   d28038a0    mov x0, #0x1c5                  // #453
    4000003c:   d51e4000    msr spsr_el3, x0
    40000040:   910003f8    mov x24, sp
    40000044:   d51c4118    msr sp_el1, x24
    40000048:   d69f03e0    eret

000000004000004c <EL1_start>:
    4000004c:   94000009    bl  40000070 <main>
    40000050:   14000000    b   40000050 <EL1_start+0x4>
    40000054:   00000000    udf #0
    40000058:   40000800    .word   0x40000800
    4000005c:   00000000    .word   0x00000000
    40000060:   40002000    .word   0x40002000
    40000064:   00000000    .word   0x00000000
    40000068:   4000004c    .word   0x4000004c
    4000006c:   00000000    .word   0x00000000

0000000040000070 <main>:
    40000070:   b0000001    adrp    x1, 40001000 <_bss_end>
    40000074:   b0000000    adrp    x0, 40001000 <_bss_end>
    40000078:   91000022    add x2, x1, #0x0
    4000007c:   91000000    add x0, x0, #0x0
    40000080:   eb00005f    cmp x2, x0
    40000084:   54000142    b.cs    400000ac <main+0x3c>  // b.hs, b.nlast
    40000088:   d1000401    sub x1, x0, #0x1
    4000008c:   aa0203e0    mov x0, x2
    40000090:   cb020021    sub x1, x1, x2
    40000094:   927df021    and x1, x1, #0xfffffffffffffff8
    40000098:   91002021    add x1, x1, #0x8
    4000009c:   8b020021    add x1, x1, x2
    400000a0:   f800841f    str xzr, [x0], #8
    400000a4:   eb01001f    cmp x0, x1
    400000a8:   54ffffc1    b.ne    400000a0 <main+0x30>  // b.any
    400000ac:   d65f03c0    ret

Disassembly of section .entry.text:

0000000040000800 <default_vector_table>:
    40000800:   14000000    b   40000800 <default_vector_table>
    40000804:   d503201f    nop
    40000808:   d503201f    nop
    4000080c:   d503201f    nop
    40000810:   d503201f    nop
    40000814:   d503201f    nop
    40000818:   d503201f    nop
    4000081c:   d503201f    nop
    40000820:   d503201f    nop
    40000824:   d503201f    nop
    40000828:   d503201f    nop
    4000082c:   d503201f    nop
    40000830:   d503201f    nop
    40000834:   d503201f    nop
    40000838:   d503201f    nop
    4000083c:   d503201f    nop
    40000840:   d503201f    nop
    40000844:   d503201f    nop
    40000848:   d503201f    nop
    4000084c:   d503201f    nop
    40000850:   d503201f    nop
    40000854:   d503201f    nop
    40000858:   d503201f    nop
    4000085c:   d503201f    nop
    40000860:   d503201f    nop
    40000864:   d503201f    nop
    40000868:   d503201f    nop
    4000086c:   d503201f    nop
    40000870:   d503201f    nop
    40000874:   d503201f    nop
    40000878:   d503201f    nop
    4000087c:   d503201f    nop

0000000040000880 <curr_el_sp0_irq>:

  (以降同じ内容のベクタテーブルが続くので省略)

QEMU上で起動

生成されたオブジェクトa.outをaarch64用qemu上で起動します。 -d in_asmオプションを指定し、実行された命令を出力してみます。

-cpuオプションはARMv8-Aのcortex-A57を指定しています。 またqemuではオプションを指定しないとEL=1(EL3,EL2未実装状態)で実行開始されるようです。 -machineオプションを使用して vitrualizaion=true でEL2有効、secure=onでEL3有効としています。

$ qemu-system-aarch64 -machine virt,virtualization=true,secure=on -cpu cortex-a57 -machine type=virt -m 128M -nographic -no-reboot -kernel a.out -d in_asm

_startが配置された0x40000000番地から実行開始されているのがわかります。 msr/mrs命令の部分で(unknown)と出力されているのはqemuの逆アセンブル処理がシステムレジスタに対応していないためと思われます。

IN: 
0x0000000040000000:  580002c5      ldr x5, pc+88 (addr 0x40000058)
0x0000000040000004:  d51ec005      msr (unknown), x5

----------------
IN: 
0x0000000040000008:  d51cc005      msr (unknown), x5

----------------
IN: 
0x000000004000000c:  d518c005      msr (unknown), x5

----------------
IN: 
0x0000000040000010:  d53e1100      mrs x0, (unknown)
0x0000000040000014:  d2808021      mov x1, #0x401
0x0000000040000018:  aa010000      orr x0, x0, x1
0x000000004000001c:  d51e1100      msr (unknown), x0

----------------
IN: 
0x0000000040000020:  d2b00002      mov x2, #0x80000000
0x0000000040000024:  d51c1102      msr (unknown), x2

----------------
IN: 
0x0000000040000028:  580001c4      ldr x4, pc+56 (addr 0x40000060)
0x000000004000002c:  9100009f      mov sp, x4
0x0000000040000030:  580001c0      ldr x0, pc+56 (addr 0x40000068)
0x0000000040000034:  d51e4020      msr (unknown), x0

----------------
IN: 
0x0000000040000038:  d28038a0      mov x0, #0x1c5
0x000000004000003c:  d51e4000      msr (unknown), x0

----------------
IN: 
0x0000000040000040:  910003f8      mov x24, sp
0x0000000040000044:  d51c4118      msr (unknown), x24

----------------
IN: 
0x0000000040000048:  d69f03e0      unimplemented (UnconditionalBranchToRegister)

----------------
IN: 
0x000000004000004c:  94000009      bl #+0x24 (addr 0x40000070)

----------------
IN: main
0x0000000040000070:  b0000001      adrp x1, #+0x1000 (addr 0x40001000)
0x0000000040000074:  b0000000      adrp x0, #+0x1000 (addr 0x40001000)
0x0000000040000078:  91000022      add x2, x1, #0x0 (0)
0x000000004000007c:  91000000      add x0, x0, #0x0 (0)
0x0000000040000080:  eb00005f      cmp x2, x0
0x0000000040000084:  54000142      b.hs #+0x28 (addr 0x400000ac)

----------------
IN: main
0x00000000400000ac:  d65f03c0      ret

----------------
IN: 
0x0000000040000050:  14000000      b #+0x0 (addr 0x40000050)

main関数をret命令で抜けて実行によりstart.Sの最後に配置しておいたセルフジャンプに入ってることがわかります。

GDBで制御

qemu上のプログラムをgdbを用いて操作してみます。 まず、qemuをgdb接続待ちで立ち上げます。待ち受けるポートは10000以外でも構いません。

qemu-system-aarch64 -machine virt,virtualization=true,secure=on -cpu cortex-a57 -machine type=virt -m 128M -nographic -no-reboot -kernel a.out -S -gdb tcp::10000

別のターミナルウィンドウからgdbを起動し、localhostの10000ポートに接続します。

docker@28fcb11dde64:/work$ aarch64-none-elf-gdb a.out       
GNU gdb (GNU Toolchain for the A-profile Architecture 10.3-2021.07 (arm-10.29)) 10.2.90.20210621-git
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=x86_64-pc-linux-gnu --target=aarch64-none-elf".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://bugs.linaro.org/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.out...
(gdb) 
(gdb) target remote localhost:10000
Remote debugging using :10000
_start () at start.S:7
7       ldr     x5, =default_vector_table

逆アセンブルしてみます。 qemuの出力では(unknown)となっていたはmsr/mrs命令のシステムレジスタ名が表示されています。

(gdb) disassemble 
Dump of assembler code for function _start:
=> 0x0000000040000000 <+0>:    ldr x5, 0x40000058 <EL1_start+12>
   0x0000000040000004 <+4>:   msr vbar_el3, x5
   0x0000000040000008 <+8>:   msr vbar_el2, x5
   0x000000004000000c <+12>:  msr vbar_el1, x5
   0x0000000040000010 <+16>:  mrs x0, scr_el3
   0x0000000040000014 <+20>:  mov x1, #0x401                  // #1025
   0x0000000040000018 <+24>:  orr x0, x0, x1
   0x000000004000001c <+28>:  msr scr_el3, x0
   0x0000000040000020 <+32>:  mov x2, #0x80000000             // #2147483648
   0x0000000040000024 <+36>:  msr hcr_el2, x2
   0x0000000040000028 <+40>:  ldr x4, 0x40000060 <EL1_start+20>
   0x000000004000002c <+44>:  mov sp, x4
End of assembler dump.

例外レベルを確認してみます。 これはアーキテクチャ仕様としてはPSTATEで確認するのですが、 gdb上では過去のアーキテクチャを踏襲しているためかPSTATEというレジスタ名は使用できずcpsrとしてアクセスする必要があるようです。

(gdb) info reg cpsr
cpsr           0x400003cd          1073742797

この値のbit3,2の部分がELを示し、現在EL=3となっています。

EL3からEL1に遷移したところまで実行してみます。

(gdb) b EL1_start 
Breakpoint 1 at 0x4000004c: file start.S, line 41.
(gdb) c
Continuing.

Breakpoint 1, EL1_start () at start.S:41
41      bl      main

ここで再びcpsrを確認するとEL1になっていることがわかります。

(gdb) info reg cpsr
cpsr           0x1c5               453

最後に

今回のブート処理ではAArch64ステートで実行し例外レベルをEL1にして特権プログラムの実行の準備までを行いました。 次回は割り込みを動かしてみようと思います。