virt-managerでarmのゲストを動かすメモ

忘れないようにメモっとこうという程度なので大したことはしてないです。

aarch64やarmhfpのイメージはIndex of /pub/Linux/Fedora/releasesから取得したとします。

qemuのバージョン

ホストはx86_64なFedora 29で、qemuは3.0.0になります。

masami@saga:~/arm/aarch64$ rpm -qa  | grep qemu-system
qemu-system-arm-core-3.0.0-1.fc29.x86_64
qemu-system-x86-3.0.0-1.fc29.x86_64
qemu-system-x86-core-3.0.0-1.fc29.x86_64
qemu-system-aarch64-core-3.0.0-1.fc29.x86_64
qemu-system-arm-3.0.0-1.fc29.x86_64
qemu-system-aarch64-3.0.0-1.fc29.x86_64

あとこの辺も必要かと思います。

masami@saga:~/arm/aarch64$ rpm -qa | grep edk2
edk2-aarch64-20180815gitcb5f4f45ce-1.fc29.noarch
edk2-ovmf-20180815gitcb5f4f45ce-1.fc29.noarch

aarch64

イメージはxz形式で圧縮されているのでまずは展開します。

masami@saga:~/arm/aarch64$ xz -dc Fedora-Server-28-1.1.aarch64.raw.xz  > Fedora-Server-28-1.1.aarch64.raw

そして、カーネルやコマンド諸々込みのファイルができるのですが、これをそのまま使ってもログイン可能なユーザーもいないし、rootログインできないのでまずはrootのパスワードを設定します。ここでは virt-customizeというコマンドを使います。これはlibguestfs-toolsってパッケージをインストールすると使えます。

masami@saga:~/arm/aarch64$ virt-customize --root-password file:./rootpw.txt -a Fedora-Server-28-1.1.aarch64.raw
[   0.0] Examining the guest ...
[  16.3] Setting a random seed
[  16.4] Setting the machine ID in /etc/machine-id
[  16.4] Setting passwords
[  17.7] Finishing off

このコマンドのオプションが見たままですがrootのパスワードを設定しています。パスワードはrootpw.txtってファイルに書いてます。このファイルの1行目に平文でパスワードを記述します。このコマンドでユーザー作ったりも出来ます。 それはともかく、これでrootログインできるようになったので後はvirt-installでインストールします。

ディスクイメージを/var/lib/libvirt/imagesに置き、以下のような感じでインストールします。これが最低限なコマンドラインオプションかと思います。

masami@saga:~/arm/aarch64$ sudo virt-install --name fedora28-aarch64 --ram 4096 --arch aarch64 --import --os-variant fedora22 --disk /var/lib/libvirt/images/Fedora-Server-28-1.1.aarch64.raw --boot uefi

armhfp版との違いとして--bootでufeiを指定してるというのがあります。armhfpの場合だとvmlinuzとかinitrdの指定があります。

あとはしばらく待っていればログインプロンプトが表示されるのでidをroot、パスワードは先程設定したものを入力すればログインできます(∩´∀`)∩ワーイ

[  OK  ] Started Update UTMP about System Runlevel Changes.

Fedora 28 (Server Edition)
Kernel 4.16.3-301.fc28.aarch64 on an aarch64 (ttyAMA0)

Admin Console: https://192.168.122.223:9090/ or https://[fe80::27d2:fdd6:dc02:4ab]:9090/

localhost login: root
Password:
[root@localhost ~]# uname -a
Linux localhost.localdomain 4.16.3-301.fc28.aarch64 #1 SMP Mon Apr 23 21:45:59 UTC 2018 aarch64 aarch64 aarch64 GNU/Linux  

また、サーバ版はCockpitが動いているのでAdmin Console: ~と書かれているURLにブラウザでアクセスすればブラウザからでも端末エミュレータが使えます。

f:id:masami256:20181106220907p:plain

armhfp

aarch64とも多少違うのでこちらもメモしておきます。基本的にはArchitectures/ARM/F28/Installation - Fedora Project Wikiの手順に沿えば良いのですが、qemuのバージョンが3.0.0の場合(今後はわかりません)はwikiの手順だけだとBug 1633328 – Armv7 guest fails to boot with qemu-3.0.0-1に書かれているように以下のようなワーニングが出てインストール完了しません。

[   51.049803] audit: type=1130 audit(1541508483.457:11): pid=1 uid=0 auid=4294967295 ses=4294967295 subj=kernel msg='unit=systemd-tmpfiles-setup comm="systemd" exe="/usr/lib/systemd/systemd" hostname=? addr=? terminal=? res=success'
[  OK  ] Reached target System Initialization.
[  OK  ] Reached target Basic System.
[  371.409670] dracut-initqueue[338]: Warning: dracut-initqueue timeout - starting timeout scripts
[  373.553982] dracut-initqueue[338]: Warning: dracut-initqueue timeout - starting timeout scripts
[  374.886815] dracut-initqueue[338]: Warning: dracut-initqueue timeout - starting timeout scripts
[  376.308904] dracut-initqueue[338]: Warning: dracut-initqueue timeout - starting timeout scripts
[  377.638877] dracut-initqueue[338]: Warning: dracut-initqueue timeout - starting timeout scripts

そのため、commentの14に書かれているようにvirt-install--qemu-commandline='-machine highmem=off というオプションを追加します。よって、virt-installコマンドラインはこんなふうになります。

sudo virt-install --name fedora28-armhfp --ram 1024 --arch armv7l \
  --import --os-variant fedora22 \
  --disk /var/lib/libvirt/images/Fedora-Minimal-armhfp-28-1.1-sda.raw \
  --boot kernel=/var/lib/libvirt/images/vmlinuz-4.16.3-301.fc28.armv7hl,initrd=/var/lib/libvirt/images/initramfs-4.16.3-301.fc28.armv7hl.img,kernel_args="console=ttyAMA0 rw root=LABEL=_/ rootwait" --qemu-commandline='-machine highmem=off'

こちらもvirt-installの前にvirt-customizeでrootのパスワードを設定しておく必要があります。

ゲストへの接続

普通にvirsh console ゲスト名で接続できます。

その他

armhfpのほうはhighmem=offなどの設定が必要なのが注意事項ですね ( ´ー`)フゥー...

ARMで学ぶ アセンブリ言語入門

ARMで学ぶ アセンブリ言語入門

When kallsyms doesn't show addresses even though kptr_restrict is 0

When I did grep kernel symbol address, addresses were all zero.

masami@saga:~$ grep slab_alloc /proc/kallsyms 
0000000000000000 t ___slab_alloc
0000000000000000 t __slab_alloc

So, I checked kptr_restrict if it's not zero.

masami@saga:~$ sudo sysctl kernel.kptr_restrict
kernel.kptr_restrict = 0

Um, kptr_restrict was already zero. By the way, how about kernel config that enables KALLSYMS ? Current kernel version is here.

masami@saga:~$ uname -a
Linux saga 4.18.13-300.fc29.x86_64 #1 SMP Wed Oct 10 17:22:50 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

So, in /boot there is kernel configs that I could check it.

masami@saga:~$ grep -i kallsyms /boot/config-4.18.13-300.fc29.x86_64
CONFIG_KALLSYMS=y
CONFIG_KALLSYMS_ALL=y
CONFIG_KALLSYMS_ABSOLUTE_PERCPU=y
CONFIG_KALLSYMS_BASE_RELATIVE=y

Ok, KALLSYMS is enabled. But how come I couldn't get addresses? Let's check kernel functions.

When you open /proc/kallsyms, kallsyms_show_value() is called. In my case, kptr_restrict is 0, so kallsyms_for_perf() is called.

int kallsyms_show_value(void)
{
    switch (kptr_restrict) {
    case 0:
        if (kallsyms_for_perf())
            return 1;
    /* fallthrough */
    case 1:
        if (has_capability_noaudit(current, CAP_SYSLOG))
            return 1;
    /* fallthrough */
    default:
        return 0;
    }
}

kallsyms_for_perf() is following simple function.

static inline int kallsyms_for_perf(void)
{
#ifdef CONFIG_PERF_EVENTS
    extern int sysctl_perf_event_paranoid;
    if (sysctl_perf_event_paranoid <= 1)
        return 1;
#endif
    return 0;
}

As you can see, thee is #ifdef macro. so, check CONFIG_PERF_EVENTS in kernel config.

masami@saga:~$ grep CONFIG_PERF_EVENTS /boot/config-4.18.13-300.fc29.x86_64
CONFIG_PERF_EVENTS=y
CONFIG_PERF_EVENTS_INTEL_UNCORE=m
CONFIG_PERF_EVENTS_INTEL_RAPL=m
CONFIG_PERF_EVENTS_INTEL_CSTATE=m
CONFIG_PERF_EVENTS_AMD_POWER=m

Ok, if perf_event_paranoid is less than or equal to 1, it returns 1, otherwise return 0. Let's check perf_event_paranoid.

masami@saga:~$ sudo sysctl kernel.perf_event_paranoid
kernel.perf_event_paranoid = 2

Ah, I got it. However, what value can I use for? According to Documentation/sysctl/kernel.txt, there is a paragraph that describes perf_event_paranoid values. Maybe 1 is okay for me now.

masami@saga:~$ sudo sysctl -w kernel.perf_event_paranoid=1
kernel.perf_event_paranoid = 1

Then check symbols again.

masami@saga:~$ grep slab_alloc /proc/kallsyms 
ffffffff96272150 t ___slab_alloc
ffffffff96272790 t __slab_alloc

got it :)

Understanding the Linux Kernel: From I/O Ports to Process Management

Understanding the Linux Kernel: From I/O Ports to Process Management

fedoraでdockerを使ってaarch64のクロスビルド環境を作る

qemuのstaticなバイナリとbinfmt_miscを使ってdockerでクロスビルド環境を作ってみます。

今回作ったものはこちらに置きました。

github.com

環境

HostはFedora 29で、arm64v8/fedoraにあるaarch 64のFedora 28(2018/10/11時点でのlatestはfedora 28です)。このイメージはオフィシャルなものです。

qemu

qemuのパッケージで必要なのは qemu-user-static です。これをサクッとインストールしましょう。

binfmt_misc

つぎにbinfmt_miscの設定が必要なんですが、fedoraの場合systemdのproc-sys-fs-binfmt_misc.mountが動いてればOKです。自分の場合はインストールした後に所要で再起動したのですが、restartとかすれば良い気がします。

masami@saga:~$ systemctl status proc-sys-fs-binfmt_misc.mount
● proc-sys-fs-binfmt_misc.mount - Arbitrary Executable File Formats File System
   Loaded: loaded (/usr/lib/systemd/system/proc-sys-fs-binfmt_misc.mount; static; vendor preset: disabled)
   Active: active (mounted) since Thu 2018-10-11 20:07:16 JST; 2h 2min ago
    Where: /proc/sys/fs/binfmt_misc
     What: binfmt_misc
     Docs: https://www.kernel.org/doc/html/latest/admin-guide/binfmt-misc.html
           https://www.freedesktop.org/wiki/Software/systemd/APIFileSystems
    Tasks: 0 (limit: 4915)
   Memory: 60.0K
   CGroup: /system.slice/proc-sys-fs-binfmt_misc.mount

Oct 11 20:07:16 saga systemd[1]: Mounting Arbitrary Executable File Formats File System...
Oct 11 20:07:16 saga systemd[1]: Mounted Arbitrary Executable File Formats File System.

/proc/sys/fs/binfmt_miscを手動で設定するときはELFバイナリのシグネチャとかを自分で設定しないといけないんですが、proc-sys-fs-binfmt_misc.mountが上手いことやってくれてるようです。/proc/sys/fs/binfmt_misc/が↓のようになっていれば設定完了です。

masami@saga:~$ ls /proc/sys/fs/binfmt_misc/
./            qemu-aarch64_be  qemu-armeb  qemu-microblaze    qemu-mips64    qemu-mipsn32    qemu-ppc      qemu-riscv32  qemu-sh4          qemu-xtensa    status
../           qemu-alpha       qemu-hppa   qemu-microblazeel  qemu-mips64el  qemu-mipsn32el  qemu-ppc64    qemu-riscv64  qemu-sh4eb        qemu-xtensaeb
qemu-aarch64  qemu-arm         qemu-m68k   qemu-mips          qemu-mipsel    qemu-or1k       qemu-ppc64le  qemu-s390x    qemu-sparc32plus  register

docker image

arm64v8/fedoraのイメージを使います。

試しに動作確認をしてたのが以下になります。unameコマンドで表示されるアーキテクチャがaarch64になってますね。

masami@saga:~$ sudo docker pull arm64v8/fedora
Using default tag: latest
Trying to pull repository docker.io/arm64v8/fedora ... 
sha256:a2adc1621935869bcd51a84b007b3ee6aa8aa9aef0bf30a947f7312fd43e5b0b: Pulling from docker.io/arm64v8/fedora
f9afc51fb922: Pull complete 
Digest: sha256:a2adc1621935869bcd51a84b007b3ee6aa8aa9aef0bf30a947f7312fd43e5b0b
Status: Downloaded newer image for docker.io/arm64v8/fedora:latest
masami@saga:~$ sudo docker image ls
REPOSITORY                 TAG                 IMAGE ID            CREATED             SIZE
docker.io/arm64v8/fedora   latest              95cddd448075        4 weeks ago         290 MB
runtimejstest_web          latest              5cf57ccadd2f        4 weeks ago         675 MB
docker.io/node             8                   8198006b2b57        5 weeks ago         673 MB
docker.io/amazonlinux      latest              585cc50169e6        3 months ago        163 MB
alpine                     latest              3fd9065eaf02        9 months ago        4.15 MB
masami@saga:~$ sudo docker run -it docker.io/arm64v8/fedora /bin/bash
[root@54034b092275 /]# uname -a
Linux 54034b092275 4.19.0-rc7-test+ #102 SMP Mon Oct 8 11:06:09 JST 2018 aarch64 aarch64 aarch64 GNU/Linux

ホストでunameコマンド使ったらx86_64となります。

masami@saga:~$ uname -a
Linux saga 4.19.0-rc7-test+ #102 SMP Mon Oct 8 11:06:09 JST 2018 x86_64 x86_64 x86_64 GNU/Linux

クロスビルド環境の作成

では、本題のクロスビルド環境を作ってみます。今回はソースはホストで編集し、docker-compose upでmakeとmake testを動かすようにします。ソースはDockerfileが有るディレクトリにsrc/というディレクトリを作ってそこに置きましょう。

Dockerfileはこうなります。とりあえずCMDでmakeしないでbashを起動させるだけにします。

FROM docker.io/arm64v8/fedora

RUN mkdir /src && \
dnf install -y gcc make file

WORKDIR /src

CMD /bin/bash

docker-compose.ymlはこうなります。selinuxが有効なのでvolumesの時にzオプションを付けています。commandのところでCMDを上書きしてmakeコマンドを実行するようにしました。

version: '3'

services:
  builder:
    build: .
    image: aarch64-cross-builder
    hostname: fedora-aarch64
    volumes:
      - ./src:/src:z
    command: make all

実行

masami@saga:~/codes/arm-cross-build$ sudo docker-compose up
Creating arm-cross-build_builder_1 ... done
Attaching to arm-cross-build_builder_1
builder_1  | rm -f hello
builder_1  | cc hello.c -o hello
builder_1  | file ./hello
builder_1  | ./hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=39cbef1ba26dcae61413b891b9bd4e9d202a5083, not stripped
builder_1  | ./hello
builder_1  | Hello, World
arm-cross-build_builder_1 exited with code 0

まとめ

Fedora 29ならかなり簡単にクロスビルド環境を作ることが出来ますね( ´∀`)bグッ! qemuで完全なエミュレーションをしてももちろんOKですが、ユーザーランドのアプリならdockerでも良さげですね。

( ´ー`)フゥー...

ARMで学ぶ アセンブリ言語入門

ARMで学ぶ アセンブリ言語入門

linux: seq_fileの使い方めも

sysfs、debugfs、procfs等でファイルを作ってユーザーランドからreadするときにseq_file構造体を使う方法が有るけど使ったことなかったのでめもです。

コード

こんな感じです。

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/debugfs.h>
#include <linux/seq_file.h>

MODULE_DESCRIPTION("seq file test");
MODULE_AUTHOR("masami256");
MODULE_LICENSE("GPL");

#define SEQ_FILE_TEST_DATA_ARRAY_SIZE 8

struct seq_file_test_data {
    char s[16];
};

static struct seq_file_test_data data_array[SEQ_FILE_TEST_DATA_ARRAY_SIZE];

static struct dentry *seq_file_test_file;

static void *seq_file_get_next_pos(loff_t *pos)
{
    if (*pos > SEQ_FILE_TEST_DATA_ARRAY_SIZE)
        return NULL;

    return data_array + *pos;
}

static void *seq_file_test_start(struct seq_file *m, loff_t *pos)
{
    return seq_file_get_next_pos(pos);
}

static void seq_file_test_stop(struct seq_file *m, void *v)
{
}

static void *seq_file_test_next(struct seq_file *m, void *v, loff_t *pos)
{
    (*pos)++;

    return seq_file_get_next_pos(pos);
}

static int seq_file_test_show(struct seq_file *m, void *v)
{
    struct seq_file_test_data *p = v;

    seq_printf(m, "%s", p->s);
    return 0;
}

static struct seq_operations seq_file_test_seq_ops = {
    .start = seq_file_test_start,
    .stop = seq_file_test_stop,
    .next = seq_file_test_next,
    .show = seq_file_test_show,
};

static int seq_file_test_open(struct inode *inode, struct file *file)
{
    return seq_open(file, &seq_file_test_seq_ops);
}

static struct file_operations seq_file_test_fops = {
    .owner = THIS_MODULE,
    .open = seq_file_test_open,
    .read = seq_read,
    .llseek = seq_lseek,
    .release = seq_release,
};

static int seq_file_test_init(void)
{
    int i;
    pr_info("%s start\n", __func__);

    for (i = 0; i < SEQ_FILE_TEST_DATA_ARRAY_SIZE; i++) {
        struct seq_file_test_data *p = &data_array[i];
        sprintf(p->s, "test data %d\n", i);
    }

    seq_file_test_file = debugfs_create_file("seq_file_test", 0444,
                        NULL, NULL,
                        &seq_file_test_fops);

    if (IS_ERR(seq_file_test_file))
        return PTR_ERR(seq_file_test_file);

    return 0;
}

static void seq_file_test_exit(void)
{
    pr_info("%s bye\n", __func__);
    debugfs_remove(seq_file_test_file);
}

module_init(seq_file_test_init);
module_exit(seq_file_test_exit);

実行例

masami@kerntest:~/seq_file_test$ sudo cat /sys/kernel/debug/seq_file_test
test data 0
test data 1
test data 2
test data 3
test data 4
test data 5
test data 6
test data 7
masami@kerntest:~/seq_file_test$

動作の概要

seq_file構造体はinclude/linux/seq_file.hでこんな感じで定義されてます。

struct seq_operations {
    void * (*start) (struct seq_file *m, loff_t *pos);
    void (*stop) (struct seq_file *m, void *v);
    void * (*next) (struct seq_file *m, void *v, loff_t *pos);
    int (*show) (struct seq_file *m, void *v);
};

show()がデータを表示するための関数です。start()がファイル読み込み時に呼ばれて、データがあればデータのポインタを返します。続いてnext()が呼ばれていってNULLが返るまでは処理が続く感じです。 show()のほうは引数のvにstart()とかnext()で返したポインタが渡ってきます。なのでseq_file_test_show()ではseq_printf()で一つのデータの内容を表示させてるだけなのにcatはちゃんとすべてのデータが読めるってことですね。 ファイルを作成したときはfile_operations構造体でファイルのopen、readなどの関数を設定しますがseq_file構造体を使ってreadの処理を行う場合はopen()を行う関数の方でseq_open()を呼びます。seq_open()に渡すのはfile_operations構造体のopen()に渡されるfile構造体と、seq_file用のseq_operations構造体です。

分かってみればそんなに難しいことはないのでこれからは利用していきたいですね( ´∀`)bグッ!

systemtapでlivepatchする

CVE-2018-14634(Integer overflow in Linux's create_elf_tables function)でRed Hatのbugzillaを見ていて軽減策としてsystemtapでlivepatch的なことをしていて面白いな〜って思ったのでめもです。

環境はFedora 28で、カーネルのバージョンは4.18.9-200です。systemtapを使っているのでkernel-debuginfoパッケージも入れてます。

utsname構造体のnodenameを書き換えてホスト名を変えてみたいと思います。まずは初期状態はこうです。kerntestがnodenameになります。

masami@kerntest:~$ uname -a                                                                                                                                                                                        
Linux kerntest 4.18.9-200.fc28.x86_64 #1 SMP Thu Sep 20 02:43:23 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

cのコードでuname(2)を実行するものを作ります。

#include <stdio.h>
#include <sys/utsname.h>

int main(int argc, char **argv)
{
        struct utsname utsname = { 0 };

        printf("call uname(2) to execute livepatch via systemtap\n");

        if (uname(&utsname)) {
                perror("uname");
                return -1;
        }

        printf("patched nodename: %s\n", utsname.nodename);
        return 0;
}

まずはこれを-oオプション無しにコンパイルしてa.outで実行した場合。

masami@kerntest:~$ ./a.out
call uname(2) to execute livepatch via systemtap
patched nodename: kerntest

なにも起きませんね。

次に以下のsystemtapスクリプトを実行します。 uname(2)で呼ばれる__do_sys_newuname()にprobeを設定しています。 このスクリプトの内容としてはuname(2)を実行したプロセス名がdo-livepatchならそのプロセスが所属するuts名前空間に設定されてるnodenameの値を書き換えてます。

#!/usr/bin/env stap


%{
#include <linux/sched.h>
#include <linux/string.h>
%}

function set_dummy_uname:long()
%{
        struct task_struct *p = current;
        if (strcmp(p->nsproxy->uts_ns->name.nodename, "livepatched")) {
                if (unlikely(!strcmp(p->comm, "do-livepatch"))) {
                        memset(p->nsproxy->uts_ns->name.nodename, 0, sizeof(p->nsproxy->uts_ns->name.nodename));
                        strcpy(p->nsproxy->uts_ns->name.nodename, "livepatched");
                }
        }

        STAP_RETURN(0);

%}

probe begin {
        printf("start\n")
}


probe kernel.function("__do_sys_newuname") {
        set_dummy_uname()
}

probe end {
        printf("Done\n")
}

上記のコードを実行します。

masami@kerntest:~$ sudo stap -v -g test.stp                                                                                                                                                                       
Pass 1: parsed user script and 474 library scripts using 157272virt/50300res/8740shr/41432data kb, in 90usr/20sys/106real ms.                                                                                     
Pass 2: analyzed script: 3 probes, 1 function, 1 embed, 0 globals using 211816virt/105600res/9628shr/95976data kb, in 750usr/30sys/777real ms.                                                                    
Pass 3: translated to C into "/tmp/stapmurOiN/stap_134d2448e08da1a0409648297476fac2_1549_src.c" using 211816virt/105912res/9940shr/95976data kb, in 10usr/0sys/6real ms.                                          
Pass 4: compiled C into "stap_134d2448e08da1a0409648297476fac2_1549.ko" in 1990usr/740sys/2506real ms.
Pass 5: starting run.
start

そして、cコードの方をdo-livepatchという名前のバイナリでコンパイルして実行するとnodenameが書き換えられます。これ以降はuname(2)は変更した名前が返るようになってます。

masami@kerntest:~$ ./do-livepatch 
call uname(2) to execute livepatch via systemtap
patched nodename: livepatched
masami@kerntest:~$ uname -a
Linux livepatched 4.18.9-200.fc28.x86_64 #1 SMP Thu Sep 20 02:43:23 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
masami@kerntest:~$ uname -n
livepatched

今まではすでにログインしてたのでbashプロンプトのほうはホスト名変わってませんが、新規に接続するとホスト名が新しい名前になってます。

masami@saga:~$ ssh kerntest 
Last login: Thu Sep 27 02:05:41 2018 from 192.168.122.1
masami@livepatched:~$ 

というわけで、systemtapでlivepatch的なことをする実験でした( ´ー`)フゥー...

unikernelのruntimejsを試してみる

runtime.jsを試してみたのでメモ書きです。

f:id:masami256:20180909150234p:plain

runtme.jsはunikernelの一つでカーネルにv8のエンジンが組み込まれてて、JavaScriptの実行がサポートされています。runtime.jsのサンプルで、eshttpを使ったwebアプリのサンプルが有ったのでこれを使って動かしてみます。 今回使ったjsのコードやDockerfileなどは↓にあります。

github.com

実行するコードは以下の内容でnodeコマンドでそのまま動かすことができるようになってます。

const eshttp = require('eshttp');
const server = new eshttp.HttpServer();
const response = new eshttp.HttpResponse(200, { 'server': 'runtimejs' }, 'Hello World!');

server.onrequest = request => {
  request.respondWith(response);
};

server.listen(9000);
console.log('listening to port 9000');

今回の計測はやり方として良くはないんだけど、手頃な環境がなかったのですべて1つのマシン上で行ってます。ベンチマークの実行にはabコマンドを使ってます。 hostマシンはFedora28です。

実行方法は以下の3パターンを試しました。

  1. ホストローカルで実行
  2. runteime.jsで実行
  3. Fedora 28の仮想マシン上のdockerで実行

まずはdockerもruntime.jsも使わずにローカルでindex.jsを実行した場合。

Test 1: Requests per second:    3031.68 [#/sec] (mean)
Test 2: Requests per second:    4135.48 [#/sec] (mean)
Test 3: Requests per second:    4542.56 [#/sec] (mean)
Test 4: Requests per second:    3834.94 [#/sec] (mean)
Test 5: Requests per second:    4983.80 [#/sec] (mean)
Test 6: Requests per second:    4654.19 [#/sec] (mean)
Test 7: Requests per second:    4711.87 [#/sec] (mean)
Test 8: Requests per second:    5130.57 [#/sec] (mean)
Test 9: Requests per second:    5187.80 [#/sec] (mean)
Test 10: Requests per second:    5351.89 [#/sec] (mean)

runtime.jsで実行した場合。

Test 1: Requests per second:    2577.45 [#/sec] (mean)
Test 2: Requests per second:    2562.92 [#/sec] (mean)
Test 3: Requests per second:    2494.01 [#/sec] (mean)
Test 4: Requests per second:    2558.20 [#/sec] (mean)
Test 5: Requests per second:    2802.85 [#/sec] (mean)
Test 6: Requests per second:    2705.70 [#/sec] (mean)
Test 7: Requests per second:    2850.22 [#/sec] (mean)
Test 8: Requests per second:    2901.33 [#/sec] (mean)
Test 9: Requests per second:    2505.95 [#/sec] (mean)
Test 10: Requests per second:    2331.00 [#/sec] (mean)

fedora 28の仮想環境の上でdockerで実行した場合。

Test 1: Requests per second:    2086.77 [#/sec] (mean)
Test 2: Requests per second:    3054.37 [#/sec] (mean)
Test 3: Requests per second:    3071.91 [#/sec] (mean)
Test 4: Requests per second:    2478.68 [#/sec] (mean)
Test 5: Requests per second:    2888.59 [#/sec] (mean)
Test 6: Requests per second:    2938.15 [#/sec] (mean)
Test 7: Requests per second:    3135.09 [#/sec] (mean)
Test 8: Requests per second:    3611.41 [#/sec] (mean)
Test 9: Requests per second:    3088.80 [#/sec] (mean)
Test 10: Requests per second:    3299.46 [#/sec] (mean)

この結果だとdockerのほうが良い感じの結果になってます。qemuの実行方法やvm環境で動くカーネルも違うので一概にはどっちが良いとか言えない面もありすね。

runtime.jsのqemuコマンドライン

masami   19666 28.3  0.2 1110156 139224 pts/1  Sl+  14:20   0:01 qemu-system-x86_64 -m 512 -smp 1 -s -kernel /home/masami/.runtime/runtime.2064 -initrd .initrd -net nic,model=virtio,macaddr=1a:46:0b:ca:bc:7c -net user,net=192.168.76.0/24,dhcpstart=192.168.76.9,hostfwd=udp::9000-:9000,hostfwd=tcp::9000-:9000 -nographic -monitor none -enable-kvm -no-kvm-irqchip -serial stdio

virt-managerのほうのqemuコマンドライン

qemu     11627  0.9  1.4 10844960 974920 ?     Sl   12:43   0:52 /usr/bin/qemu-system-x86_64 -machine accel=kvm -name guest=fedora-labo,debug-threads=on -S -object secret,id=masterKey0,format=raw,file=/var/lib/libvirt/qemu/domain-1-fedora-labo/master-key.aes -machine pc-i440fx-2.7,accel=kvm,usb=off,vmport=off,dump-guest-core=off -cpu Haswell-noTSX-IBRS,vme=on,ss=on,vmx=on,f16c=on,rdrand=on,hypervisor=on,arat=on,tsc_adjust=on,ssbd=on,xsaveopt=on,pdpe1gb=on,abm=on -m 8192 -realtime mlock=off -smp 1,sockets=1,cores=1,threads=1 -uuid 239af4a6-ef28-4fa8-aacb-13b2588b0c21 -no-user-config -nodefaults -chardev socket,id=charmonitor,path=/var/lib/libvirt/qemu/domain-1-fedora-labo/monitor.sock,server,nowait -mon chardev=charmonitor,id=monitor,mode=control -rtc base=utc,driftfix=slew -global kvm-pit.lost_tick_policy=delay -no-hpet -no-shutdown -global PIIX4_PM.disable_s3=1 -global PIIX4_PM.disable_s4=1 -boot strict=on -device ich9-usb-ehci1,id=usb,bus=pci.0,addr=0x6.0x7 -device ich9-usb-uhci1,masterbus=usb.0,firstport=0,bus=pci.0,multifunction=on,addr=0x6 -device ich9-usb-uhci2,masterbus=usb.0,firstport=2,bus=pci.0,addr=0x6.0x1 -device ich9-usb-uhci3,masterbus=usb.0,firstport=4,bus=pci.0,addr=0x6.0x2 -device virtio-serial-pci,id=virtio-serial0,bus=pci.0,addr=0x5 -drive file=/var/lib/libvirt/images/fedora-labo.qcow2,format=qcow2,if=none,id=drive-virtio-disk0 -device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x7,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=1 -drive if=none,id=drive-ide0-0-0,readonly=on -device ide-cd,bus=ide.0,unit=0,drive=drive-ide0-0-0,id=ide0-0-0 -netdev tap,fd=25,id=hostnet0,vhost=on,vhostfd=27 -device virtio-net-pci,netdev=hostnet0,id=net0,mac=52:54:00:95:01:e7,bus=pci.0,addr=0x3 -chardev pty,id=charserial0 -device isa-serial,chardev=charserial0,id=serial0 -chardev socket,id=charchannel0,path=/var/lib/libvirt/qemu/channel/target/domain-1-fedora-labo/org.qemu.guest_agent.0,server,nowait -device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=org.qemu.guest_agent.0 -chardev spicevmc,id=charchannel1,name=vdagent -device virtserialport,bus=virtio-serial0.0,nr=2,chardev=charchannel1,id=channel1,name=com.redhat.spice.0 -device usb-tablet,id=input0,bus=usb.0,port=1 -spice port=5900,addr=127.0.0.1,disable-ticketing,image-compression=off,seamless-migration=on -device qxl-vga,id=video0,ram_size=67108864,vram_size=67108864,vram64_size_mb=0,vgamem_mb=16,max_outputs=1,bus=pci.0,addr=0x2 -device intel-hda,id=sound0,bus=pci.0,addr=0x4 -device hda-duplex,id=sound0-codec0,bus=sound0.0,cad=0 -chardev spicevmc,id=charredir0,name=usbredir -device usb-redir,chardev=charredir0,id=redir0,bus=usb.0,port=2 -chardev spicevmc,id=charredir1,name=usbredir -device usb-redir,chardev=charredir1,id=redir1,bus=usb.0,port=3 -device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x8 -msg timestamp=on

runtime.jsのほうはqemuのオプションを調整することで結果がよくなるかも?

以下はめも runtime.jsは--kvmオプションを使わないとkvmが有効になりません。--nographicオプションを付けるとqemuGUIは表示しません。

--kernelでqemuに渡すカーネルを渡せるようだけど、initrdも作る必要ありそう。

( ´ー`)フゥー...

Docker/Kubernetes 実践コンテナ開発入門

Docker/Kubernetes 実践コンテナ開発入門

EPTの設定をbhyveで調べる

KVMよりもEPTの使用前提なbhyveのほうがコード読みやすかったのです。Nested Paging in bhyveというFreeBSDのメモリ管理からbhyveでのEPT周りの実装を解説した論文があったのも理由としては大きいですね。

EPTPの設定

vmx_vminit()という関数でeptpの設定をする関数を呼びます。

  814 static void *
  815 vmx_vminit(struct vm *vm, pmap_t pmap)
  816 {
  817         uint16_t vpid[VM_MAXCPU];
  ~ 略 ~
  827         }
  828         vmx->vm = vm;
  829 
  830         vmx->eptp = eptp(vtophys((vm_offset_t)pmap->pm_pml4));

pmap_tはpmap構造体で、PML4のアドレスを保持している(LinuxだとPGD)。pml4_entry_tはu_int64_tの別名(typedef)。

  309 /*
  310  * The kernel virtual address (KVA) of the level 4 page table page is always
  311  * within the direct map (DMAP) region.
  312  */
  313 struct pmap {
  314         struct mtx              pm_mtx;
  315         pml4_entry_t            *pm_pml4;       /* KVA of level 4 page table */
  316         uint64_t                pm_cr3;
  317         TAILQ_HEAD(,pv_chunk)   pm_pvchunk;     /* list of mappings in pmap */
  318         cpuset_t                pm_active;      /* active on cpus */
  319         enum pmap_type          pm_type;        /* regular or nested tables */
  320         struct pmap_statistics  pm_stats;       /* pmap statistics */
  321         struct vm_radix         pm_root;        /* spare page table pages */
  322         long                    pm_eptgen;      /* EPT pmap generation id */
  323         int                     pm_flags;
  324         struct pmap_pcids       pm_pcids[MAXCPU];
  325 };

eptp()はこのような処理。

  195 uint64_t
  196 eptp(uint64_t pml4)
  197 {
  198         uint64_t eptp_val;
  199 
  200         eptp_val = pml4 | (EPT_PWLEVELS - 1) << 3 | PAT_WRITE_BACK;
  201         if (ept_enable_ad_bits)
  202                 eptp_val |= EPT_ENABLE_AD_BITS;
  203 
  204         return (eptp_val);
  205 }

この関数はExtended-Page-Table Pointer (EPTP)の設定をしています。EPTPはIntel SDM Vol3の24.6.11 Extended-Page-Table Pointer (EPTP)に説明があります。

まず200行目を見ます。EPT_PWLEVELSは4で、PAT_WRITE_BACKは0x6です。そうすると、以下のようなbit列になります。

>>> bin(3 << 3 | 0x6)
'0b11110'

EPTPのbit2:0は0もしくは6を設定する仕様です。ここではPAT_WRITE_BACKで6を設定しています。次にbit5:3がEPT page-walk lengthとなっています。bit5:3は0b11なので3です。 202行目のEPT_ENABLE_AD_BITSは(1 << 6)です。1<<6は2進数で0b1000000なので6bit目を1にしてます。EPTPの6bit目はAccess/Dirty flagを有効にする設定です。EPTPのbit11:7は予約済みで、bit N-1:12はSDMには「Bits N–1:12 of the physical address of the 4-KByte aligned EPT PML4 table 3」とあります。Nは「N is the physical-address width supported by the logical processor.」とのことです。bit63:Nは予約済みです。なので、bit6:0までを設定するのがEPTPの設定ですね。

EPTの設定

vmx_init()からept_init()を呼びます。

ここでvmx_init()までをφ(..)メモメモ

初期化の関数はvmm_ops構造体のinit変数にvmx_init()を設定する。

 3423 struct vmm_ops vmm_ops_intel = {
 3424         vmx_init,

呼び出し方はVMM_INITマクロで定義されている。

  169 static struct vmm_ops *ops;
  170 #define VMM_INIT(num)   (ops != NULL ? (*ops->init)(num) : 0)

vmm_init()からvmx_init()を呼び出している。

  321 static int
  322 vmm_init(void)
  323 {
  324         int error;
  325 
  326         vmm_host_state_init();
  327 
  328         vmm_ipinum = lapic_ipi_alloc(&IDTVEC(justreturn));
  329         if (vmm_ipinum < 0)
  330                 vmm_ipinum = IPI_AST;
  331 
  332         error = vmm_mem_init();
  333         if (error)
  334                 return (error);
  335         
  336         if (vmm_is_intel())
  337                 ops = &vmm_ops_intel;
  338         else if (vmm_is_amd())
  339                 ops = &vmm_ops_amd;
  340         else
  341                 return (ENXIO);
  342 
  343         vmm_resume_p = vmm_resume;
  344 
  345         return (VMM_INIT(vmm_ipinum));
  346 }

で、本題に戻ってept_init()

   77 int
   78 ept_init(int ipinum)
   79 {
   80         int use_hw_ad_bits, use_superpages, use_exec_only;
   81         uint64_t cap;
   82 
   83         cap = rdmsr(MSR_VMX_EPT_VPID_CAP);
   84 
   85         /*
   86          * Verify that:
   87          * - page walk length is 4 steps
   88          * - extended page tables can be laid out in write-back memory
   89          * - invvpid instruction with all possible types is supported
   90          * - invept instruction with all possible types is supported
   91          */
   92         if (!EPT_PWL4(cap) ||
   93             !EPT_MEMORY_TYPE_WB(cap) ||
   94             !INVVPID_SUPPORTED(cap) ||
   95             !INVVPID_ALL_TYPES_SUPPORTED(cap) ||
   96             !INVEPT_SUPPORTED(cap) ||
   97             !INVEPT_ALL_TYPES_SUPPORTED(cap))
   98                 return (EINVAL);
   99 
  100         ept_pmap_flags = ipinum & PMAP_NESTED_IPIMASK;
  101 
  102         use_superpages = 1;
  103         TUNABLE_INT_FETCH("hw.vmm.ept.use_superpages", &use_superpages);
  104         if (use_superpages && EPT_PDE_SUPERPAGE(cap))
  105                 ept_pmap_flags |= PMAP_PDE_SUPERPAGE;   /* 2MB superpage */
  106 
  107         use_hw_ad_bits = 1;
  108         TUNABLE_INT_FETCH("hw.vmm.ept.use_hw_ad_bits", &use_hw_ad_bits);
  109         if (use_hw_ad_bits && AD_BITS_SUPPORTED(cap))
  110                 ept_enable_ad_bits = 1;
  111         else
  112                 ept_pmap_flags |= PMAP_EMULATE_AD_BITS;
  113 
  114         use_exec_only = 1;
  115         TUNABLE_INT_FETCH("hw.vmm.ept.use_exec_only", &use_exec_only);
  116         if (use_exec_only && EPT_SUPPORTS_EXEC_ONLY(cap))
  117                 ept_pmap_flags |= PMAP_SUPPORTS_EXEC_ONLY;
  118 
  119         return (0);
  120 }

最初にMSRからEPTをVPIDのケーパビリティを読み出します。これはSDM Vol3のA.10 VPID AND EPT CAPABILITIESに説明があります。そして、必要な機能が使えるかチェックしてます。あとはLinuxで言うところのsysctlで設定されたデータの読み出しと、フラグの設定ですね。

amd64/vmm/intel/ept.cにはもう一つ名前にinitが付く関数があります。それはept_pinit()です。この関数はept_vmspace_alloc()の処理から呼ばれます。

  181 struct vmspace *
  182 ept_vmspace_alloc(vm_offset_t min, vm_offset_t max)
  183 {
  184 
  185         return (vmspace_alloc(min, max, ept_pinit));
  186 }
  187 

vmspace_alloc()はメモリ管理サブシステムの関数です。3番目の引数にept_pinit()を渡すことで、初期化処理の関数としてept_pinit()を呼ぶようにしています。これは論文によるとVMMをサポートするためにこのような形になったようです。

ept_pinit()はこのような関数です。こちらもメモリ管理サブシステムのほうの関数を呼びます。

  174 static int
  175 ept_pinit(pmap_t pmap)
  176 {
  177 
  178         return (pmap_pinit_type(pmap, PT_EPT, ept_pmap_flags));
  179 }

pmap_pinit_type()はbyhveの追加時に新規に作られた関数とのことです。こちらもEPTのためですね。

 2407 /*
 2408  * Initialize a preallocated and zeroed pmap structure,
 2409  * such as one in a vmspace structure.
 2410  */
 2411 int
 2412 pmap_pinit_type(pmap_t pmap, enum pmap_type pm_type, int flags)
 2413 {
 2414         vm_page_t pml4pg;
 2415         vm_paddr_t pml4phys;
 2416         int i;
 2417 
 2418         /*
 2419          * allocate the page directory page
 2420          */
 2421         while ((pml4pg = vm_page_alloc(NULL, 0, VM_ALLOC_NORMAL |
 2422             VM_ALLOC_NOOBJ | VM_ALLOC_WIRED | VM_ALLOC_ZERO)) == NULL)
 2423                 VM_WAIT;
 2424 
 2425         pml4phys = VM_PAGE_TO_PHYS(pml4pg);
 2426         pmap->pm_pml4 = (pml4_entry_t *)PHYS_TO_DMAP(pml4phys);
 2427         CPU_FOREACH(i) {
 2428                 pmap->pm_pcids[i].pm_pcid = PMAP_PCID_NONE;
 2429                 pmap->pm_pcids[i].pm_gen = 0;
 2430         }
 2431         pmap->pm_cr3 = ~0;      /* initialize to an invalid value */
 2432 
 2433         if ((pml4pg->flags & PG_ZERO) == 0)
 2434                 pagezero(pmap->pm_pml4);
 2435 
 2436         /*
 2437          * Do not install the host kernel mappings in the nested page
 2438          * tables. These mappings are meaningless in the guest physical
 2439          * address space.
 2440          */
 2441         if ((pmap->pm_type = pm_type) == PT_X86) {
 2442                 pmap->pm_cr3 = pml4phys;
 2443                 pmap_pinit_pml4(pml4pg);
 2444         }
 2445 
 2446         pmap->pm_root.rt_root = 0;
 2447         CPU_ZERO(&pmap->pm_active);
 2448         TAILQ_INIT(&pmap->pm_pvchunk);
 2449         bzero(&pmap->pm_stats, sizeof pmap->pm_stats);
 2450         pmap->pm_flags = flags;
 2451         pmap->pm_eptgen = 0;
 2452 
 2453         return (1);
 2454 }

pm_typeとしてPT_EPTを渡しているので2441行目のところは実行されません。ここは通常のページテーブルの設定の場合のみに実行ですね。それ以外はpmap構造体の設定でpml4の物理アドレを設定したりとかしてます。

次に気になるのはept_vmspace_alloc()が何時呼ばれるのか?ですね。

ept_vmspace_alloc()の呼ばれ方

vmx_init()と同様にvmm_ops構造体に関数を設定しています。設定先の変数はvmspace_allocです。この関数も直接は呼び出さないでマクロのVMSPACE_ALLOCマクロから呼ばれます。

  178 #define VMSPACE_ALLOC(min, max) \
  179         (ops != NULL ? (*ops->vmspace_alloc)(min, max) : NULL)

vm_create()がVMSPACE_ALLOCマクロを使っています。

  422 int
  423 vm_create(const char *name, struct vm **retvm)
  424 {
  425         struct vm *vm;
  426         struct vmspace *vmspace;
  427 
  428         /*
  429          * If vmm.ko could not be successfully initialized then don't attempt
  430          * to create the virtual machine.
  431          */
  432         if (!vmm_initialized)
  433                 return (ENXIO);
  434 
  435         if (name == NULL || strlen(name) >= VM_MAX_NAMELEN)
  436                 return (EINVAL);
  437 
  438         vmspace = VMSPACE_ALLOC(0, VM_MAXUSER_ADDRESS);
  439         if (vmspace == NULL)
  440                 return (ENOMEM);
  441 
  442         vm = malloc(sizeof(struct vm), M_VM, M_WAITOK | M_ZERO);
  443         strcpy(vm->name, name);
  444         vm->vmspace = vmspace;
  445         mtx_init(&vm->rendezvous_mtx, "vm rendezvous lock", 0, MTX_DEF);
  446 
  447         vm_init(vm, true);
  448 
  449         *retvm = vm;
  450         return (0);
  451 }

vm_create()を呼んでいるのはsysctl_vmm_create()です。なのでVMの作成時に初期化処理の流れで呼ばれる感じですね。

まとめ

EPTPはeptl()で設定します。EPTに使うページテーブルの設定はept_init()でケーパビリティのチェックやフラグの設定をしてからpmap_pinit_type()でpmap構造体の設定を行うことで設定しています。

(´-`).。oO(BSD系のコードは初見でも読みやすい

( ´ー`)フゥー...

はじめてUNIXで仕事をする人が読む本 (アスキードワンゴ)

はじめてUNIXで仕事をする人が読む本 (アスキードワンゴ)