Posted at
LinuxDay 18

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

More than 1 year has passed since last update.


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!