/home/tnishinaga/TechMEMO

日々行ったこと、面白かったことを書き留めます。

QEMUのARM Virtマシンのペリフェラルメモリマップについてのメモ

QEMUのARM Virtマシンのペリフェラルメモリマップは何処を見ればいいんだっけ......とよく忘れるので、パスと見るべきところをメモします。

マシンを定義しているファイルはこれ。

qemu/virt.c at master · qemu/qemu · GitHub

メモリマップは125行目辺り MemMapEntry a15memmap[] にかかれています。 以下、該当コードを引用します。

static const MemMapEntry a15memmap[] = {
    /* 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 },
    /* 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_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 },
    [VIRT_MEM] =                { 0x40000000, RAMLIMIT_BYTES },
    /* Second PCIe window, 512GB wide at the 512GB boundary */
    [VIRT_PCIE_MMIO_HIGH] =   { 0x8000000000ULL, 0x8000000000ULL },
};

どのペリフェラルが使われているかはここからはわからないので、memmapの1列目の文字列から探していく必要があります。

例えばUARTなら、VIRT_UARTで検索をすると1422行目あたりに create_uart 関数を呼び出しているところが見つかります。

create_uart(vms, pic, VIRT_UART, sysmem, serial_hds[0]);

この create_uart 関数の定義は644行目あたりにあり、このコードを見ていくと頭の方にペリフェラルの名前が書いてあるので、PL011が使われていることがわかります。

static void create_uart(const VirtMachineState *vms, qemu_irq *pic, int uart,
                        MemoryRegion *mem, Chardev *chr)
{
    char *nodename;
    hwaddr base = vms->memmap[uart].base;
    hwaddr size = vms->memmap[uart].size;
    int irq = vms->irqmap[uart];
    const char compat[] = "arm,pl011\0arm,primecell";

後はペリフェラルの資料を探して読んでドライバを実装すればOKです。

おしまい。

UEFIのHTTP BOOTを試してみる

UEFI 2.5からネットワークブートの方法として"HTTP BOOT"が増えました。

今回はこのHTTP BOOTは従来の方式に比べ何が嬉しいのか、この機能を使ってUEFIのアプリを起動するにはどのようにすればよいかを書いていこうと思います。

忙しい人のまとめ

  • ネットワークブートがHTTP経由でできます
  • DHCPの設定変更なしでもOKです
  • 対応バージョンはUEFI 2.5以上です

HTTP BOOT とは

HTTP BOOTはUEFI 2.5で追加されたネットワークブートのためのUEFIの機能です。

この機能を使うと、HTTP(またはHTTPS)サーバーからファイルを取得し、UEFIアプリやOSを起動することができるようになります。

HTTP BOOT と PXEブートの違い

よく知られたネットワークブートの方法としてPXEブートがあります。これとHTTP BOOTはどのような点が異なるのでしょうか。 この疑問に関しては、以下のSUSE LinuxのドキュメントのIntroductionにわかりやすく書かれています。

UEFI HTTPBoot with OVMF - openSUSE

ここに書かれている内容をまとめると、HTTP BOOTではPXEに比べ以下の点が異なります。

  • HTTP URLを使って起動イメージ指定が可能
  • アドレス指定にDNSが利用可能
  • ローカルネットワーク外からもネットワークブート可能

また、UEFIの仕様書(24.7 HTTP Boot)やtinanocoreの解説サイトを読むと、以下の点でも異なることがわかります。

  • DHCPサーバーの設定変更が必須ではない(家庭用ルーターでもネットワークブート可能)
  • TFTPサーバー不要

このように、HTTP BOOTでは従来PXEによるネットワークブートを行うために必要であったDHCPサーバーの設定変更、TFTPサーバーの用意等が必要なくなるため、ネットワークブートのハードルが非常に低くなります。

実際にどのくらい簡単になるかを、想定環境の説明より確認していきましょう。

HTTP BOOTの想定環境

HTTP BOOTの想定環境には、以下の2つがあります。

  • Corporate Environment
  • Home Environment

それぞれどのような想定環境かを見ていきましょう。

Corporate Environment

最初にCorporate Environmentについて説明するために、UEFIの仕様書「24.7.2.1 Use in Corporate environment」のFigure 76より構成図を引用します。

f:id:tnishinaga:20171221030526p:plain

Corporate Environmentの想定は、基本的には従来のPXEブートを行える環境と同様です。

この環境ではDHCPサーバーに対しHTTP BOOT Extensionの設定を入れることで、クライアントの起動時にDHCPからアドレスと同時にHTTP BOOTのURL等をもらい、HTTP BOOTを使ったネットワークブートを開始することが可能です。

よって、Corporate Environmentでサーバー側で用意する必要が出るのは、最大で以下の3つになります。

  • "HTTP Extension設定可能"なDHCPサーバー(必須)
  • DNSサーバー(ドメイン名を使う場合は必要。IPアドレス指定の場合は不要)
  • HTTP(S)サーバー(外部の配布サイトを使う場合は不要)

Home Environment

次にHome Environmentについて説明するために、UEFIの仕様書「24.7.2.2 Use case in Home environment」のFigure 77より構成図を引用します。

f:id:tnishinaga:20171221032502p:plain

この環境はDHCPサーバーにHTTP BOOT Extensionの設定を入れることのできない、一般的な家庭用ルーターでHTTP BOOTを行うことを想定しています。 この環境では一般的なDHCPサーバーからIPアドレス(とDNSサーバー等の情報)のみを取得し、HTTP BOOTのURL等はユーザーが自分で入力することでHTTP BOOTを行います。

よって、Home Environmentでサーバー側で用意する必要が出るのは、最大で以下の3つになります。

  • 一般的なDHCPサーバー(必須)
  • DNSサーバー(ドメイン名を使う場合は必要。IPアドレス指定の場合は不要)
  • HTTP(S)サーバー(外部の配布サイトを使う場合は不要)

HTTP BOOTでUEFIアプリを動かしてみる

HTTP BOOTの使い方を確かめるために、Home Environmentにおいて実際にUEFIアプリをHTTP BOOTしてみようと思います。 なお、今回は動作確認が目的なので、OSの起動やインストール等まではやりません。

また、ターゲットはARM64環境とします。趣味です。

実験環境はいつもおなじみ Thinkpad x201と Arch Linux(x86_64) です。

HTTP BOOTに対応したUEFIのビルド

LinaroのビルドしてくれているQemuのイメージはHTTP BOOTが有効になっていなかったので、自分で有効にしてedk2からビルドし直します。

変更するところは edk2/ArmVirtPkg/ArmVirtQemu.dsc の以下の部分です。

  DEFINE HTTP_BOOT_ENABLE        = FALSE

ここのFALSEをTRUEにするとHTTP BOOTが有効になります。

!if $(HTTP_BOOT_ENABLE) == TRUE
  HttpLib|MdeModulePkg/Library/DxeHttpLib/DxeHttpLib.inf
!endif

残りのビルドの方法はansibleのスクリプトにしたので、このスクリプトを動かすか、中身を読んで同じことをコマンドで実行すればHTTP BOOTが有効になったQEMU用のUEFIファームウェア(ARM64用)が edk2/Build/ArmVirtQemu-AARCH64/RELEASE_GCC5/FV/QEMU_EFI.fd から得られます。

github.com

QEMU環境の用意

HTTP BOOTを行うには、QEMU仮想マシンを、ホストマシンが繋がっている家庭用のLANにつなげる必要があります。

このために、QEMUの作り出すTAPデバイスとホストマシンのNICをブリッジで繋ぐ必要があります。

この作業は理解するのが大変なので、勝手に行ってくれるスクリプトを作りました。

github.com

最初に、init.shIFACE="enp0s25" のenp0s25のところを任意の物理NIC名に変更して、init.shを実行してください。

sudo ./init.sh

これでホスト側にブリッジデバイスの作成とブリッジデバイスへのNICの追加が終わります。

次に、 run_qemu_aarch64.sh を実行すると、自動で qemu-ifupが実行され、QEMUの作ったTAPデバイスがブリッジに追加されます。

./run_qemu_aarch64.sh 

これで、LANにつながるQEMU仮想マシン環境が出来上がります。

HTTP BOOT

./run_qemu_aarch64.sh を実行してQEMU上でUEFI Shellが立ち上がったら、いよいよHTTP BOOTを行ってみます。

まずはじめに、IPアドレスの取得を行います。 これはUEFI Shellの上で以下のコマンドを打つと、簡単に設定できます。

ifconfig -s eth0 dhcp

UEFI Shell上ではpingコマンドが使えるので、ホストマシンのIPアドレスPingをうってみて、疎通を確認してください。 疎通が確認できれば、次に進みます。

HTTP BOOTはUEFI Shell上ではコマンドがなく行えないため、Shell上で exit コマンドを実行して一度マネージャーに戻ります。

f:id:tnishinaga:20171222221742p:plain

するとこのような画面が出てくるので、ここから Device Manager -> Network Device List -> NICのMACアドレスを選択 -> HTTP Boot Configuration と進みます。

f:id:tnishinaga:20171222012657p:plain

するとこのような画面が得られるので、 Boot URI のところに *.efi のあるURLを指定します。

今回はhttpboot/hello 以下でビルドしたmain.efiを以下のコマンドで起動したHTTPサーバーから配布しています。

python3 -m http.sever

ホストのIPアドレス192.168.111.50 でHTTPサーバーの待受ポートは8000番なので、http://192.168.111.50:8000/main.efiURIとして指定しています。

設定が終わったら ESCキー を押してください。 すると「設定を保存するならYをおしなさい」というメッセージが出るので、Yを押して保存します。 その後なんどかESCを押して、以下のマネージャーのTOPに戻ってください。

f:id:tnishinaga:20171222221742p:plain

次にBoot Manager を開くと一番最後に UEFI HTTP という項目が増えているはずなので、ここを選択してEnterを押します。

f:id:tnishinaga:20171222013348p:plain

しばらく待つとUEFIアプリが読み込まれて、私の作ったサンプルでは hello が表示されます。

f:id:tnishinaga:20171222014036p:plain

一つ注意として、HTTP BOOTはUEFIアプリの実行が終わるとすぐにブートマネージャーの画面に戻されてしまいます。 そうならないようしたい場合は、終了前にキー入力を待つような処理を最後に入れる必要がありそうです。

おわり

すみません、最近時間がなくて細かいところまで丁寧に書くことができませんでした。

質問をコメントでいただければ補足しますので、よろしくお願いします。

UEFIで任意のProtocolを使ってプログラムを書くためにはどうすればいいのかメモ

ふと、UEFIアプリでシリアルを直接さわって通信がしたくなりました。

UEFI 2.0にはEFI SERIAL IO PROTOCOLというプロトコルがあり、これを用いればシリアル通信をハードウェアを直接触ること無く行えるようです。

早速、このプロトコルを使ってシリアル通信をするプログラムを作ろう……としたのですが、大変お恥ずかしいことにその時の私はUEFIで任意のプロトコルを呼び出して制御を行う方法がイマイチわかっておらず、すぐに作ることができませんでした。

その後、KernelVM探検隊のみなさまにUEFIについて質問したところ、参考になるサイトやプログラムを教えていただき、以下のシリアルの入力をそのまま出力に返すプログラムを作ることができました。

baremetal_hikeyboard/uefi_serial_echo_sample at master · tnishinaga/baremetal_hikeyboard · GitHub

このプログラムを作ったことでUEFIアプリで任意のプロトコルを使うために、なんとなくどのようなことをすべきかが見えるようになってきたので、初心者の心を忘れないうちに書き残しておこうと思います。

注意

  • 本記事はどうすれば目的のプロトコルを取得し、インターフェースを使えるかに注目します。UEFIのすべてを解説する記事ではありません
  • 一部筆者の理解が足りていないため、説明に誤りがある場合があります。コメントで教えていただけると嬉しいです。

任意のプロトコルの取得と利用方法

UEFIの提供する様々な機能はインターフェースを介して利用できるようになっており、 インターフェースは「プロトコル(Protocol)」と呼ばれる組でグルーピングされて提供されています。

プロトコルの取得は、以下の手順で行なえます。

  1. BootService(BS)のLocateHandleで目的のプロトコルを取得できるハンドラを見つける
  2. BSのHandleProtocolで先程取得したハンドラから目的のプロトコルを取得する

ハンドラの取得

プログラム上でのプロトコルは、各インターフェースの関数ポインタを格納した構造体であり、メモリ上の何処かに置かれています。

そのため、目的のインターフェースを使うためには、プロトコルの先頭アドレスを取得し、プロトコルの構造体にキャストし、そこから関数ポインタを使って目的のインターフェースを使うということが必要になります。

プロトコルを取得するためには、そのプロトコルを取得できるハンドル(Handle)を見つける必要があります。 この作業を行う関数がLocateHandleです。 プロトコルにはGUIDというIDがあるため、LocateHandleに目的のプロトコルのGUIDを渡すことで、ハンドルを見つけてきてくれます。

詳しい使い方は以下を参照。

EFI BOOT SERVICES - PhoenixWiki

今回作ったサンプルでは、以下の部分がハンドラの取得部分になります。

    EFI_GUID serial_io_protocol = SERIAL_IO_PROTOCOL;
    EFI_STATUS efi_status;

    // ... skip ...

    // search handler
    UINTN handlers_size = 0;
    EFI_HANDLE *handlers = NULL;
    efi_status = uefi_call_wrapper(
        BS->LocateHandle,
        5,
        ByProtocol,
        &serial_io_protocol,
        0,
        &handlers_size,
        handlers
    );
    if (efi_status == EFI_BUFFER_TOO_SMALL) {
        efi_status = uefi_call_wrapper(BS->AllocatePool,
            3,
            EfiBootServicesData,
            handlers_size,
            (VOID **)&handlers
        );
        efi_status = uefi_call_wrapper(
            BS->LocateHandle,
            5,
            ByProtocol,
            &serial_io_protocol,
            0,
            &handlers_size,
            handlers
        );
    }
    if (handlers == NULL || EFI_ERROR(efi_status)) {
        FreePool(handlers);
        return efi_status;
    }

このコードではEFI SERIAL IO PROTOCOLを取得できるハンドラを探しています。

このプロトコルにはシリアルポートを選択するインターフェースが無いため、ポートの数だけ与えられたハンドラを切り替えて使って、シリアルポートの選択を行うようです。

そのため、LocateHandleを1度実行した後、ハンドラのサイズが足りない場合はAllocatePool(UEFI版のmallocのようなもの)でメモリを確保し、再度LocateHandleを実行するようになっています。

プロトコルの取得

先程取得したハンドラと目的のプロトコルのGUIDより、HandleProtocolを使ってプロトコルを取得します。

HandleProtocolの使い方は以下を参照。

EFI BOOT SERVICES - PhoenixWiki

今回作ったサンプルでは、以下の部分がハンドラからのプロトコル取得部分になります。

 efi_status = uefi_call_wrapper(
        BS->HandleProtocol,
        3,
        handlers[0],
        &serial_io_protocol,
        (VOID **)&serialio
    );

    if (EFI_ERROR(efi_status)) {
        Print(L"efi_status is not EFI_SUCCESS\n");
        return efi_status;
    }

インターフェースの利用

プロトコルのもつインターフェースの利用は、構造体のメンバにアロー演算子でアクセスして使うだけです。

今回作ったサンプルでは、以下の部分が該当します。

efi_status = uefi_call_wrapper(serialio->GetControl, 2, serialio, &control);

このコードではEFI SERIAL IO PROTOCOLのもつGetConrolインターフェースを呼び出し、シリアルポートのインターフェース情報を取得しています。

大体こんな感じでUEFIのハンドラ取得、プロトコル取得、インターフェース利用が行なえます。

質問と回答コーナー

LocateHandle使わずにLocateProtocolで直接Protocol取得してるひともいるけど、違いは何?

僕もよくわかってません。 ハンドル取得してからプロトコル取得が正しい道だとは思うのですが……

uefi_call_wrapperってなに? なぜ使う必要があるの? 引数のマジックナンバーの意味は何?

後で追記します……

vagrant-azureとansibleを使ってRaspberry PiのLinux Kernelをビルドしてみた話

vagrant-azureを使ってAzureの上にVMを建てることを前回行いました。

vagrant-azureを使ってAzureに仮想マシンを建ててみたメモ - /home/tnishinaga/TechMEMO

今回はこれに加えてansibleを使ってRaspberry PiLinux Kernelをビルドすることを行ってみたいと思います。

スクリプトの修正とLinux Kernelのビルド

ansibleを使ったプロビジョンの追加

前回書いたVagrantfileのVagrant.configureのところに以下を追記します。

# ansible provision
config.vm.provision 'ansible' do |ansible|
  ansible.playbook = "provisioning/playbook.yml"
end

これでprovisioning/playbook.ymlの記述に従って、Ansibleを使ったプロビジョンを行えるようになります。

追記した後のVagrantfileはこんな感じになります。

require 'dotenv'
Dotenv.load

Vagrant.configure('2') do |config|
  # ansible provision
  config.vm.provision 'ansible' do |ansible|
    ansible.playbook = "provisioning/playbook.yml"
  end

  config.vm.box = 'azure'

  # use local ssh key to connect to remote vagrant box
  config.ssh.private_key_path = '~/.ssh/id_rsa'
  config.vm.provider :azure do |azure, override|

    # each of the below values will default to use the env vars named as below if not specified explicitly
    azure.tenant_id = ENV['AZURE_TENANT_ID']
    azure.client_id = ENV['AZURE_CLIENT_ID']
    azure.client_secret = ENV['AZURE_CLIENT_SECRET']
    azure.subscription_id = ENV['AZURE_SUBSCRIPTION_ID']
    azure.location = ENV['AZURE_LOCATION']
  end
end

その他必要なファイルは前回の記事を参照してください。

Ansible-Playbookを作る

  • Ubuntu のアップデート
  • ビルドに必要なパッケージのインストール
  • ロスコンパイラの取得とインストール
  • Linux kernel sourceの取得
  • Raspberry Pi(BCM2835)用configの設定
  • Linux Kernelのビルド

以上を行うAnsible playbookをつくります。

hostsのところはallにしておけば、inventoryファイルはいらないようです。

# update
- hosts: all
  tasks:
    - name: install basic packages
      apt: name={{ item }} update_cache=yes state=latest
      with_items:
        - git
        - build-essential
        - bc
      become: true
    - name: install cross-compiler
      unarchive:
        src: https://releases.linaro.org/components/toolchain/binaries/latest/arm-eabi/gcc-linaro-6.3.1-2017.05-x86_64_arm-eabi.tar.xz
        dest: /usr/local/
        remote_src: True
      become: true
    - name: add /usr/local/gcc-linaro-6.3.1-2017.05-x86_64_arm-eabi/bin to path
      lineinfile:
        dest: .bashrc
        insertafter: EOF
        line: "export PATH=$PATH:/usr/local/gcc-linaro-6.3.1-2017.05-x86_64_arm-eabi/bin"
    - name: clone linux source
      git:
        repo: https://github.com/torvalds/linux
        dest: linux
        version: master
        depth: 1
    - name: load bcm2835_defconfig
      command: "make bcm2835_defconfig"
      args:
        chdir: ./linux
      environment:
        PATH: "{{ ansible_env.PATH }}:/usr/local/gcc-linaro-6.3.1-2017.05-x86_64_arm-eabi/bin"
        ARCH: arm
        CROSS_COMPILE: arm-eabi-
    - name: make linux-kernel
      command: "make -j4 zImage dtbs"
      args:
        chdir: ./linux
      environment:
        PATH: "{{ ansible_env.PATH }}:/usr/local/gcc-linaro-6.3.1-2017.05-x86_64_arm-eabi/bin"
        ARCH: arm
        CROSS_COMPILE: arm-eabi-

ビルドしてみる

前回同様、Vagrantfileのあるディレクトリで以下のコマンドを実行するとクラウド上のVM起動からLinux Kernelのビルドまでが行われます。

vagrant up

使い終わったら vagrant destroy でマシンを潰すのを忘れないようにしてください。

性能計測

このスクリプトを使ったビルドに必要な時間を計測して、より速くビルドするにはどうすればよいかを考えてみたいと思います。

ビルド時間の概要

今回のビルドにかかる時間は、大まかに以下の2つに分けることができると考えています。

  • Azure上でVMを用意して立ち上げるまでの時間
  • ansibleのプロビジョンにかかる時間

VM立ち上げとプロビジョン完了までの時間はtimeコマンドを使えば計測可能です。 なので、後者のプロビジョン時間を知ることができれば、計算でVM立ち上げまでの時間を得ることができるでしょう。

VM立ち上げの時間はこちら側で速くすることは難しそうなので、後者のプロビジョン時間を速くする方向性で考えていきたいと思います。

プロビジョン時間の計測

ansibleのプロビジョンの各タスクの実行にかかった時間はansible-profileプラグインを使うことで調べることができるようです。

このプラグインは現在ansibleにデフォルトで入っているので、少し設定を追記するだけで利用可能です。

ansibleの設定は、Vagrantfileと同じディレクトリにanisble.cfg というファイルを作り、そこに書き込めば良いようです。 以下の内容を書いた anisble.cfg を作ってください。

[defaults]
callback_whitelist = profile_tasks

そして、再度 vagrant up を行うと、今度はプロビジョンにかかった詳細な時間が表示されるはずです。

計測結果の確認と検討

私の環境でのプロビジョン時間の結果の概要を以下に示します。

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
Friday 04 August 2017  00:47:21 +0900 (0:00:00.144)       0:00:00.144 ********* 
ok: [default]

TASK [install basic packages] **************************************************
Friday 04 August 2017  00:47:25 +0900 (0:00:03.483)       0:00:03.628 ********* 
changed: [default] => (item=[u'git', u'build-essential', u'bc'])

TASK [install cross-compiler] **************************************************
Friday 04 August 2017  00:48:00 +0900 (0:00:35.629)       0:00:39.257 ********* 
changed: [default]

TASK [add /usr/local/gcc-linaro-6.3.1-2017.05-x86_64_arm-eabi/bin to path] *****
Friday 04 August 2017  00:50:04 +0900 (0:02:03.371)       0:02:42.628 ********* 
changed: [default]

TASK [clone linux source] ******************************************************
Friday 04 August 2017  00:50:06 +0900 (0:00:01.734)       0:02:44.363 ********* 
changed: [default]

TASK [load bcm2835_defconfig] **************************************************
Friday 04 August 2017  00:51:01 +0900 (0:00:55.154)       0:03:39.517 ********* 
changed: [default]

TASK [make linux-kernel] *******************************************************
Friday 04 August 2017  00:51:07 +0900 (0:00:06.207)       0:03:45.725 ********* 
changed: [default]

PLAY RECAP *********************************************************************
default                    : ok=7    changed=6    unreachable=0    failed=0   

Friday 04 August 2017  00:57:02 +0900 (0:05:54.616)       0:09:40.341 ********* 
=============================================================================== 
make linux-kernel ----------------------------------------------------- 354.62s
install cross-compiler ------------------------------------------------ 123.37s
clone linux source ----------------------------------------------------- 55.15s
install basic packages ------------------------------------------------- 35.63s
load bcm2835_defconfig -------------------------------------------------- 6.21s
Gathering Facts --------------------------------------------------------- 3.48s
add /usr/local/gcc-linaro-6.3.1-2017.05-x86_64_arm-eabi/bin to path ----- 1.73s

結果を見ると、以下の順に時間がかかっているようです。

  1. Linux Kernelのビルド(354秒)
  2. ロスコンパイラのダウンロード(123秒)
  3. Linux Kernel Sourceのクローン(ダウンロード)(55秒)

(4位のUbuntuのアップデートとパッケージのインストールは高速化が難しいのでパスします。)

一番時間のかかっているLinux Kernelのビルドは、使用するVMの性能を上げれば短縮できそうです。 もちろん、性能の良いマシンはお金がかかるので、費用対効果も考える必要がありそうです。

2番目に時間のかかっているクロスコンパイラのダウンロードは、永続ディスクなどを作ってそこに保存しておいたものを使うといったことを行えれば速くできそうです。現在は何度も実行するとダウンロード先に負荷をかけてしまうかもしれないので、この改善は早めに行ったほうが良さそうです。

3番目のSourceのクローンも2番めと同様に、永続ディスクを作って保存しておいて、差分だけpullしてくれば速くできそうな気がします。

おわり

今回はvagrant-azureとansibleを使って、Azure上でLinux Kernelのビルドを行い、その実行時間を測ることと、どうすれば改善できそうかを考えました。

次回はより速くビルドできるよう、工夫してみようと思います。

vagrant-azureを使ってAzureに仮想マシンを建ててみたメモ

自宅開発環境が貧弱で大変厳しいので、コーディング環境含めクラウド上で行えるようにできないかと色々試しています。 とりあえず最低限SSH使える環境がアレばスマフォでもコーディングとビルドが行える環境を作るのが目標です。

クラウドは使っている間だけ課金されるので、使うときにマシンを建てて終わったら即潰す運用をすると安く済みそうです。 潰し忘れを防ぐため、インスタンスの立ち上げも落とすのもすべて自動でやりたいです。

今回はその第一歩として、READMEを読みながらVagrantを使ってMicrosoft Azure上に仮想マシンを建ててみました。

必要アプリのインストール

Vagrant, Azure-cli, vagrant-azure pluginとdotenvをインストールする必要があります。

Vagrant

やるだけ

Azure-cli

Azure-cliのインストール方法はgithubのREADMEを参照して行うだけです。

github.com

curl -L https://aka.ms/InstallAzureCli | bash

インストールはデフォルトだと ~/ 以下になります。 変えたいときはインストール時にパスを入力すると、その下においてくれます。

===> In what directory would you like to place the install? (leave blank to use '/home/tnishinaga/lib/azure-cli'): ~/opt/azure/lib/azure-cli
-- We will install at '/home/tnishinaga/opt/azure/lib/azure-cli'.             
===> In what directory would you like to place the 'az' executable? (leave blank to use '/home/tnishinaga/bin'): ~/opt/azure/bin

vagrant-azure

vagrant-azureプラグインも入れるだけ……なのですが、私の環境ではエラーが起きたので頑張ってインストールしました。 同じ問題が起きた方は参考にしてください。

tnishinaga.hatenablog.com

dotenv

dotenvをインストールすると.envに書いてある設定をVagrant内で読み込むことができるようになります。 この.envにアクセスキー等を書いておいて、.gitignoreに登録しておけばアクセスキーなどのネットに上がっては困る情報を間違えてネットにあげてしまうリスクを下げることができるようです。

vagrant-azureのREADMEにはインストールするよう書いてありませんが、サンプルで使ってるのでインストールします。

vagrant plugin install dotenv

vagrant-azureで仮想マシンを建ててみる

基本はvagrant-azureのREADMEにあるGetting Startedの手順をなぞるだけです。

github.com

初期設定

Azureへのログイン

az login でAzureにログインします。

あるURLにアクセスしてこのコードを入力してくださいと言われるので、そのとおりにします。

Azure Active Directory Applicationを作る

az ad sp create-for-rbac コマンドでAzure Active Directory Applicationというものを作ります。 以下のようなメッセージが出てくるはずなので、tenant, appId, passwordをメモっておいてください。

{
  "appId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "displayName": "some-display-name",
  "name": "http://azure-cli-2017-04-03-15-30-52",
  "password": "XXXXXXXXXXXXXXXXXXXX",
  "tenant": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}

サブスクリプションIDの取得

サブスクリプションIDも必要なので az account list --query "[?isDefault].id" -o tsv で取得してメモしておきます。

Vagrantfileの作成

以下のようなVagrantfileを作ります。

require 'dotenv'
Dotenv.load

Vagrant.configure('2') do |config|
  config.vm.box = 'azure'

  # use local ssh key to connect to remote vagrant box
  config.ssh.private_key_path = '~/.ssh/id_rsa'
  config.vm.provider :azure do |azure, override|

    # each of the below values will default to use the env vars named as below if not specified explicitly
    azure.tenant_id = ENV['AZURE_TENANT_ID']
    azure.client_id = ENV['AZURE_CLIENT_ID']
    azure.client_secret = ENV['AZURE_CLIENT_SECRET']
    azure.subscription_id = ENV['AZURE_SUBSCRIPTION_ID']
  end

end

vagrant-azureのREADMEとの差異として、以下を先頭に追記しています。

require 'dotenv'
Dotenv.load

これは、AZURE_TENANT_IDなどの値を.envから引いてくるために使われるdotenvプラグインを読みこむために必要です。

.envファイルを作る

.envファイルを作り、先程メモしたtenant, appId, password、サブスクリプションIDをここに入力します。

AZURE_TENANT_ID=tenant
AZURE_CLIENT_ID=appId
AZURE_CLIENT_SECRET=password
AZURE_SUBSCRIPTION_ID=サブスクリプションID

この.envファイルはgithub等にあげてしまうと悪用されてしまうので、.gitignoreに追加して公開されないようにしておきます。

echo ".env" >> .gitignore

ssh-keyを作っていない方は、ssh-keygenでid_rsaとid_rsa.pubの鍵ペアを作って置く必要もあります。

仮想マシンを起動してみる

vagrant box add azure https://github.com/azure/vagrant-azure/raw/v2.0/dummy.box --provider azure

したあと

vagrant up --provider=azure

仮想マシンがAzureで立ち上がるはずです。

立ち上がった後は、 vagrant sshSSHアクセスができます。

ロケーションのエラーが出た場合

私の使ってるサブスクリプションの問題で、エラーがでて起動できないことがありました。

  "response": {                                                                                                                                                                                                                                    
    "body": "{\"status\":\"Failed\",\"error\":{\"code\":\"DeploymentFailed\",\"message\":\"At least one resource deployment operation failed. Please list deployment operations for details. Please see https://aka.ms/arm-debug for usage details.
\",\"details\":[{\"code\":\"Conflict\",\"message\":\"{\\r\\n  \\\"error\\\": {\\r\\n    \\\"code\\\": \\\"SkuNotAvailable\\\",\\r\\n    \\\"message\\\": \\\"The requested tier for resource '/subscriptions/6af6c932-69d5-441b-9ed1-fbd7a6dc4e09/r
esourceGroups/polished-voice-4/providers/Microsoft.Compute/virtualMachines/fragrant-sun-83' is currently not available in location 'westus' for subscription '6af6c932-69d5-441b-9ed1-fbd7a6dc4e09'. Please try another tier or deploy to a differe

ログを見る限りでは、デフォルトで選択されるwestusロケーションが使えなかったようです。

これはlocationの設定を変更すると回避できるそうなので、とりあえず色々試して東南アジア(southeastasia)を選択しました。

ロケーション選択追記後のVagrantfileは以下のようになります。

require 'dotenv'
Dotenv.load

Vagrant.configure('2') do |config|
  config.vm.box = 'azure'

  # use local ssh key to connect to remote vagrant box
  config.ssh.private_key_path = '~/.ssh/id_rsa'
  config.vm.provider :azure do |azure, override|

    # each of the below values will default to use the env vars named as below if not specified explicitly
    azure.tenant_id = ENV['AZURE_TENANT_ID']
    azure.client_id = ENV['AZURE_CLIENT_ID']
    azure.client_secret = ENV['AZURE_CLIENT_SECRET']
    azure.subscription_id = ENV['AZURE_SUBSCRIPTION_ID']
    azure.location = "southeastasia"
  end

end

仮想マシンを潰す

使い終わったら、vagrant destroy でマシンを潰せます。

おしまい

とりあえずこれで仮想マシンの立ち上げと削除ができることが確認できました。

次はansibleを用いて環境の自動設定をやってみようと思います。

Arch Linuxのマシンにvagrant-azureプラグインを入れる話

vagrantを使ってAzureに仮想マシンを建てようとしたらそれ以前のところで躓いたので備忘録として残しておきます。

問題が起こった環境

Arch Linux(x86_64)

$ yaourt vagrant
1 community/vagrant 1.9.5-1 [installed]
    Build and distribute virtualized development environments
2 community/vagrant-substrate 605.0566498-2 [installed]
    Substrate layer for Vagrant

問題1 : SEGVする

$ vagrant plugin install vagrant-azure --plugin-version '2.0.0.pre8'                                                                                                                                                   
Installing the 'vagrant-azure --version '2.0.0.pre8'' plugin. This can take a few minutes...                                                                                                                                                       
/opt/vagrant/embedded/lib/ruby/2.2.0/rubygems/source.rb:192: [BUG] Segmentation fault at 0x00000000000000                                                                                                                                          
ruby 2.2.5p319 (2016-04-26 revision 54774) [x86_64-linux] 

この問題はvagrant-substrateのバージョンをダウングレードして599.d7cedfe-2にすると回避できました。

arch linux - Install vagrant plugin on archlinux - Server Fault

ダウングレードにはAURにあるdowngraderを使いました。

パッケージのダウングレード - ArchWiki

$ downgrader vagrant-substrate
 Downgrade package: vagrant-substrate  
1: vagrant-substrate-605.056649/var/cache/pacman/pkg/vagrant-substrate-605.0566498-2-x86_64.pkg.tar.xz  (from cache)
2: vagrant-substrate-605.056649/var/cache/pacman/pkg/vagrant-substrate-605.0566498-1-x86_64.pkg.tar.xz  (from cache)
3: vagrant-substrate-599.d7cedfhttps://archive.archlinux.org/packages/v/vagrant-substrate/vagrant-substrate-599.d7cedfe-2-x86_64.pkg.tar.xz  [installed]
4: vagrant-substrate-599.d7cedfhttps://archive.archlinux.org/packages/v/vagrant-substrate/vagrant-substrate-599.d7cedfe-1-x86_64.pkg.tar.xz  (from ALA)
5: vagrant-substrate-582.d66af9https://archive.archlinux.org/packages/v/vagrant-substrate/vagrant-substrate-582.d66af96-2-x86_64.pkg.tar.xz  (from ALA)
6: vagrant-substrate-582.d66af9/var/cache/pacman/pkg/vagrant-substrate-582.d66af96-1-x86_64.pkg.tar.xz  (from cache)
7: vagrant-substrate-575.af2838https://archive.archlinux.org/packages/v/vagrant-substrate/vagrant-substrate-575.af28386-1-x86_64.pkg.tar.xz  (from ALA)
8: vagrant-substrate-569.8bb245https://archive.archlinux.org/packages/v/vagrant-substrate/vagrant-substrate-569.8bb245a-1-x86_64.pkg.tar.xz  (from ALA)
9: vagrant-substrate-554.977218https://archive.archlinux.org/packages/v/vagrant-substrate/vagrant-substrate-554.9772186-2-x86_64.pkg.tar.xz  (from ALA)
10: vagrant-substrate-554.977218https://archive.archlinux.org/packages/v/vagrant-substrate/vagrant-substrate-554.9772186-1-x86_64.pkg.tar.xz  (from ALA)
11: vagrant-substrate-526.6bb2e8https://archive.archlinux.org/packages/v/vagrant-substrate/vagrant-substrate-526.6bb2e80-1-x86_64.pkg.tar.xz  (from ALA)
12: vagrant-substrate-524.64c526https://archive.archlinux.org/packages/v/vagrant-substrate/vagrant-substrate-524.64c5261-2-x86_64.pkg.tar.xz  (from ALA)
13: vagrant-substrate-524.64c526https://archive.archlinux.org/packages/v/vagrant-substrate/vagrant-substrate-524.64c5261-1-x86_64.pkg.tar.xz  (from ALA)
>> Please enter package number, [q] to quit 

downgraderを使うとこんな感じにダウングレードできるバージョンを教えてくれるので、左の番号を入力してダウングレードを行います。

今回は599.d7cedfe-2になって欲しいので、3を入力してエンターを押すとダウングレードが始まります。

問題2: mime-typesがバージョンコンフリクトを起こす

$ vagrant plugin install vagrant-azure                                                        
Installing the 'vagrant-azure' plugin. This can take a few minutes...                                                    
/opt/vagrant/embedded/lib/ruby/2.2.0/rubygems/specification.rb:2100:in `check_version_conflict': can't activate mime-types-2.99.3, already activated mime-types-3.1 (Gem::LoadError)

こんなエラーが出ます。

Arch以外の環境の人も同じ問題にハマっているようです。

Plugin fails to install with vagrant 1.9.1 · Issue #150 · Azure/vagrant-azure · GitHub

この問題は、プラグインのバージョンを指定したら回避できました。

vagrant plugin install vagrant-azure --plugin-version '2.0.0.pre8'

おわり

とりあえずこれで問題はすべて回避できたはずです。

明日からようやくvagrantでAzureに仮想マシン建てる作業に入れそうです……

gnu-efiを使ってAARCH64/ARM64のUEFIサンプルアプリを動かしてみる

UEFIアプリを作るためのツールキットとしてはEDK2とgnu-efiの2つが有名ですが、後者のgnu-efiについて書いているところが少なかった気がしたので、サンプルビルドの方法とQEMU上で実行する手順について書いておくことにしました。

対象はx86_64ではなく、AARCH64(ARM64)です。クロスコンパイラを用意してから読んでください。

gnu-efiのビルド

ダウンロードする

以下からgnu-efiをもらってきます。

gnu-efi - Browse Files at SourceForge.net

バージョンは3.0.5が最新だったので、そちらをダウンロードしました。

解凍

tar xf gnu-efi-3.0.5.tar.bz2

ビルドする

cd gnu-efi-3.0.5

と移動して、CROSS_COMPILE変数を使ってクロスコンパイラの名前を教えてあげるとビルドができます。

make CROSS_COMPILE=aarch64-linux-gnu-

このとき、gccのバージョンが新しい(ver 7.x)の場合は、gnu-efiのコードのswitchにfallthrough(わざとbreakを挟まずに次の条件の処理を実行すること)しているところがあるので、次のようなエラーが出ます。

aarch64-linux-gnu-gcc -I/home/tnishinaga/gnu-efi-3.0.5/lib -I/home/tnishinaga/gnu-efi-3.0.5/lib/../inc -I/home/tnishinaga/gnu-efi-3.0.5/lib/../inc/aarch64 -I/home/tnishinaga/gnu-efi-3.0.5/lib/../inc/protocol -fpic  -g -O2 -Wall -Wextra -Werror -fshort-wchar -fno-strict-aliasing -fno-merge-constants -ffreestanding -fno-stack-protector -fno-stack-check -DCONFIG_aarch64 -c /home/tnishinaga/gnu-efi-3.0.5/lib/print.c -o print.o
/home/tnishinaga/gnu-efi-3.0.5/lib/print.c: In function '_Print':
/home/tnishinaga/gnu-efi-3.0.5/lib/print.c:1133:26: error: this statement may fall through [-Werror=implicit-fallthrough=]
                 Item.Pad = '0';
                 ~~~~~~~~~^~~~~
/home/tnishinaga/gnu-efi-3.0.5/lib/print.c:1134:13: note: here
             case 'x':
             ^~~~
cc1: all warnings being treated as errors

問題の詳細は以下を参照してください。

-Wimplicit-fallthrough in GCC 7 – RHD Blog

このエラーが起こった人は、makeの際に CFLAGS=-Werror=implicit-fallthrough=0を付けると、エラーを回避できます。

make CROSS_COMPILE=aarch64-linux-gnu- CFLAGS=-Werror=implicit-fallthrough=0

サンプルをビルドする

サンプルは以下のコマンドでビルドできます。

make CROSS_COMPILE=aarch64-linux-gnu- CFLAGS=-Werror=implicit-fallthrough=0 apps

ビルドさせたファイルは、gnu-efiディレクトリの下にある aarch64/apps に置かれています。

サンプルを動かしてみる

QEMUを使ってサンプルのUEFIアプリを起動してみましょう。

準備

OVMFの取得などの準備は、以下の記事を参照して行ってください。

tnishinaga.hatenablog.com

QEMUを起動する

以下のコマンドを実行すると、aarch64/apps以下が仮想マシンのドライブとしてマウントされた状態でUEFIシェルが起動します。

qemu-system-aarch64 -m 128 -cpu cortex-a57 -M virt -bios ../QEMU_EFI.fd -serial stdio -hda fat:rw:aarch64/apps

なお、ここで以下のエラーが出た人は、-hda fataarch64/appsの間に :rw: が正しく挟めているかを確認してください。

$ qemu-system-aarch64 -m 128 -cpu cortex-a57 -M virt -bios ../QEMU_EFI.fd -serial stdio -hda fat:aarch64/apps
vvfat aarch64/apps chs 1024,16,63
WARNING: Image format was not specified for 'json:{"fat-type": 0, "dir": "aarch64/apps", "driver": "vvfat", "floppy": false, "rw": false}' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
qemu-system-aarch64: -hda fat:aarch64/apps: Block node is read-only

サンプルを実行する

UEFIシェル上で以下のコマンド(fs0:の部分)を実行すると、サンプルアプリのあるデバイスに移動できます。

Shell> fs0:

ここで、以下の用にするとHelloWorldサンプルが実行され、画面にHelloWorldが表示されます。

FS0:\> t.efi
HHello World!

f:id:tnishinaga:20170620191619p:plain

とりあえず以上です。

これからgnu-efiで自分のアプリを作る方法を調べようと思います。