VDSO(arm)の実装をちょっと調べてみました

  • 10
    Like
  • 2
    Comment

Linux vdso

とある事情で、vdsoについて調べる機会がありました。
丁度Advent Calendarのネタに困っていた今日このごろ、早速書いてみることにしました。

なお、ソースは以下のバージョンです。

名称 バージョン
Linux 4.9
glibc 2.23

vdsoとは

"vDSO" (virtual dynamic shared object; 仮想動的共有オブジェクト) という共有ライブラリです。
目的は、頻繁に呼び出されるシステムコールをカーネル空間に切り替えることなくユーザ空間で処理させることにより性能を向上させることが目的です。
代表的な使用例は、gettimeofday()です。
詳細は、Linuxのmanページを見てください。

例えば・・・以下のように、lddコマンドで適当なバイナリの依存ライブラリを確認すると、linux-vdso.so.1は具体的なファイルでなく、それが存在するアドレスのみが表示されます。ちょっと特殊ですね。

% ldd /usr/games/sl
    linux-vdso.so.1 =>  (0x00007ffd58df1000)
    libncurses.so.5 => /lib/x86_64-linux-gnu/libncurses.so.5 (0x00007feb1e0db000)
    libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007feb1deb2000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007feb1dae8000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007feb1d8e4000)
    /lib64/ld-linux-x86-64.so.2 (0x000055f5bb5cb000)

また、VDSOについてはlwn.netの記事にも実装の概要含め詳しく書かれています。しかし、実装例はx86_64です。
ちょっと諸事情から、ARM(32bit)ではどう扱われているのか興味を持ったので、調べてみました。

まずlibcを見てみよう

使用例としてgettimeofday()が挙げられているので、glibcのコードを確認しました。
gettimeofday()の実装は以下のとおりです。

sysdeps/unix/sysv/linux/gettimeofday.c
#include <errno.h>
#include <sys/time.h>

#undef __gettimeofday

#ifdef HAVE_GETTIMEOFDAY_VSYSCALL
# define HAVE_VSYSCALL
#endif
#include <sysdep-vdso.h>

/* Get the current time of day and timezone information,
   putting it into *tv and *tz.  If tz is null, *tz is not filled.
   Returns 0 on success, -1 on errors.  */
int
__gettimeofday (struct timeval *tv, struct timezone *tz)
{
  return INLINE_VSYSCALL (gettimeofday, 2, tv, tz);
}
libc_hidden_def (__gettimeofday)
weak_alias (__gettimeofday, gettimeofday)
libc_hidden_weak (gettimeofday)

さらに、INLINE_VSYSCALLマクロは以下の通りになります。
なお、glibc(arm)では、HAVE_GETTIMEOFDAY_VSYSCALLは1と定義されていますが、割愛します。

sysdeps/unix/sysv/linux/sysdep-vdso.h

#  define INLINE_VSYSCALL(name, nr, args...)                      \
  ({                                          \
    __label__ out;                                \
    __label__ iserr;                                  \
    INTERNAL_SYSCALL_DECL (sc_err);                       \
    long int sc_ret;                                  \
                                          \
    __typeof (__vdso_##name) vdsop = __vdso_##name;               \
    PTR_DEMANGLE (vdsop);                             \
    if (vdsop != NULL)                                \
      {                                       \
    sc_ret = INTERNAL_VSYSCALL_CALL (vdsop, sc_err, nr, ##args);          \
    if (!INTERNAL_SYSCALL_ERROR_P (sc_ret, sc_err))               \
      goto out;                               \
    if (INTERNAL_SYSCALL_ERRNO (sc_ret, sc_err) != ENOSYS)            \
      goto iserr;                                 \
      }                                       \
                                          \
    sc_ret = INTERNAL_SYSCALL (name, sc_err, nr, ##args);             \
    if (INTERNAL_SYSCALL_ERROR_P (sc_ret, sc_err))                \
      {                                       \
      iserr:                                      \
        __set_errno (INTERNAL_SYSCALL_ERRNO (sc_ret, sc_err));            \
        sc_ret = -1L;                                 \
      }                                       \
  out:                                        \
    sc_ret;                                   \
  })

上記のマクロ内の定義全てをマトモに書くと、調べたいことの本質から離れてしまうため、必要な箇所について述べます。
今回必要な箇所は前半の部分です。前半の箇所を__typeofを除いて展開すると、以下のようになります。
なお、INTERNAL_VSYSCALL_CALLの定義は、sysdeps/unix/sysv/linux/sysdep-vdso.h にあります。

  ({
    __label__ out;
    __label__ iserr;
    // INTERNAL_SYSCALL_DECLの定義は略 
    long int sc_ret;

    __typeof (__vdso_gettimeofday) vdsop = __vdso_gettimeofday;
    // PTR_DEMANGLEの定義は略
    if (vdsop != NULL)
      {
    sc_ret = vpsop(args);

問題は、__vdso_gettimeofdayの実体がどこにいるかということです。展開されたコードから、関数の実体もしくは関数ポインタであることは想像できます。さて、どこにいるのでしょうか。

__vdso_getitimeofdayはどこにいる

glibcを検索してみると、以下のコードが見当たります。しかし、実体が定義されているわけではなさそうです。

sysdeps/unix/sysv/linux/arm/init-first.c
 24 int (*VDSO_SYMBOL(gettimeofday)) (struct timeval *, void *) attribute_hidden    ;
 25 int (*VDSO_SYMBOL(clock_gettime)) (clockid_t, struct timespec *);
 26 
 27 static inline void
 28 _libc_vdso_platform_setup (void)
 29 {
 30   PREPARE_VERSION_KNOWN (linux26, LINUX_2_6);
 31 
 32   void *p = _dl_vdso_vsym ("__vdso_gettimeofday", &linux26);
 33   PTR_MANGLE (p);
 34   VDSO_SYMBOL (gettimeofday) = p;
// 略
 39 }

実は、__vdso_gettimeofdayの実体は、Linuxカーネルのソースツリー内にいるのです!「え?」と言いたくなりますが、先に進みましょう。

Linux(arm)側ではどうなっている

Linux(arm)内の__vdso_gettimeofdayの実装は以下のとおりです。

arch/arm/vdso/vgettimeofday.c
246 notrace int __vdso_gettimeofday(struct timeval *tv, struct timezone *tz)
247 {   
248     struct timespec ts;
249     struct vdso_data *vdata;
250     int ret;
251     
252     vdata = __get_datapage();
253     
254     ret = do_realtime(&ts, vdata);
255     if (ret)
256         return gettimeofday_fallback(tv, tz);
257     
258     if (tv) {
259         tv->tv_sec = ts.tv_sec;
260         tv->tv_usec = ts.tv_nsec / 1000;
261     }
262     if (tz) {
263         tz->tz_minuteswest = vdata->tz_minuteswest;
264         tz->tz_dsttime = vdata->tz_dsttime;
265     }
266     
267     return ret;
268 }

確かにそれっぽい(実際にそうなんですが(笑))実装です。この関数が呼ばれるのなら問題はなさそうです。
ただ、今回は実装の詳細には立ち入りません。理由はgettimeofday()の実装でなく、vdsoがどうなっているかを知りたいためです。
そして、ソースツリーに検索をかけるとわかるのですが、もうひとつ大切な箇所があります。それは以下のコードです。

arch/arm/vdso/vdso.lds.S
 31 SECTIONS
 32 {   
 33     PROVIDE(_start = .);
 34     
 35     . = SIZEOF_HEADERS;
 36     
 37     .hash       : { *(.hash) }          :text
 38     .gnu.hash   : { *(.gnu.hash) }
 39     .dynsym     : { *(.dynsym) }
 40     .dynstr     : { *(.dynstr) }
/* 略(Cのコメント形式で書くのも何か変だが) */
 55     .text       : { *(.text*) }         :text   =0xe7f001f2
/* 略(Cのコメント形式で書くのも何か変だが) */
 65 }
 66 
 67 /*
 68  * We must supply the ELF program headers explicitly to get just one
 69  * PT_LOAD segment, and set the flags explicitly to make segments read-only.
 70  */
 71 PHDRS
 72 {
 73     text        PT_LOAD     FLAGS(5) FILEHDR PHDRS; /* PF_R|PF_X */
 74     dynamic     PT_DYNAMIC  FLAGS(4);       /* PF_R */
 75     note        PT_NOTE     FLAGS(4);       /* PF_R */
 76     eh_frame_hdr    PT_GNU_EH_FRAME;
 77 }
 78 
 79 VERSION
 80 {
 81     LINUX_2.6 {
 82     global:
 83         __vdso_clock_gettime;
 84         __vdso_gettimeofday;
 85     local: *;
 86     };
 87 }

リンカスクリプトの元となるファイルです。arch/arm/vdso/Makefileでは最終的にこのディレクトリにあるファイル群を用いて最終的にはvdso.soというELFオブジェクトを作成します。

arch/arm/vdso/Makefile
# Build rules
targets := $(obj-vdso) vdso.so vdso.so.dbg vdso.so.raw vdso.lds
obj-vdso := $(addprefix $(obj)/, $(obj-vdso))
# 略
# Force dependency
$(obj)/vdso.o : $(obj)/vdso.so
# 略

省略しますが、カーネルビルドの際に、arch/arm/Makefile経由でarch/arm/vdso/Makefileが叩かれるので、vdso.soが作られるのはカーネルビルドのときだとわかります。

そして、出来上がったvdso.soはカーネルビルドの過程で、vmlinuxの特定の位置に埋め込まれます。その「特定の位置」は、以下のコードからvdso_startというシンボル名を先頭とする領域であることがわかります。

arch/arm/vdso/vdso.S
 21 #include <linux/init.h>
 22 #include <linux/linkage.h>
 23 #include <linux/const.h>
 24 #include <asm/page.h>
 25 
 26     .globl vdso_start, vdso_end
 27     .section .data..ro_after_init
 28     .balign PAGE_SIZE
 29 vdso_start:
 30     .incbin "arch/arm/vdso/vdso.so"
 31     .balign PAGE_SIZE
 32 vdso_end:
 33 
 34     .previous

本当に埋め込まれているか確認

ビルドしたarm版Linuxの中に本当にvdso.soが埋め込まれているか確認しましょう。
(意図的にユーザ名やコマンドプロンプトの一部を編集しています。ご了承ください。)

まずはカーネルビルド

カーネルビルドのやり方は、こちらのページを参考にしつつ、以下の方法で実施しました。
なお、私の環境はUbuntu Linux 16.04になります。・・・なんか先のページとかなり違うけど・・・。

(1)まずはクロス環境をインストールします。

apt-get install gcc-arm-none-eabi

(2) カーネルソースディレクトリで、以下のコマンドを実行し、arch/arm/configsにある設定ファイル(multi_v7_defconfig)の設定を適用して、.configを作ります。
この設定ファイルを選んだ理由は、デフォルトでVDSOが有効になっていて手間がかからないからです。

make ARCH=arm multi_v7_defconfig

(3)カーネルビルドします。お馴染みのvmlinuxがビルドされますね。

make ARCH=arm CROSS_COMPILE=arm-none-eabi-

vmlinux内のvdso.soを確認

まず、vdso.soのサイズを確認します。

 % ls -al arch/arm/vdso/vdso.so
-rwxrwxr-x 1 hoge hoge 2700 12月 22 22:48 arch/arm/vdso/vdso.so

readelfでセクションヘッダを確認したうえ、armクロスツールのnmでvdso_startの位置を確認しましょう。

 % readelf -S vmlinux                                                                                                                                                                          
34 個のセクションヘッダ、始点オフセット 0x13e613c:

セクションヘッダ:
  [番] 名前              タイプ          アドレス Off    サイズ ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .head.text        PROGBITS        c0208000 008000 00026c 00  AX  0   0  4
  [ 2] .text             PROGBITS        c0300000 010000 8e0e28 00  AX  0   0 4096
  [ 3] .fixup            PROGBITS        c0be0e28 8f0e28 00001c 00  AX  0   0  4
  [ 4] .rodata           PROGBITS        c0c00000 900000 37f300 00  WA  0   0 4096
  [ 5] __bug_table       PROGBITS        c0f7f300 c7f300 008238 00   A  0   0  4
// 省略
  [33] .strtab           STRTAB          00000000 1222fec 1c2fea 00      0   0  1
フラグのキー:
  W (write), A (alloc), X (実行), M (merge), S (文字列)
  I (情報), L (リンク順), G (グループ), T (TLS), E (排他), x (不明)
  O (追加の OS 処理が必要) o (OS 固有), p (プロセッサ固有)

 % arm-none-eabi-nm vmlinux | grep vdso_start                                                                                                                                                  
c0f7e000 R vdso_start

これでファイル先頭からvdso_startのある位置がわかりました( 0xc0f7e000 - 0xc0c00000 + 0x900000)ので、vdso.soのサイズ分切り出します。
その上でvdso.soと一致するか確認しましょう。

 % dd if=vmlinux of=vdso.bin bs=1 count=2700 skip=13099008
2700+0 レコード入力
2700+0 レコード出力
2700 bytes (2.7 kB, 2.6 KiB) copied, 0.00900085 s, 300 kB/s

 % diff arch/arm/vdso/vdso.so vdso.bin 

確かに一致しました。あとは、どうやってvdso.soをユーザ空間に見せるかを見ましょう。

ユーザ空間にvdsoを見せる

カーネルに埋め込まれたvdso.soは、execの際にユーザアドレス空間にマップされます。
execのメイン処理であるdo_execveat_common()から以下関数群の呼び出しを経て、arch_setup_additional_pages()が呼ばれ、その先でvdso.soのマップが行われます。

fs/exec.c
/*
 * sys_execve() executes a new program.
 */
static int do_execveat_common(int fd, struct filename *filename,
                  struct user_arg_ptr argv,
                  struct user_arg_ptr envp,
                  int flags)
{
/* 略 */
    retval = exec_binprm(bprm);
    if (retval < 0)
        goto out;
fs/exec.c
static int exec_binprm(struct linux_binprm *bprm)
{
/* 略 */
    ret = search_binary_handler(bprm);
    if (ret >= 0) {
        audit_bprm(bprm);
        trace_sched_process_exec(current, old_pid, bprm);
        ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
        proc_exec_connector(current);
    }

    return ret;
}
fs/exec.c

/*
 * cycle the list of binary formats handler, until one recognizes the image
 */
int search_binary_handler(struct linux_binprm *bprm)
{
/* 略 */
 retry:
    read_lock(&binfmt_lock);
    list_for_each_entry(fmt, &formats, lh) {
/* 略 */
        retval = fmt->load_binary(bprm);
fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
/* 略 */
#ifdef ARCH_HAS_SETUP_ADDITIONAL_PAGES
    retval = arch_setup_additional_pages(bprm, !!elf_interpreter);
    if (retval < 0)
        goto out;
#endif /* ARCH_HAS_SETUP_ADDITIONAL_PAGES */

arch_setup_additional_pages()では、最後にarm_install_vdso()を呼び出します。

arch/arm/kernel/process.c
int arch_setup_additional_pages(struct linux_binprm *bprm, int uses_interp)
{
    struct mm_struct *mm = current->mm;
/* 略 */
    /* Unlike the sigpage, failure to install the vdso is unlikely
     * to be fatal to the process, so no error check needed
     * here.
     */
    arm_install_vdso(mm, addr + PAGE_SIZE);

up_fail:
    up_write(&mm->mmap_sem);
    return ret;
}

以下がarm_install_vdso()です。この関数では、_install_special_mapping()を呼び出し、これによって最後の引数vdso_text_mappingで指定した領域をマップします。

arch/arm/kernel/process.c
/* assumes mmap_sem is write-locked */ 
void arm_install_vdso(struct mm_struct *mm, unsigned long addr)
{
    struct vm_area_struct *vma;
    unsigned long len;

    mm->context.vdso = 0;

    if (vdso_text_pagelist == NULL)
        return;

    if (install_vvar(mm, addr))
        return;

    /* Account for vvar page. */
    addr += PAGE_SIZE;
    len = (vdso_total_pages - 1) << PAGE_SHIFT;

    vma = _install_special_mapping(mm, addr, len,
        VM_READ | VM_EXEC | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC,
        &vdso_text_mapping);

    if (!IS_ERR(vma))
        mm->context.vdso = addr;
}

vdso_text_mappingはカーネルの起動時に以下の関数で初期化されます。先に書いたvdso_startとvdso_endに挟まれた領域(ページ)を示すpage構造体を取得し、これを用いてvdso_text_mappingを初期化しているのがわかります。

arch/arm/kernel/vdso.c
/*
 * The VDSO data page.
 */
/* 略 */
static struct vm_special_mapping vdso_text_mapping __ro_after_init = {
    .name = "[vdso]",
};
/* 略 */
static int __init vdso_init(void)
{
    unsigned int text_pages;
    int i;

    if (memcmp(&vdso_start, "\177ELF", 4)) {
        pr_err("VDSO is not a valid ELF object!\n");
        return -ENOEXEC;
    }

    text_pages = (&vdso_end - &vdso_start) >> PAGE_SHIFT;
/* 略 */

    /* Grab the VDSO text pages. */
    for (i = 0; i < text_pages; i++) {
        struct page *page;

        page = virt_to_page(&vdso_start + i * PAGE_SIZE);
        vdso_text_pagelist[i] = page;
    }

    vdso_text_mapping.pages = vdso_text_pagelist;

    vdso_total_pages = 1; /* for the data/vvar page */
    vdso_total_pages += text_pages; 

これで確かにvdso.soがユーザ空間から見えるようになりそうです。

最後に

今回はARM版LinuxでVDSOをどう実現しているか確かめてみました。
カーネルのソースを読むのもとても面白いのですが、ビルドしたバイナリでちょっと遊んでみるのもまた一興です。また、普段はx86の人もたまにはARMやMIPSのコードを読んでみると、新たな発見があります。
カーネルのソースを読むのはどこから読めばよいのか聞かれますが、まあ、日常のちょっとした疑問の解消レベルで興味あるところから少しずつ読んでみれば良いと思います。
今年も少なくなり、相変わらず進捗に追われっぱなし、この記事も進捗遅れましたが、最後にハッピーになれることを祈ります。
それでは、皆様、Happy Hacking!