泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

还在观望?5小时公开课入门RISC-V架构
请稍侯

RISC-V Syscall 系列 4:vDSO 实现原理分析

cc 创作于 2022/09/15

Corrector: TinyCorrect v0.1-rc1 - [images urls] Author: envestcc chen1233216@hotmail.com Date: 2022/08/16 Revisor: Falcon falcon@tinylab.org Project: RISC-V Linux 内核剖析 Environment: 泰晓 Linux 实验盘 Sponsor: PLCT Lab, ISCAS

1 概述

在上一篇文章 什么是 vDSO 中介绍了 vDSO 的相关背景和概念,本篇文章会进一步通过对 Linux 内核及 glibc 相关代码的研究,来分析 vDSO 的实现原理。

说明:文中涉及的 Linux 源码是基于 5.17 版本,glibc 是基于 2.35 版本。

2 Build

Linux 内核中 vDSO 代码包括以下几部分:

  • lib/vdso/:架构无关部分
    • gettimeofday.c
  • arch/riscv/kernel/:架构相关部分
    • vdso.c:数据结构定义及初始化
    • vdso/:导出函数入口
      • flush_icache.S
      • getcpu.S
      • rt_sigreturn.S
      • vgettimeofday.c
      • vdso.S
      • vdso.lds.S

下面未加路径的文件默认路径为 arch/riscv/kernel/vdso

objcopy -S
.incbin
lib/vdso/gettimeofday.c
vdso.so.dbg / linux-vdso.so.1
vgettimeofday.c
flush_icache.S
getcpu.S
rt_sigreturn.S
note.S
vdso.lds.S
vdso.so
vdso.o
vdso.S
kernel
arch/riscv/kernel/vdso.c

下载由 Mermaid 生成的 PNG 图片

上图描述了上述代码如何编译成 linux-vdso.so.1 及如何集成到内核中的大体流程。整个流程大致可以分为两个阶段:

  1. 生成共享库 linux-vdso.so.1
  2. 共享库集成到内核

下面会结合内核编译日志和内核源码一起分析整个构建过程。

2.1 生成共享库 linux-vdso.so.1

生成共享库主要分为两个阶段:

  1. 编译生成 .o 文件
  2. 链接生成 .so 共享库文件

2.1.1 编译生成 .o 文件

在 Linux Lab 下,可通过 make kernel arch/riscv/kernel/vdso/*.o V=1 查看到生成 .o 的过程:

  1. riscv64-linux-gnu-gcc -E -Wp,-MMD,arch/riscv/kernel/vdso/.vdso.lds.d -nostdinc -I./arch/riscv/include -I./arch/riscv/include/generated -I./include -I./arch/riscv/include/uapi -I./arch/riscv/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/compiler-version.h -include ./include/linux/kconfig.h -D__KERNEL__ -fmacro-prefix-map=./= -P -C -Uriscv -P -Uriscv -D__ASSEMBLY__ -DLINKER_SCRIPT -o arch/riscv/kernel/vdso/vdso.lds arch/riscv/kernel/vdso/vdso.lds.S
  2. riscv64-linux-gnu-gcc -Wp,-MMD,arch/riscv/kernel/vdso/.rt_sigreturn.o.d -nostdinc -I./arch/riscv/include -I./arch/riscv/include/generated -I./include -I./arch/riscv/include/uapi -I./arch/riscv/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/compiler-version.h -include ./include/linux/kconfig.h -D__KERNEL__ -fmacro-prefix-map=./= -D__ASSEMBLY__ -fno-PIE -mabi=lp64 -march=rv64imafdc -Wa,-gdwarf-2 -c -o arch/riscv/kernel/vdso/rt_sigreturn.o arch/riscv/kernel/vdso/rt_sigreturn.S
  3. riscv64-linux-gnu-gcc -Wp,-MMD,arch/riscv/kernel/vdso/.vgettimeofday.o.d -nostdinc -I./arch/riscv/include -I./arch/riscv/include/generated -I./include -I./arch/riscv/include/uapi -I./arch/riscv/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/compiler-version.h -include ./include/linux/kconfig.h -include ./include/linux/compiler_types.h -D__KERNEL__ -fmacro-prefix-map=./= -Wall -Wundef -Werror=strict-prototypes -Wno-trigraphs -fno-strict-aliasing -fno-common -fshort-wchar -fno-PIE -Werror=implicit-function-declaration -Werror=implicit-int -Werror=return-type -Wno-format-security -std=gnu89 -mabi=lp64 -march=rv64imac -mno-save-restore -DCONFIG_PAGE_OFFSET=0xffffaf8000000000 -mcmodel=medany -fno-omit-frame-pointer -mstrict-align -fno-delete-null-pointer-checks -Wno-frame-address -Wno-format-truncation -Wno-format-overflow -Wno-address-of-packed-member -O2 --param=allow-store-data-races=0 -Wframe-larger-than=2048 -fstack-protector-strong -Wimplicit-fallthrough=5 -Wno-main -Wno-unused-but-set-variable -Wno-unused-const-variable -fno-omit-frame-pointer -fno-optimize-sibling-calls -fno-stack-clash-protection -Wdeclaration-after-statement -Wvla -Wno-pointer-sign -Wcast-function-type -Wno-stringop-truncation -Wno-array-bounds -Wno-stringop-overflow -Wno-restrict -Wno-maybe-uninitialized -Wno-alloc-size-larger-than -fno-strict-overflow -fno-stack-check -fconserve-stack -Werror=date-time -Werror=incompatible-pointer-types -Werror=designated-init -Wno-packed-not-aligned -g -fno-stack-protector -fPIC -include /labs/linux-lab/src/linux-stable/lib/vdso/gettimeofday.c -DKBUILD_MODFILE='"arch/riscv/kernel/vdso/vgettimeofday"' -DKBUILD_BASENAME='"vgettimeofday"' -DKBUILD_MODNAME='"vgettimeofday"' -D__KBUILD_MODNAME=kmod_vgettimeofday -c -o arch/riscv/kernel/vdso/vgettimeofday.o arch/riscv/kernel/vdso/vgettimeofday.c
  4. riscv64-linux-gnu-gcc -Wp,-MMD,arch/riscv/kernel/vdso/.getcpu.o.d -nostdinc -I./arch/riscv/include -I./arch/riscv/include/generated -I./include -I./arch/riscv/include/uapi -I./arch/riscv/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/compiler-version.h -include ./include/linux/kconfig.h -D__KERNEL__ -fmacro-prefix-map=./= -D__ASSEMBLY__ -fno-PIE -mabi=lp64 -march=rv64imafdc -Wa,-gdwarf-2 -c -o arch/riscv/kernel/vdso/getcpu.o arch/riscv/kernel/vdso/getcpu.S
  5. riscv64-linux-gnu-gcc -Wp,-MMD,arch/riscv/kernel/vdso/.flush_icache.o.d -nostdinc -I./arch/riscv/include -I./arch/riscv/include/generated -I./include -I./arch/riscv/include/uapi -I./arch/riscv/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/compiler-version.h -include ./include/linux/kconfig.h -D__KERNEL__ -fmacro-prefix-map=./= -D__ASSEMBLY__ -fno-PIE -mabi=lp64 -march=rv64imafdc -Wa,-gdwarf-2 -c -o arch/riscv/kernel/vdso/flush_icache.o arch/riscv/kernel/vdso/flush_icache.S
  6. riscv64-linux-gnu-gcc -Wp,-MMD,arch/riscv/kernel/vdso/.note.o.d -nostdinc -I./arch/riscv/include -I./arch/riscv/include/generated -I./include -I./arch/riscv/include/uapi -I./arch/riscv/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/compiler-version.h -include ./include/linux/kconfig.h -D__KERNEL__ -fmacro-prefix-map=./= -D__ASSEMBLY__ -fno-PIE -mabi=lp64 -march=rv64imafdc -Wa,-gdwarf-2 -c -o arch/riscv/kernel/vdso/note.o arch/riscv/kernel/vdso/note.S

从上述编译日志可以看出,首先 vdso.lds.S 是链接脚本文件,会通过 gcc -E 命令执行预处理。然后 lib/vdso/gettimeofday.cvgettimeofday.cflush_icache.Sgetcpu.Srt_sigreturn.Snote.S 这几个文件会通过 gcc -c 命令编译成 .o 文件。

2.1.2 链接生成 .so 共享库文件

在 Linux Lab 下,可通过 make kernel arch/riscv/kernel/vdso/vdso.so V=1 查看到生成 .so 的过程:

  1. riscv64-linux-gnu-ld -melf64lriscv -shared -S -soname=linux-vdso.so.1 --build-id=sha1 --hash-style=both --eh-frame-hdr -T arch/riscv/kernel/vdso/vdso.lds arch/riscv/kernel/vdso/rt_sigreturn.o arch/riscv/kernel/vdso/vgettimeofday.o arch/riscv/kernel/vdso/getcpu.o arch/riscv/kernel/vdso/flush_icache.o arch/riscv/kernel/vdso/note.o -o arch/riscv/kernel/vdso/vdso.so.dbg.tmp && riscv64-linux-gnu-objcopy -G __vdso_rt_sigreturn -G __vdso_vgettimeofday -G __vdso_getcpu -G __vdso_flush_icache arch/riscv/kernel/vdso/vdso.so.dbg.tmp arch/riscv/kernel/vdso/vdso.so.dbg && rm arch/riscv/kernel/vdso/vdso.so.dbg.tmp

通过把上一步生成的中间文件通过 ld 命令链接起来,最后生成 vdso.so.dbg 共享库文件。这里通过 -soname=linux-vdso.so.1 参数指定了库的真实名字。另外其中的 objcopy -G 命令是将本地函数变为全局函数,我理解现在的版本中已经不需要了,因为在后面的流程中,会移除静态符号表信息。

vdso.so.dbg 的真实名字就是 linux-vdso.so.1,也可以通过下面的命令进行验证:

  1. $ readelf -d /labs/linux-lab/build/riscv64/virt/linux/v5.17/arch/riscv/kernel/vdso/vdso.so.dbg
  2. Dynamic section at offset 0x390 contains 14 entries:
  3. Tag Type Name/Value
  4. 0x000000000000000e (SONAME) Library soname: [linux-vdso.so.1]
  5. 0x0000000000000004 (HASH) 0x120
  6. 0x000000006ffffef5 (GNU_HASH) 0x158
  7. 0x0000000000000005 (STRTAB) 0x270
  8. 0x0000000000000006 (SYMTAB) 0x198
  9. 0x000000000000000a (STRSZ) 143 (bytes)
  10. 0x000000000000000b (SYMENT) 24 (bytes)
  11. 0x0000000000000007 (RELA) 0x0
  12. 0x0000000000000008 (RELASZ) 0 (bytes)
  13. 0x0000000000000009 (RELAENT) 24 (bytes)
  14. 0x000000006ffffffc (VERDEF) 0x318
  15. 0x000000006ffffffd (VERDEFNUM) 2
  16. 0x000000006ffffff0 (VERSYM) 0x300
  17. 0x0000000000000000 (NULL) 0x0

2.2 共享库集成到内核

  1. riscv64-linux-gnu-objcopy -S arch/riscv/kernel/vdso/vdso.so.dbg arch/riscv/kernel/vdso/vdso.so

先通过 objcopy -S 命令将 vdso.so.dbg 移除符号信息进而生成 vdso.so。这主要是为了减少集成到内核的代码大小。

  1. $ readelf -sW /labs/linux-lab/build/riscv64/virt/linux/v5.17/arch/riscv/kernel/vdso/vdso.so.dbg
  2. Symbol table '.dynsym' contains 9 entries:
  3. Num: Value Size Type Bind Vis Ndx Name
  4. ...
  5. __vdso_gettimeofday@@LINUX_4.15
  6. 3: 0000000000000bee 122 FUNC GLOBAL DEFAULT 11
  7. ...
  8. Symbol table '.symtab' contains 29 entries:
  9. Num: Value Size Type Bind Vis Ndx Name
  10. ...
  11. 14: 0000000000000000 0 FILE LOCAL DEFAULT ABS vgettimeofday.c
  12. 15: 0000000000000000 0 FILE LOCAL DEFAULT ABS
  13. 16: fffffffffffff000 0 NOTYPE LOCAL DEFAULT ABS _timens_data
  14. 17: 0000000000000390 0 OBJECT LOCAL DEFAULT ABS _DYNAMIC
  15. 18: 0000000000000c80 0 OBJECT LOCAL DEFAULT ABS _PROCEDURE_LINKAGE_TABLE_
  16. 19: ffffffffffffe000 0 NOTYPE LOCAL DEFAULT 1 _vdso_data
  17. ...
  18. $ readelf -sW /labs/linux-lab/build/riscv64/virt/linux/v5.17/arch/riscv/kernel/vdso/vdso.so
  19. Symbol table '.dynsym' contains 9 entries:
  20. Num: Value Size Type Bind Vis Ndx Name
  21. ...
  22. 2: 0000000000000a64 394 FUNC GLOBAL DEFAULT 11 __vdso_gettimeofday@@LINUX_4.15
  23. ...

通过上面两个命令输出的对比,能看出 vdso.dbg.so 生成 vdso.so 之后移除了静态符号表信息。

  1. riscv64-linux-gnu-gcc -Wp,-MMD,arch/riscv/kernel/.vdso.o.d -nostdinc -I./arch/riscv/include -I./arch/riscv/include/generated -I./include -I./arch/riscv/include/uapi -I./arch/riscv/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/compiler-version.h -include ./include/linux/kconfig.h -include ./include/linux/compiler_types.h -D__KERNEL__ -fmacro-prefix-map=./= -Wall -Wundef -Werror=strict-prototypes -Wno-trigraphs -fno-strict-aliasing -fno-common -fshort-wchar -fno-PIE -Werror=implicit-function-declaration -Werror=implicit-int -Werror=return-type -Wno-format-security -std=gnu89 -mabi=lp64 -march=rv64imac -mno-save-restore -DCONFIG_PAGE_OFFSET=0xffffaf8000000000 -mcmodel=medany -fno-omit-frame-pointer -mstrict-align -fno-delete-null-pointer-checks -Wno-frame-address -Wno-format-truncation -Wno-format-overflow -Wno-address-of-packed-member -O2 --param=allow-store-data-races=0 -Wframe-larger-than=2048 -fstack-protector-strong -Wimplicit-fallthrough=5 -Wno-main -Wno-unused-but-set-variable -Wno-unused-const-variable -fno-omit-frame-pointer -fno-optimize-sibling-calls -fno-stack-clash-protection -Wdeclaration-after-statement -Wvla -Wno-pointer-sign -Wcast-function-type -Wno-stringop-truncation -Wno-array-bounds -Wno-stringop-overflow -Wno-restrict -Wno-maybe-uninitialized -Wno-alloc-size-larger-than -fno-strict-overflow -fno-stack-check -fconserve-stack -Werror=date-time -Werror=incompatible-pointer-types -Werror=designated-init -Wno-packed-not-aligned -g -DKBUILD_MODFILE='"arch/riscv/kernel/vdso"' -DKBUILD_BASENAME='"vdso"' -DKBUILD_MODNAME='"vdso"' -D__KBUILD_MODNAME=kmod_vdso -c -o arch/riscv/kernel/vdso.o arch/riscv/kernel/vdso.c

然后通过 gcc 命令将 arch/riscv/kernel/vdso.c 编译成 arch/riscv/kernel/vdso.o 文件。

  1. riscv64-linux-gnu-gcc -Wp,-MMD,arch/riscv/kernel/vdso/.vdso.o.d -nostdinc -I./arch/riscv/include -I./arch/riscv/include/generated -I./include -I./arch/riscv/include/uapi -I./arch/riscv/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/compiler-version.h -include ./include/linux/kconfig.h -D__KERNEL__ -fmacro-prefix-map=./= -D__ASSEMBLY__ -fno-PIE -mabi=lp64 -march=rv64imafdc -Wa,-gdwarf-2 -c -o arch/riscv/kernel/vdso/vdso.o arch/riscv/kernel/vdso/vdso.S

然后又通过 gcc 命令将 vdso.S 编译生成了 vdso.o 文件。vdso.S 文件内部其实就是通过 .incbinvdso.so 共享库包含进来,同时设置一下内存页对齐。vdso.S 的代码如下:

  1. #include <linux/init.h>
  2. #include <linux/linkage.h>
  3. #include <asm/page.h>
  4. __PAGE_ALIGNED_DATA
  5. .globl vdso_start, vdso_end
  6. .balign PAGE_SIZE
  7. vdso_start:
  8. .incbin "arch/riscv/kernel/vdso/vdso.so"
  9. .balign PAGE_SIZE
  10. vdso_end:
  11. .previous

注意这里的 vdso.o 文件和上一步生成的 arch/riscv/kernel/vdso.o 不在同一个目录下。

  1. riscv64-linux-gnu-ar cDPrST arch/riscv/kernel/vdso/built-in.a arch/riscv/kernel/vdso/vdso.o
  2. riscv64-linux-gnu-ar cDPrST arch/riscv/kernel/built-in.a arch/riscv/kernel/vdso.o arch/riscv/kernel/vdso/built-in.a ...

然后通过 ar 命令将 vdso.o 打包到 built-in.a 文件中,再将 built-in.aarch/riscv/kernel/vdso.o 一起打包到 arch/riscv/kernel/built-in.a 文件中,最终被打包进内核中。

3 vDSO 初始化

vDSO 的初始化按照触发时机可以分为两部分:

  • 内核启动时初始化
  • 用户进程启动时初始化

3.1 内核启动时初始化

内核启动时初始化的主要是 vdso_info 这个内核对象。它包含的主要信息包括:

  • vDSO 代码在内核中的地址
  • vDSO 数据在内核中的地址
  • vDSO 代码部分虚拟内存映射结构
  • vDSO 数据部分虚拟内存映射结构

vdso_info 源码中的相关定义如下:

  1. // arch/riscv/kernel/vdso.c
  2. extern char vdso_start[], vdso_end[];
  3. struct __vdso_info {
  4. const char *name;
  5. const char *vdso_code_start; // vdso 代码起始地址
  6. const char *vdso_code_end; // vdso 代码结束地址
  7. unsigned long vdso_pages; // vdso 代码部分所占内存页数
  8. /* Data Mapping */
  9. struct vm_special_mapping *dm;
  10. /* Code Mapping */
  11. struct vm_special_mapping *cm;
  12. };
  13. // include/linux/mm_types.h
  14. struct vm_special_mapping {
  15. const char *name; /* The name, e.g. "[vdso]". */
  16. /*
  17. * If .fault is not provided, this points to a
  18. * NULL-terminated array of pages that back the special mapping.
  19. *
  20. * This must not be NULL unless .fault is provided.
  21. */
  22. struct page **pages;
  23. /*
  24. * If non-NULL, then this is called to resolve page faults
  25. * on the special mapping. If used, .pages is not checked.
  26. */
  27. vm_fault_t (*fault)(const struct vm_special_mapping *sm,
  28. struct vm_area_struct *vma,
  29. struct vm_fault *vmf);
  30. int (*mremap)(const struct vm_special_mapping *sm,
  31. struct vm_area_struct *new_vma);
  32. };

vDSO 内核中代码部分地址初始化的时候,vdso_code_startvdso_code_end 分别赋值了 vdso_startvdso_end。它们声明成了外部引用,实际上 vdso_startvdso_end 这两个变量定义在本文 共享库集成到内核 章节中提到的 vdso.S 文件中,它们表示了 vDSO 代码段的起始位置和结束位置。

vDSO 内核中数据部分的定义就是 vdso_data。它直接定义在内核代码中。

  1. // arch/riscv/kernel/vdso.c
  2. static union {
  3. struct vdso_data data;
  4. u8 page[PAGE_SIZE];
  5. } vdso_data_store __page_aligned_data;
  6. struct vdso_data *vdso_data = &vdso_data_store.data;
  7. static struct __vdso_info vdso_info __ro_after_init = {
  8. .name = "vdso",
  9. .vdso_code_start = vdso_start,
  10. .vdso_code_end = vdso_end,
  11. };

dmcm 分别表示代码和数据部分的 vm_special_mapping(虚拟内存特殊映射对象)。

cm 使用定义在内核的静态变量 rv_vdso_maps 进行初始化,其中比较重要的 pages 内存页成员在 __vdso_init 函数中进行初始化,申请代码部分所占页数量的内存页,并建立虚拟内存和物理内存页映射。

  1. // arch/riscv/kernel/vdso.c
  2. static struct vm_special_mapping rv_vdso_maps[] __ro_after_init = {
  3. [RV_VDSO_MAP_VVAR] = {
  4. .name = "[vvar]",
  5. .fault = vvar_fault,
  6. },
  7. [RV_VDSO_MAP_VDSO] = {
  8. .name = "[vdso]",
  9. .mremap = vdso_mremap,
  10. },
  11. };
  12. static int __init vdso_init(void)
  13. {
  14. vdso_info.dm = &rv_vdso_maps[RV_VDSO_MAP_VVAR];
  15. vdso_info.cm = &rv_vdso_maps[RV_VDSO_MAP_VDSO];
  16. return __vdso_init();
  17. }
  18. static int __init __vdso_init(void)
  19. {
  20. unsigned int i;
  21. struct page **vdso_pagelist;
  22. unsigned long pfn;
  23. if (memcmp(vdso_info.vdso_code_start, "\177ELF", 4)) {
  24. pr_err("vDSO is not a valid ELF object!\n");
  25. return -EINVAL;
  26. }
  27. vdso_info.vdso_pages = (
  28. vdso_info.vdso_code_end -
  29. vdso_info.vdso_code_start) >>
  30. PAGE_SHIFT;
  31. vdso_pagelist = kcalloc(vdso_info.vdso_pages,
  32. sizeof(struct page *),
  33. GFP_KERNEL);
  34. if (vdso_pagelist == NULL)
  35. return -ENOMEM;
  36. /* Grab the vDSO code pages. */
  37. pfn = sym_to_pfn(vdso_info.vdso_code_start);
  38. for (i = 0; i < vdso_info.vdso_pages; i++)
  39. vdso_pagelist[i] = pfn_to_page(pfn + i);
  40. vdso_info.cm->pages = vdso_pagelist;
  41. return 0;
  42. }

dm 的初始化在 vvar_fault 函数中实现。vvar_faultdm 缺页中断的回调函数。从代码中可以看出,实际映射的对象是上文中提到的内核定义的数据部分对象 vdso_data

  1. // arch/riscv/kernel/vdso.c
  2. static vm_fault_t vvar_fault(const struct vm_special_mapping *sm,
  3. struct vm_area_struct *vma, struct vm_fault *vmf)
  4. {
  5. ...
  6. pfn = sym_to_pfn(vdso_data);
  7. ...
  8. }

3.2 用户进程启动时初始化

接下来是在用户进程启动时才会执行的初始化过程,主要的目的是初始化加速系统调用的几个函数指针,以达到用户程序调用 glibc 中支持 vDSO 函数时能够正确跳转到 vDSO 相应的代码地址。

但是程序启动过程有些复杂,涉及到 vDSO 相关的大致可以分为三个阶段:

  1. 在内核态执行 execve 系统调用,将 vDSO 代码和数据映射到用户内存,并将代码地址记录在用户栈内存中
  2. 在用户态执行 dynamic linker,找到 vDSO 代码地址并加载,初始化 vDSO 函数的地址
  3. 在用户态执行 libc init,针对静态链接的程序进行初始化 vDSO 函数的地址

vdso_setup

图片来自 Unified_vDSO_LPC_2020

3.2.1 execve

在 Linux 系统中,运行一个程序依赖 forkexecve 这两个系统调用。fork 会创建一个新进程并复制父进程的数据到新进程中;而 execve 则是解析 ELF 文件,将其载入内存,并修改进程的堆栈数据来准备运行环境。而 vDSO 的初始化功能也是在 execve 中完成的。

  1. // fs/exec.c
  2. SYSCALL_DEFINE3(execve,
  3. const char __user *, filename,
  4. const char __user *const __user *, argv,
  5. const char __user *const __user *, envp)
  6. {
  7. return do_execve(getname(filename), argv, envp);
  8. }

SYSCALL_DEFINE3 是定义系统调用的宏,详情可以参考本系列之前的文章 RISC-V Syscall 系列 2:Syscall 过程分析

execve 会先经过如下函数调用到达 load_elf_binary

  1. do_execve
  2. do_execveat_common:初始化环境和启动参数信息
  3. bprm_execve:打开文件,使调度器负载均衡等
  4. exec_binprm
  5. search_binary_handler:寻找文件格式对应的解析模块
  6. fmt->load_binary():调用格式对应的载入函数

而对于 ELF 文件来说,load_binary 就是 load_elf_binary,下面是 ELF 文件格式载入函数的初始化代码和 load_elf_binary 函数代码。

  1. // fs/binfmt_elf.c
  2. static struct linux_binfmt elf_format = {
  3. .module = THIS_MODULE,
  4. .load_binary = load_elf_binary,
  5. .load_shlib = load_elf_library,
  6. .core_dump = elf_core_dump,
  7. .min_coredump = ELF_EXEC_PAGESIZE,
  8. };
  9. static int load_elf_binary(struct linux_binprm *bprm)
  10. {
  11. ...
  12. retval = ARCH_SETUP_ADDITIONAL_PAGES(bprm, elf_ex, !!interpreter);
  13. ...
  14. retval = create_elf_tables(bprm, elf_ex,load_addr, interp_load_addr, e_entry);
  15. ...
  16. }
  17. // include/linux/elf.h
  18. #define ARCH_SETUP_ADDITIONAL_PAGES(bprm, ex, interpreter) arch_setup_additional_pages(bprm, interpreter)

load_elf_binary 函数内容比较庞大,实现了加载 ELF 文件的核心逻辑。其中跟 vDSO 初始化相关的有如下两个函数:

  1. arch_setup_additional_pages
  2. create_elf_tables
3.2.1.1 arch_setup_additional_pages

arch_setup_additional_pages 是处理器架构相关的函数,里面主要调用了 __setup_additional_pages,它的主要功能是将 vDSO 的代码部分 (text) 和数据部分(vvar)载入用户内存。具体代码如下:

  1. // arch/riscv/kernel/vdso.c
  2. enum vvar_pages {
  3. VVAR_DATA_PAGE_OFFSET,
  4. VVAR_TIMENS_PAGE_OFFSET,
  5. VVAR_NR_PAGES,
  6. };
  7. #define VVAR_SIZE (VVAR_NR_PAGES << PAGE_SHIFT)
  8. int arch_setup_additional_pages(struct linux_binprm *bprm, int uses_interp)
  9. {
  10. ...
  11. ret = __setup_additional_pages(mm, bprm, uses_interp);
  12. ...
  13. }
  14. static int __setup_additional_pages(struct mm_struct *mm, struct linux_binprm *bprm, int uses_interp)
  15. {
  16. unsigned long vdso_base, vdso_text_len, vdso_mapping_len;
  17. void *ret;
  18. BUILD_BUG_ON(VVAR_NR_PAGES != __VVAR_PAGES);
  19. vdso_text_len = vdso_info.vdso_pages << PAGE_SHIFT;
  20. /* Be sure to map the data page */
  21. vdso_mapping_len = vdso_text_len + VVAR_SIZE;
  22. vdso_base = get_unmapped_area(NULL, 0, vdso_mapping_len, 0, 0);
  23. if (IS_ERR_VALUE(vdso_base)) {
  24. ret = ERR_PTR(vdso_base);
  25. goto up_fail;
  26. }
  27. ret = _install_special_mapping(mm, vdso_base, VVAR_SIZE,
  28. (VM_READ | VM_MAYREAD | VM_PFNMAP), vdso_info.dm);
  29. if (IS_ERR(ret))
  30. goto up_fail;
  31. vdso_base += VVAR_SIZE;
  32. mm->context.vdso = (void *)vdso_base;
  33. ret =
  34. _install_special_mapping(mm, vdso_base, vdso_text_len,
  35. (VM_READ | VM_EXEC | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC),
  36. vdso_info.cm);
  37. if (IS_ERR(ret))
  38. goto up_fail;
  39. return 0;
  40. up_fail:
  41. mm->context.vdso = NULL;
  42. return PTR_ERR(ret);
  43. }

首先计算 vDSO 映射需要占用的内存空间大小 vdso_mapping_len。它由 vdso_text_len 代码部分和 VVAR_SIZE 数据部分相加得到。vdso_text_len 很显然可以由 vdso_info.vdso_pages 代码段所占内存页数乘以内存页大小计算得到,而代码中 vdso_info.vdso_pages << PAGE_SHIFT 的计算可以达到相同的效果。而通过查看 VVAR_SIZE 的定义可知,目前内核给 vDSO 数据部分分配了两个内存页。

然后调用 get_unmapped_area 内核接口在当前进程的用户空间中获取一个为映射区间的起始地址,其中第三个参数表示获取的为映射空间的大小。

然后调用 _install_special_mapping 将 vDSO 的数据部分映射到用户内存中。这里的第四个参数可以设置内存页的访问标记,这里可以简单理解为用户程序对 vDSO 的数据部分是只读的,具体分别设置了三个值:

  • VM_READ:内存页可读取
  • VM_MAYREAD:VM_READ 标志可被设置
  • VM_PFNMAP:Page-ranges managed without “struct page”, just pure PFN

最后再次调用 _install_special_mapping 将 vDSO 的代码部分映射到用户内存中,位置紧接着数据部分。与数据页标记不同,用户程序对代码部分是可读可执行的,具体设置了五个值:

  • VM_READ:内存页可读取
  • VM_EXEC:内存页可执行
  • VM_MAYREAD:VM_READ 标志可被设置
  • VM_MAYWRITE:VM_WRITE 标志可被设置
  • VM_MAYEXEC:VM_EXEC 标志可被设置
3.2.1.2 create_elf_tables

create_elf_tables 主要负责添加需要的信息到应用程序用户栈中,包括 auxiliary vector(辅助向量),argv(命令行参数),environ(环境变量)。而 vDSO 的地址信息就写入了 auxiliary vector

auxiliary vector 是一种用户态和内核态之间通信的一种机制。本质上来说,它是由一系列键值对组成的一个列表。内核在加载应用程序时会将其存储在用户栈上。可以通过在运行程序时添加 LD_SHOW_AUXV 环境变量来查看列表的具体内容,其中 AT_SYSINFO_EHDR 对应的就是 vDSO 代码部分的起始地址。示例如下:

  1. $ LD_SHOW_AUXV=1 sleep 1
  2. AT_SYSINFO_EHDR: 0x7fff9d185000
  3. AT_HWCAP: bfebfbff
  4. AT_PAGESZ: 4096
  5. AT_CLKTCK: 100
  6. AT_PHDR: 0x55c64e14c040
  7. AT_PHENT: 56
  8. AT_PHNUM: 13
  9. AT_BASE: 0x7fd3399b8000
  10. AT_FLAGS: 0x0
  11. AT_ENTRY: 0x55c64e14e850
  12. AT_UID: 1000
  13. AT_EUID: 1000
  14. AT_GID: 1000
  15. AT_EGID: 1000
  16. AT_SECURE: 0
  17. AT_RANDOM: 0x7fff9d111309
  18. AT_HWCAP2: 0x2
  19. AT_EXECFN: /usr/bin/sleep
  20. AT_PLATFORM: x86_64

create_elf_tables 的具体代码如下:

  1. // fs/binfmt_elf.c
  2. static int create_elf_tables(struct linux_binprm *bprm, const struct elfhdr *exec, unsigned long load_addr, unsigned long interp_load_addr,unsigned long e_entry)
  3. {
  4. ...
  5. elf_info = (elf_addr_t *)mm->saved_auxv;
  6. #define NEW_AUX_ENT(id, val) \
  7. do { \
  8. *elf_info++ = id; \
  9. *elf_info++ = val; \
  10. } while (0)
  11. ...
  12. ARCH_DLINFO;
  13. ...
  14. }

NEW_AUX_ENT 是一个用来给 auxiliary vector 添加健值对的宏,其中 elf_info 的实际是指向 unsigned long saved_auxv[AT_VECTOR_SIZE] 这样一个存储在 mm 中的一个数组,每两个元素组成一个键值对。

ARCH_DLINFO 是一个初始化多个键值对的宏定义,展开如下:

  1. // arch/riscv/include/asm/elf.h
  2. #define ARCH_DLINFO \
  3. do { \
  4. NEW_AUX_ENT(AT_SYSINFO_EHDR, \
  5. (elf_addr_t)current->mm->context.vdso); \
  6. ...
  7. } while (0)

可以看出,这里将 AT_SYSINFO_EHDR 对应的值赋值成了 mm->context.vdso,而根据上文中列出的 __setup_additional_pages 函数代码,可以看出实际上赋值的就是 vDSO 代码部分的起始地址。

3.2.1.3 start_thread
  1. // fs/binfmt_elf.c
  2. #define START_THREAD(elf_ex, regs, elf_entry, start_stack) start_thread(regs, elf_entry, start_stack)
  3. START_THREAD(elf_ex, regs, elf_entry, bprm->p);
  4. // arch/riscv/kernel/process.c
  5. void start_thread(struct pt_regs *regs, unsigned long pc,
  6. unsigned long sp)
  7. {
  8. ...
  9. regs->epc = pc;
  10. regs->sp = sp;
  11. }

最后,start_thread 会将 epc 和 sp 改成新的地址,使得 execve 系统调用返回到用户空间时就能进入新的程序入口。

  1. // fs/binfmt_elf.c
  2. static int load_elf_binary(struct linux_binprm *bprm)
  3. {
  4. ...
  5. e_entry = elf_ex->e_entry + load_bias;
  6. ...
  7. if (interpreter) {
  8. elf_entry = load_elf_interp(interp_elf_ex,interpreter,load_bias, interp_elf_phdata,&arch_state);
  9. ...
  10. } else {
  11. elf_entry = e_entry;
  12. ...
  13. }
  14. ...
  15. }

根据上述代码所示,程序入口 elf_entry 的取值分以下两种情况:

  • 需要载入解释器(有动态链接的依赖库):就通过 load_elf_interp 载入解释器,并返回值(解释器的入口地址)赋值给 elf_entry
  • 不需要载入解释器(静态链接依赖库):elf_entry 取值为当前 ELF 本身的入口地址

3.2.2 dynamic linker

当应用程序有依赖共享库时,程序启动时会进入 dynamic linker

dynamic linker 位于 glibc 的代码中,执行时会经过如下函数调用到达 dl_main

  • _dl_start (elf/rtld.c)
  • _dl_start_final
  • _dl_sysdep_start
  • dl_main

dl_main 函数中跟 vDSO 初始化相关的有 setup_vdsosetup_vdso_pointers 两个函数调用。

setup_vdso 会初始化 vDSO 相关的数据结构,其中就包含 _dl_sysinfo_map,它在后面的 setup_vdso_pointers 中会用到。

  1. // elf/setup-vdso.h
  2. static inline void __attribute__ ((always_inline)) setup_vdso (struct link_map *main_map __attribute__ ((unused)), struct link_map ***first_preload __attribute__ ((unused)))
  3. {
  4. ...
  5. l->l_phdr = ((const void *) GLRO(dl_sysinfo_dso) + GLRO(dl_sysinfo_dso)->e_phoff);
  6. l->l_phnum = GLRO(dl_sysinfo_dso)->e_phnum;
  7. ...
  8. GLRO(dl_sysinfo_map) = l;
  9. ...
  10. }

setup_vdso_pointers 用来初始化 vDSO 相关函数指针。

  1. // sysdeps/unix/sysv/linux/dl-vdso-setup.h
  2. /* Initialize the VDSO functions pointers. */
  3. static inline void __attribute__ ((always_inline))
  4. setup_vdso_pointers (void)
  5. {
  6. ...
  7. #ifdef HAVE_CLOCK_GETTIME64_VSYSCALL
  8. GLRO(dl_vdso_clock_gettime64) = dl_vdso_vsym (HAVE_CLOCK_GETTIME64_VSYSCALL);
  9. #endif
  10. #ifdef HAVE_GETTIMEOFDAY_VSYSCALL
  11. GLRO(dl_vdso_gettimeofday) = dl_vdso_vsym (HAVE_GETTIMEOFDAY_VSYSCALL);
  12. #endif
  13. #ifdef HAVE_CLOCK_GETRES64_VSYSCALL
  14. GLRO(dl_vdso_clock_getres_time64) = dl_vdso_vsym (HAVE_CLOCK_GETRES64_VSYSCALL);
  15. #endif
  16. }
  17. // string/test-string.h
  18. #define GLRO(x) _##x
  19. // sysdeps/unix/sysv/linux/riscv/sysdep.h
  20. /* List of system calls which are supported as vsyscalls only
  21. for RV64. */

GLRO 将变量名前加上下划线(例如 GLRO(dl_vdso_gettimeofday) 表示 _dl_vdso_gettimeofday),其变量类型是函数指针,具体定义如下:

  1. // sysdeps/unix/sysv/linux/dl-vdso-setup.c
  2. PROCINFO_CLASS int (*_dl_vdso_clock_gettime64) (clockid_t,
  3. struct __timespec64 *) RELRO;
  4. #endif
  5. PROCINFO_CLASS int (*_dl_vdso_gettimeofday) (struct timeval *, void *) RELRO;
  6. #endif
  7. PROCINFO_CLASS int (*_dl_vdso_clock_getres_time64) (clockid_t,
  8. struct __timespec64 *) RELRO;

dl_vdso_vsym 会根据 _dl_sysinfo_map 这个对象找到指定函数名在 vDSO 中的地址并返回。

  1. // sysdeps/unix/sysv/linux/dl-vdso.h
  2. /* Functions for resolving symbols in the VDSO link map. */
  3. static inline void *
  4. dl_vdso_vsym (const char *name)
  5. {
  6. struct link_map *map = GLRO (dl_sysinfo_map);
  7. if (map == NULL)
  8. return NULL;
  9. /* Use a WEAK REF so we don't error out if the symbol is not found. */
  10. ElfW (Sym) wsym = { 0 };
  11. wsym.st_info = (unsigned char) ELFW (ST_INFO (STB_WEAK, STT_NOTYPE));
  12. struct r_found_version rfv = { VDSO_NAME, VDSO_HASH, 1, NULL };
  13. /* Search the scope of the vdso map. */
  14. const ElfW (Sym) *ref = &wsym;
  15. lookup_t result = GLRO (dl_lookup_symbol_x) (name, map, &ref,
  16. map->l_local_scope,
  17. &rfv, 0, 0, NULL);
  18. return ref != NULL ? DL_SYMBOL_ADDRESS (result, ref) : NULL;
  19. }
  20. // include/link.h
  21. /* Structure describing a loaded shared object. The `l_next' and `l_prev'
  22. members form a chain of all the shared objects loaded at startup.
  23. These data structures exist in space used by the run-time dynamic linker;
  24. modifying them may have disastrous results.
  25. This data structure might change in future, if necessary. User-level
  26. programs must avoid defining objects of this type. */
  27. struct link_map {...}

根据上面的 setup_vdso 函数代码可以看出,我们根据 _dl_sysinfo_dso 结构的信息对 _dl_sysinfo_map 结构进行初始化。

_dl_sysinfo_dso 的初始化函数由上至下依次调用路径如下:

  • _dl_start_final(elf/rtld.c)
  • _dl_sysdep_start(sysdeps/unix/sysv/linux/dl-sysdep.c)
  • _dl_sysdep_parse_arguments(sysdeps/unix/sysv/linux/dl-sysdep.c)
  • _dl_parse_auxv(sysdeps/unix/sysv/linux/dl-parse-auxv.h)

_dl_sysdep_parse_arguments 函数中,找到辅助向量的位置并作为参数传递给 _dl_parse_auxv

auxvec memory layout (图片源自 LWN.net)

辅助向量在内存中的位置如上图所示,所以只要从栈顶开始,越过 argv(命令行参数)和 environ(环境变量)就能找到辅助向量的地址。

  1. // sysdeps/unix/sysv/linux/dl-sysdep.c
  2. static void _dl_sysdep_parse_arguments (void **start_argptr, struct dl_main_arguments *args)
  3. {
  4. _dl_argc = (intptr_t) *start_argptr;
  5. _dl_argv = (char **) (start_argptr + 1); /* Necessary aliasing violation. */
  6. _environ = _dl_argv + _dl_argc + 1;
  7. for (char **tmp = _environ; ; ++tmp)
  8. if (*tmp == NULL)
  9. {
  10. /* Another necessary aliasing violation. */
  11. GLRO(dl_auxv) = (ElfW(auxv_t) *) (tmp + 1);
  12. break;
  13. }
  14. dl_parse_auxv_t auxv_values = { 0, };
  15. _dl_parse_auxv (GLRO(dl_auxv), auxv_values);
  16. args->phdr = (const ElfW(Phdr) *) auxv_values[AT_PHDR];
  17. args->phnum = auxv_values[AT_PHNUM];
  18. args->user_entry = auxv_values[AT_ENTRY];
  19. }

_dl_parse_auxv 函数将辅助向量的信息存储到 AUXV_VALUES 中,并初始化 GLRO 变量,这其中就包括 _dl_sysinfo_dso

  1. // sysdeps/unix/sysv/linux/dl-parse-auxv.h
  2. typedef ElfW(Addr) dl_parse_auxv_t[AT_MINSIGSTKSZ + 1];
  3. /* Copy the auxiliary vector into AUXV_VALUES and set up GLRO
  4. variables. */
  5. static inline void _dl_parse_auxv (ElfW(auxv_t) *av, dl_parse_auxv_t auxv_values)
  6. {
  7. ...
  8. for (; av->a_type != AT_NULL; av++)
  9. if (av->a_type <= AT_MINSIGSTKSZ)
  10. auxv_values[av->a_type] = av->a_un.a_val;
  11. GLRO(dl_sysinfo_dso) = (void *) auxv_values[AT_SYSINFO_EHDR];
  12. ...
  13. }

setup_vdso_pointers 函数里初始化的函数指针是 _dl_vdso_gettimeofday,它跟我们使用的 gettimeofday 又有什么关系?

  1. // sysdeps/unix/sysv/linux/gettimeofday.c
  2. int __gettimeofday (struct timeval *restrict tv, void *restrict tz)
  3. {
  4. if (__glibc_unlikely (tz != 0))
  5. memset (tz, 0, sizeof *tz);
  6. return INLINE_VSYSCALL (gettimeofday, 2, tv, tz);
  7. }
  8. weak_alias (__gettimeofday, gettimeofday)

gettimeofday 实际是 __gettimeofday 的别名,而 __gettimeofday 内部实际调用的是 INLINE_VSYSCALL

  1. // sysdeps/unix/sysv/linux/sysdep-vdso.h
  2. funcptr (args)
  3. #define INLINE_VSYSCALL(name, nr, args...) \
  4. ({ \
  5. __label__ out; \
  6. __label__ iserr; \
  7. long int sc_ret; \
  8. \
  9. __typeof (GLRO(dl_vdso_##name)) vdsop = GLRO(dl_vdso_##name); \
  10. if (vdsop != NULL) \
  11. { \
  12. sc_ret = INTERNAL_VSYSCALL_CALL (vdsop, nr, ##args); \
  13. if (!INTERNAL_SYSCALL_ERROR_P (sc_ret)) \
  14. goto out; \
  15. if (INTERNAL_SYSCALL_ERRNO (sc_ret) != ENOSYS) \
  16. goto iserr; \
  17. } \
  18. \
  19. sc_ret = INTERNAL_SYSCALL_CALL (name, ##args); \
  20. if (INTERNAL_SYSCALL_ERROR_P (sc_ret)) \
  21. { \
  22. iserr: \
  23. __set_errno (INTERNAL_SYSCALL_ERRNO (sc_ret)); \
  24. sc_ret = -1L; \
  25. } \
  26. out: \
  27. sc_ret; \
  28. })

从上面的宏定义可以看出,INLINE_VSYSCALL (gettimeofday, 2, tv, tz) 实际上是执行 _dl_vdso_gettimeofday(tv, tz)。而 _dl_vdso_gettimeofday 就是 setup_vdso_pointers 里初始化的函数指针。

3.2.3 libc init

而对那些静态链接的程序来说,虽然不会执行上述 dynamic linker,但会在应用程序开始部分进行类似的初始化过程。而初始化的关键在于,从辅助向量中找到 vDSO 地址并初始化对应的函数指针。

大致的初始化过程如下:

  • ENTRY_POINT / _start(sysdeps/riscv/start.S)
    • __libc_start_main@plt
      • LIBC_START_MAIN / __libc_start_main_impl(csu/libc-start.c)
        • _dl_aux_init(elf/dl-support.c)
          • _dl_parse_auxv(sysdeps/unix/sysv/linux/dl-parse_auxv.h)
        • __libc_init_first(csu/init-first.c)
          • _dl_non_dynamic_init(elf/dl-support.c)
            • setup_vdso
            • setup_vdso_pointers

从上面的调用过程可以看出,最终也是通过执行 _dl_parse_auxvsetup_vdsosetup_vdso_pointers 这几个关键函数进行 vDSO 的初始化。

至此 vDSO 的初始化部分就完成了。先小结一下,经过上述过程的初始化,目前准备就绪的有:

  • vDSO 的代码和数据均在用户内存中完成映射
  • 用户内存中的加速系统调用的函数指针已经指向 vDSO
  • 内核中可以使用 vdso_data 对象访问 vDSO 数据部分
  • 用户态中可以使用 _vdso_data 对象访问 vDSO 数据部分(这部分会在下文中阐述)

4 vDSO Read & Write

vDSO 初始化完成后,就可以对其数据部分进行读写操作了。

vdso implement

图片来自 Unified_vDSO_LPC_2020

4.1 read

当用户程序需要读取系统时间的时候,一般会调用 glibc 中提供的 gettimeofday 方法,该方法会通过上一节中设置好的相关变量,找到 vDSO 中对应函数 __vdso_gettimeofday 并执行调用。

  1. // arch/riscv/kernel/vdso/vgettimeofday.c
  2. int __vdso_gettimeofday(struct __kernel_old_timeval *tv, struct timezone *tz)
  3. {
  4. return __cvdso_gettimeofday(tv, tz);
  5. }
  6. // lib/vdso/gettimeofday.c
  7. static __maybe_unused int
  8. __cvdso_gettimeofday(struct __kernel_old_timeval *tv, struct timezone *tz)
  9. {
  10. return __cvdso_gettimeofday_data(__arch_get_vdso_data(), tv, tz);
  11. }

__vdso_gettimeofday 函数直接调用了 __cvdso_gettimeofday__cvdso_gettimeofday 里面涉及两个函数:

  • __arch_get_vdso_data:获取 vDSO 数据部分地址
  • __cvdso_gettimeofday_data:获取系统时间具体逻辑

4.1.1 __arch_get_vdso_data

  1. // arch/riscv/include/asm/vdso/gettimeofday.h
  2. static __always_inline const struct vdso_data *__arch_get_vdso_data(void)
  3. {
  4. return _vdso_data;
  5. }

__arch_get_vdso_data 里面直接返回 _vdso_data 变量,说明该变量存储的是用户态中 vDSO 数据部分内存地址。那它是如何初始化的呢?

  1. // arch/riscv/kernel/vdso/vdso.lds.S
  2. PROVIDE(_vdso_data = . - __VVAR_PAGES * PAGE_SIZE);
  1. // arch/riscv/include/asm/vdso.h
  2. #define __VVAR_PAGES 2
  3. // arch/riscv/include/asm/page.h
  4. #define PAGE_SHIFT (12)
  5. #define PAGE_SIZE (_AC(1, UL) << PAGE_SHIFT)

首先,在本文 Build 章节中提到,vdso.lds.S 用于生成 vdso.so.dbg 共享库文件,这个链接脚本里对 _vdso_data 进行了初始化,具体赋值成了 - 2 * 4096。这个值可以通过查看 vdso.so.dbg 库文件进行验证:

  1. $ readelf -s /labs/linux-lab/build/riscv64/virt/linux/v5.17/arch/riscv/kernel/vdso/vdso.so.dbg | grep _vdso_data
  2. 19: ffffffffffffe000 0 NOTYPE LOCAL DEFAULT 1 _vdso_data

我们知道共享库加载进内存后需要进行地址重定位,操作系统通过上文提到的 setup_vdso 对 vDSO 执行重定位。

  1. // elf/setup-vdso.h
  2. static inline void __attribute__ ((always_inline))
  3. setup_vdso (struct link_map *main_map __attribute__ ((unused)), struct link_map ***first_preload __attribute__ ((unused)))
  4. {
  5. ...
  6. l->l_map_start = (ElfW(Addr)) GLRO(dl_sysinfo_dso);
  7. ...
  8. }

从上面的代码来看,重定位的起始地址被赋值成了 _dl_sysinfo_dso。而根据本文之前的描述,_dl_sysinfo_dso 在用户进程启动时会初始化为 vDSO 代码部分的起始地址,所以重定向后的 _vdso_data = _dl_sysinfo_dso - __VVAR_PAGES * PAGE_SIZE。而 vDSO 数据部分正好位于代码部分之前,所以 _vdso_data 就被初始化为 vDSO 数据部分起始地址。

4.1.2 __cvdso_gettimeofday_data

__cvdso_gettimeofday_data 函数实现逻辑主要分两部分:

  • 优先调用 do_hres 函数从 _vdso_data 中获取系统时间
  • 如果 do_hres 返回失败,则调用 gettimeofday_fallback 执行系统调用
  1. // lib/vdso/gettimeofday.c
  2. static __maybe_unused int
  3. __cvdso_gettimeofday_data(const struct vdso_data *vd,
  4. struct __kernel_old_timeval *tv, struct timezone *tz)
  5. {
  6. if (likely(tv != NULL)) {
  7. struct __kernel_timespec ts;
  8. if (do_hres(&vd[CS_HRES_COARSE], CLOCK_REALTIME, &ts))
  9. return gettimeofday_fallback(tv, tz);
  10. tv->tv_sec = ts.tv_sec;
  11. tv->tv_usec = (u32)ts.tv_nsec / NSEC_PER_USEC;
  12. }
  13. ...
  14. }
  15. static __always_inline int do_hres(const struct vdso_data *vd, clockid_t clk, struct __kernel_timespec *ts)
  16. {
  17. const struct vdso_timestamp *vdso_ts = &vd->basetime[clk];
  18. ...
  19. ns = vdso_ts->nsec;
  20. sec = vdso_ts->sec;
  21. ...
  22. ts->tv_sec = sec + __iter_div_u64_rem(ns, NSEC_PER_SEC, &ns);
  23. ts->tv_nsec = ns;
  24. return 0;
  25. }
  1. // arch/riscv/include/asm/vdso/gettimeofday.h
  2. static __always_inline
  3. int gettimeofday_fallback(struct __kernel_old_timeval *_tv,
  4. struct timezone *_tz)
  5. {
  6. register struct __kernel_old_timeval *tv asm("a0") = _tv;
  7. register struct timezone *tz asm("a1") = _tz;
  8. register long ret asm("a0");
  9. register long nr asm("a7") = __NR_gettimeofday;
  10. asm volatile ("ecall\n"
  11. : "=r" (ret)
  12. : "r"(tv), "r"(tz), "r"(nr)
  13. : "memory");
  14. return ret;
  15. }

4.2 write

vDSO 数据部分的更新按照触发的方式可以分为以下两种情况:

  • 时钟中断时更新(timekeeping_update)
  • 应用程序主动触发(settimeofday)

4.2.1 timekeeping_update

当发生时钟中断时,中断处理程序会调用 timekeeping_update,进一步调用 update_vsyscall 来更新 vDSO 中系统时间信息。

  1. // kernel/time/timekeeping.c
  2. static void timekeeping_update(struct timekeeper *tk, unsigned int action)
  3. {
  4. ...
  5. update_vsyscall(tk);
  6. ...
  7. }

update_vsyscall 函数里通过调用 __arch_get_k_vdso_data 获取内核中 vDSO 数据对象。

  1. // kernel/time/vsyscall.c
  2. void update_vsyscall(struct timekeeper *tk)
  3. {
  4. struct vdso_data *vdata = __arch_get_k_vdso_data();
  5. struct vdso_timestamp *vdso_ts;
  6. s32 clock_mode;
  7. u64 nsec;
  8. /* copy vsyscall data */
  9. vdso_write_begin(vdata);
  10. clock_mode = tk->tkr_mono.clock->vdso_clock_mode;
  11. vdata[CS_HRES_COARSE].clock_mode = clock_mode;
  12. vdata[CS_RAW].clock_mode = clock_mode;
  13. /* CLOCK_REALTIME also required for time() */
  14. vdso_ts = &vdata[CS_HRES_COARSE].basetime[CLOCK_REALTIME];
  15. vdso_ts->sec = tk->xtime_sec;
  16. vdso_ts->nsec = tk->tkr_mono.xtime_nsec;
  17. /* CLOCK_REALTIME_COARSE */
  18. vdso_ts = &vdata[CS_HRES_COARSE].basetime[CLOCK_REALTIME_COARSE];
  19. vdso_ts->sec = tk->xtime_sec;
  20. vdso_ts->nsec = tk->tkr_mono.xtime_nsec >> tk->tkr_mono.shift;
  21. /* CLOCK_MONOTONIC_COARSE */
  22. vdso_ts = &vdata[CS_HRES_COARSE].basetime[CLOCK_MONOTONIC_COARSE];
  23. vdso_ts->sec = tk->xtime_sec + tk->wall_to_monotonic.tv_sec;
  24. nsec = tk->tkr_mono.xtime_nsec >> tk->tkr_mono.shift;
  25. nsec = nsec + tk->wall_to_monotonic.tv_nsec;
  26. vdso_ts->sec += __iter_div_u64_rem(nsec, NSEC_PER_SEC, &vdso_ts->nsec);
  27. /*
  28. * Read without the seqlock held by clock_getres().
  29. * Note: No need to have a second copy.
  30. */
  31. WRITE_ONCE(vdata[CS_HRES_COARSE].hrtimer_res, hrtimer_resolution);
  32. /*
  33. * If the current clocksource is not VDSO capable, then spare the
  34. * update of the high resolution parts.
  35. */
  36. if (clock_mode != VDSO_CLOCKMODE_NONE)
  37. update_vdso_data(vdata, tk);
  38. __arch_update_vsyscall(vdata, tk);
  39. vdso_write_end(vdata);
  40. __arch_sync_vdso_data(vdata);
  41. }

__arch_get_k_vdso_data 实际返回的是 vdso_data 对象。

  1. // arch/riscv/include/asm/vdso/vsyscall.h
  2. /*
  3. * Update the vDSO data page to keep in sync with kernel timekeeping.
  4. */
  5. static __always_inline struct vdso_data *__riscv_get_k_vdso_data(void)
  6. {
  7. return vdso_data;
  8. }
  9. #define __arch_get_k_vdso_data __riscv_get_k_vdso_data
  10. // arch/riscv/kernel/vdso.c
  11. static union {
  12. struct vdso_data data;
  13. u8 page[PAGE_SIZE];
  14. } vdso_data_store __page_aligned_data;
  15. struct vdso_data *vdso_data = &vdso_data_store.data;

4.2.2 settimeofday

settimeofday 系统调用执行过程中会调用 update_vsyscall_tz 更新 vDSO 的数据。

  1. // kernel/time/vsyscall.c
  2. void update_vsyscall_tz(void)
  3. {
  4. struct vdso_data *vdata = __arch_get_k_vdso_data();
  5. vdata[CS_HRES_COARSE].tz_minuteswest = sys_tz.tz_minuteswest;
  6. vdata[CS_HRES_COARSE].tz_dsttime = sys_tz.tz_dsttime;
  7. __arch_sync_vdso_data(vdata);
  8. }

update_vsyscall_tzupdate_vsyscall 类似,都是通过调用 __arch_get_k_vdso_data 获取内核中 vDSO 数据对象并进行更新。

5 总结

本文依据 Linux 和 glibc 源代码,先从编译期解释了 vDSO 共享库如何集成到 Linux 操作系统内核,然后从运行期解释了 vDSO 相关数据结构的初始化,最后分析了用户程序读取 vDSO 数据和内核更新数据的过程。希望能帮助读者理解 vDSO 技术的实现原理。

6 参考资料



Read Album:

Read Related:

Read Latest: