Linux 内核系统调用提速机制解析:vsyscall 与 vDSO(linux-insides-zh 系统调用系列第三节) 文档教程操作系统【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/gh_mirrors/li/linux-insides-zh点击查看免费下载本文基于 linux-insides-zh 仓库中《Linux 内核系统调用》章节的第三节整理扩充。该节从 上一节 讨论的用户空间如何发起系统调用、内核如何经系统调用表处理出发深入讲解两个与系统调用形态相似但旨在绕过传统系统调用开销的机制——vsyscall虚拟系统调用与vDSO虚拟动态共享对象。读完本文你将掌握vsyscall 固定地址映射的初始化路径、vsyscall内核命令行参数的三种模式及其安全考量、vDSO 镜像的生成与按进程动态映射的原理以及两者在x86_64平台上的本质差异。为什么需要加速的系统调用常规系统调用是昂贵的用户程序执行syscall指令后处理器需要中断当前任务、切换到内核模式上下文由内核经 系统调用表 分发到具体处理函数处理完毕后再跳回用户空间。这一过程伴随频繁的上下文切换context switch。vsyscall与vDSO的设计目标就是让某些高频且逻辑简单的系统调用如读取时间戳gettimeofday、time、getcpu在用户空间直接执行从而完全避免上下文切换。两者的核心思路都是内核把包含系统调用实现的内存页映射进用户进程用户程序不经过内核即可完成调用。但两者在映射形态上有根本区别这正是本节内容的重点。vsyscall最古老的虚拟系统调用固定地址的用户空间内核页vsyscallvirtual system call是最早出现的加速机制工作原理非常朴素Linux 内核在用户空间映射一个包含若干变量及若干系统调用实现的内存页。对于x86_64架构内核文档给出的 vsyscall 内存区域地址为ffffffffff600000 - ffffffffffdfffff (8 MB) vsyscalls实际观察一个运行中的进程其 vsyscall 映射通常只有一页4 KB~$ sudo cat /proc/1/maps | grep vsyscall ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]注意映射属性中的r-xp——可读、可执行、不可写且该地址是全局固定的在任何进程、任何时刻vsyscall 页都位于ffffffffff600000。这份固定既是 vsyscall 的优势glibc 可以直接硬编码地址也埋下了安全隐患后文详述。map_vsyscall映射发生在内核初始化期vsyscall 页的映射由 arch/x86/entry/vsyscall/vsyscall_64.c 中的map_vsyscall函数完成。该函数在 Linux 内核架构初始化阶段被调用——具体是在setup_arch函数中而setup_arch是本书 初始化系列第五章 讨论过的庞大架构初始化函数本仓库的 Initialization/linux-initialization-7.md 中vsyscall 映射一节也复述了这段调用关系。map_vsyscall的实现是否生效取决于内核配置选项CONFIG_X86_VSYSCALL_EMULATION#ifdef CONFIG_X86_VSYSCALL_EMULATION extern void map_vsyscall(void); #else static inline void map_vsyscall(void) {} #endif该配置选项在帮助文本中描述为使能 vsyscall 模拟enable vsyscall emulation。这里就引出一个关键问题为什么要模拟 vsyscall答案是vsyscall 出于安全原因是一种遗留legacyABI。它的地址是绑定的、固定不变的这意味着攻击者可以预知该页位置增加了被利用的风险因此现代内核默认不再以原生执行的方式对待它而是选择模拟。map_vsyscall的实现如下void __init map_vsyscall(void) { extern char __vsyscall_page; unsigned long physaddr_vsyscall __pa_symbol(__vsyscall_page); ... ... ... }函数开头通过宏__pa_symbol获取__vsyscall_page的物理地址该宏的原理在本书初始化系列第四章中讨论过。__vsyscall_page一个装着三个系统调用的汇编页__vsyscall_page定义在 arch/x86/entry/vsyscall/vsyscall_emu_64.S 汇编文件中位于.data..page_aligned, aw段其虚拟地址为ffffffff81881000 D __vsyscall_page页面内容极其精简——只包含三个系统调用的入口__vsyscall_page: mov $__NR_gettimeofday, %rax syscall ret .balign 1024, 0xcc mov $__NR_time, %rax syscall ret .balign 1024, 0xcc mov $__NR_getcpu, %rax syscall ret三个系统调用分别是gettimeofday获取当前时间time获取当前时间戳getcpu获取当前 CPU 编号其中0xcc是int3断点指令的操作码0x4001024字节的对齐让每个入口的地址可被精确预计算——这正是 glibc 能够硬编码入口地址的基础。在 Initialization/linux-initialization-7.md 中可以找到该页的完整汇编定义含.balign PAGE_SIZE, 0xcc与.globl __vsyscall_page。固定映射fixmap与页权限标志回到map_vsyscall拿到__vsyscall_page的物理地址后内核通过__set_fixmap把 vsyscall 页挂载到固定映射fix-mapped地址区if (vsyscall_mode ! NONE) __set_fixmap(VSYSCALL_PAGE, physaddr_vsyscall, vsyscall_mode NATIVE ? PAGE_KERNEL_VSYSCALL : PAGE_KERNEL_VVAR);关于固定映射地址fixmap本仓库的 MM/linux-mm-2.md 有专门讲解固定映射地址是一组编译期确定的虚拟地址物理地址只在引导期被设定之后内核像指针一样使用它们但绝不修改地址。固定映射区位于内核虚拟地址空间的顶部模块区之后、0xffffffffffffffff之前。__set_fixmap的三个参数依次是fixed_addresses枚举的索引、待映射页的物理地址、页标志位。VSYSCALL_PAGE是x86_64下fixed_addresses枚举中的一个元素值为511enum fixed_addresses { ... #ifdef CONFIG_X86_VSYSCALL_EMULATION VSYSCALL_PAGE (FIXADDR_TOP - VSYSCALL_ADDR) PAGE_SHIFT, #endif ... };页标志位取决于vsyscall_mode变量两种宏展开如下#define __PAGE_KERNEL_VSYSCALL (__PAGE_KERNEL_RX | _PAGE_USER) #define __PAGE_KERNEL_VVAR (__PAGE_KERNEL_RO | _PAGE_USER)两者的共同点是都带_PAGE_USER即允许低特权级的用户模式进程访问区别在于权限性质__PAGE_KERNEL_VSYSCALL__PAGE_KERNEL_RX | _PAGE_USER可读可执行__PAGE_KERNEL_VVAR__PAGE_KERNEL_RO | _PAGE_USER只读不可执行。也就是说vsyscall_mode为NATIVE时页面可执行虚拟系统调用以本地syscall指令直接运行为EMULATE时页面只读不可执行任何执行尝试都会触发 page fault由内核模拟处理。vsyscall 内核命令行参数native / emulate / nonevsyscall_mode的值由vsyscall_setup函数根据内核命令行参数设定static int __init vsyscall_setup(char *str) { if (str) { if (!strcmp(emulate, str)) vsyscall_mode EMULATE; else if (!strcmp(native, str)) vsyscall_mode NATIVE; else if (!strcmp(none, str)) vsyscall_mode NONE; else return -EINVAL; return 0; } return -EINVAL; }该函数通过early_param宏注册在内核早期参数解析阶段被调用early_param(vsyscall, vsyscall_setup);early_param宏的机制把参数处理函数挂入__setup_param链表、由parse_early_param/do_early_param遍历执行详见本书初始化系列第六章。三种取值对应三种行为命令行值vsyscall_mode页标志行为vsyscallnativeNATIVEPAGE_KERNEL_VSYSCALLRX页面可执行虚拟系统调用以本地syscall指令运行vsyscallemulateEMULATEPAGE_KERNEL_VVARRO页面不可执行执行尝试触发 page fault 后由内核模拟默认vsyscallnoneNONE—不映射 vsyscall 页任何访问都出错BUILD_BUG_ON编译期校验地址一致性map_vsyscall的最后通过BUILD_BUG_ON宏检查 vsyscall 页的虚拟地址是否严格等于常量VSYSCALL_ADDRffffffffff600000BUILD_BUG_ON((unsigned long)__fix_to_virt(VSYSCALL_PAGE) ! (unsigned long)VSYSCALL_ADDR);BUILD_BUG_ON是编译期断言若条件不成立则编译失败——这保证了固定映射页的虚拟地址与glibc 预知的地址永远一致。该宏的用法在初始化系列第一章有介绍。glibc 如何知道 vsyscall 入口地址由于 vsyscall 地址固定且入口按 1024 字节对齐glibc 可以在编译期硬编码所有虚拟系统调用处理器的地址#define VSYSCALL_ADDR_vgettimeofday 0xffffffffff600000 #define VSYSCALL_ADDR_vtime 0xffffffffff600400 #define VSYSCALL_ADDR_vgetcpu 0xffffffffff600800调用流程为把虚拟系统调用请求映射到__vsyscall_pageVSYSCALL_ADDR_xxx的偏移处将虚拟系统调用编号放入通用寄存器%rax随后执行本地的syscall指令。emulate 模式page fault 与 emulate_vsyscall在vsyscallemulate模式下由于页属性为只读不可执行__PAGE_KERNEL_VVAR任何对虚拟系统调用处理器的提升执行尝试都会触发 page fault#PF。do_page_fault是 page fault 的处理函数它会分析最近一次缺页的原因当判断为 emulate 模式下的虚拟系统调用时控制权交给定义在 arch/x86/entry/vsyscall/vsyscall_64.c 中的emulate_vsyscall函数。emulate_vsyscall首先通过addr_to_vsyscall_nr把触发地址转换为虚拟系统调用编号并做一系列合法性检查非法或未对齐的地址会被视为错误并发送段错误信号SIGSEGV... vsyscall_nr addr_to_vsyscall_nr(address); if (vsyscall_nr 0) { warn_bad_vsyscall(KERN_WARNING, regs, misaligned vsyscall...); goto sigsegv; } ... sigsegv: force_sig(SIGSEGV, current); return true;通过检查后函数按编号分发到对应的内核系统调用实现——注意参数来自保存的寄存器regs-di、regs-si对应%rdi、%rsiswitch (vsyscall_nr) { case 0: ret sys_gettimeofday( (struct timeval __user *)regs-di, (struct timezone __user *)regs-si); break; ... ... ... }最后与普通系统调用处理完毕后一样把返回值写入%raxregs-ax并手工恢复指令指针、将栈指针加 8 字节以模拟ret指令从而回到调用者继续执行regs-ax ret; do_ret: regs-ip caller; regs-sp 8; return true;至此一个虚拟系统调用在用户空间触发 → 内核缺页处理 → 模拟执行 → 伪造返回的完整闭环完成。vDSO现代的动态共享对象方案从静态地址到动态映射如前述vsyscall 的致命缺陷是地址固定、形态静态。vDSOvirtual dynamic shared object虚拟动态共享对象正是为解决这一局限而生。它与 vsyscall 的核心区别在于vDSO 以共享对象shared object的形式把内存页映射到每个进程地址是动态的而 vsyscall 在内存中静态存在、地址恒定。在x86_64架构上这个共享对象名为linux-vdso.so.1所有用户空间程序经由 glibc 与其链接。用ldd观察一个普通工具如uname即可看到~$ ldd /bin/uname linux-vdso.so.1 (0x00007ffe014b7000) libc.so.6 /lib64/libc.so.6 (0x00007fbfee2fe000) /lib64/ld-linux-x86-64.so.2 (0x00005559aab7c000)uname链接了三个库linux-vdso.so.1—— 提供 vDSO 功能libc.so.6—— C 标准库ld-linux-x86-64.so.2—— 程序解释器动态链接器其工作原理见本书杂项系列《链接器》。进程地址空间中对应的映射~$ sudo cat /proc/1/maps | grep vdso 7fff39f73000-7fff39f75000 r-xp 00000000 00:00 0 [vdso]注意这里 vdso 的地址7fff39f73000是每个进程运行时动态分配的与 vsyscall 的固定地址ffffffffff600000形成鲜明对比。init_vdsovDSO 镜像的初始化vDSO 的初始化发生在 arch/x86/entry/vdso/vma.c 中的init_vdso函数它依据CONFIG_X86_X32_ABI配置决定初始化 32 位与 64 位镜像static int __init init_vdso(void) { init_vdso_image(vdso_image_64); #ifdef CONFIG_X86_X32_ABI init_vdso_image(vdso_image_x32); #endif ... ... ... }vdso_image结构体代表了某种特定系统调用入口方式下的 vDSO 镜像其具体实例由vdso2c程序从不同的源文件生成生成产物为vdso-image-64.c等源码文件。之所以需要多份镜像是因为不同的调用方式对应不同的指令序列兼容模式下可以用int 0x80中断进入系统调用也可以使用本机syscall指令或sysenter指令。镜像的完整集合取决于内核配置#ifdef CONFIG_X86_64 extern const struct vdso_image vdso_image_64; #endif#ifdef CONFIG_X86_X32 extern const struct vdso_image vdso_image_x32; #endif#if defined CONFIG_X86_32 || defined CONFIG_COMPAT extern const struct vdso_image vdso_image_32_int80; #ifdef CONFIG_COMPAT extern const struct vdso_image vdso_image_32_syscall; #endif extern const struct vdso_image vdso_image_32_sysenter; #endifvdso_image 结构镜像的元数据vdso_image结构描述一个 vDSO 镜像的完整布局vDSO 区域的大小始终为PAGE_SIZE4096 字节的整数倍、指向文本映射text mapping的指针、alternatives针对特定处理器的指令替换集合的起始地址与长度等。以x86_64为例const struct vdso_image vdso_image_64 { .data raw_data, .size 8192, .text_mapping { .name [vdso], .pages pages, }, .alt 3145, .alt_len 26, .sym_vvar_start -8192, .sym_vvar_page -8192, .sym_hpet_page -4096, };其中raw_data是 64 位 vDSO 系统调用的原始二进制代码大小为两个页pages[2]即 8 KB。init_vdso_image定义在同一个源文件中主要工作是把raw_data中的每一页转换为对应的page结构并填入text_mapping.pagesvoid __init init_vdso_image(const struct vdso_image *image) { int i; int npages (image-size) / PAGE_SIZE; for (i 0; i npages; i) image-text_mapping.pages[i] virt_to_page(image-data i*PAGE_SIZE); ... ... ... }这里用virt_to_page宏把内核虚拟地址转换成对应的struct page。init_vdso通过subsys_initcall宏注册进initcalls链表最终由 init/main.c 中的do_initcalls在内核启动阶段逐一调用subsys_initcall(init_vdso);程序加载时的动态映射arch_setup_additional_pages 与 map_vdso初始化只是准备了page结构真正的映射发生在内核把可执行文件装载进内存时。程序加载后Linux 内核会调用 arch/x86/entry/vdso/vma.c 中的arch_setup_additional_pages函数它检查x86_64上 vDSO 是否启用然后调用map_vdsoint arch_setup_additional_pages(struct linux_binprm *bprm, int uses_interp) { if (!vdso64_enabled) return 0; return map_vdso(vdso_image_64, true); }map_vdso定义在同一文件中负责把 vDSO 的代码页以及共享的 vDSO 变量页映射进新进程的地址空间。这也解释了为什么每个进程看到的[vdso]映射地址各不相同——它是在execve加载 ELF 程序时按进程动态布局的。关于 ELF 加载的完整流程load_elf_binary、start_thread等见本系列第四节《Linux 内核如何运行程序》。vDSO 提供的四个系统调用vDSO 不仅地址动态提供的调用也比 vsyscall 多一个__vdso_clock_gettime__vdso_getcpu__vdso_gettimeofday__vdso_timevsyscall 与 vDSO 对比总结维度vsyscallvDSO地址形态静态固定ffffffffff600000每个进程动态映射载体形式固定内存页不可写、固定位置共享对象linux-vdso.so.1提供的调用3 个gettimeofday、time、getcpu4 个clock_gettime、getcpu、gettimeofday、time映射时机内核初始化期map_vsyscallsetup_arch内程序加载期arch_setup_additional_pages→map_vdso安全形态遗留 ABI地址可预知默认转为模拟emulate现代方案动态布局无固定地址暴露相关源码arch/x86/entry/vsyscall/vsyscall_64.c、vsyscall_emu_64.Sarch/x86/entry/vdso/vma.c两者本质相同——都是把系统调用实现放进用户空间可访问的内存页、省去上下文切换区别在于 vsyscall 是静态的遗留方案vDSO 是动态的现代替代。vDSO 的初始化路径、镜像生成vdso2c与按进程映射机制共同解决了 vsyscall 固定地址带来的安全隐患同时保留用户空间零上下文切换执行轻量系统调用的性能优势。延伸阅读系统调用概念简介——系统调用的基本概念与用户空间视角Linux 内核如何处理系统调用——系统调用表的初始化与内核侧处理流程Linux 内核如何运行程序——ELF 加载与arch_setup_additional_pages的调用背景固定映射地址和输入输出重映射——fixmap 地址区与VSYSCALL_PAGE枚举链接器Linkers——程序解释器与动态链接原理Linux 内核初始化第六章——early_param宏与早期参数解析Linux 内核初始化第七章——setup_arch中的map_vsyscall调用细节赞分享文档教程操作系统【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/gh_mirrors/li/linux-insides-zh点击查看免费下载相关推荐Linux 内核系统调用系列三vsyscall 与 vDSO 机制深度解析Linux 内核系统调用系列三vsyscall 与 vDSO 机制深度解析 导读 本文是《linux insides》仓库 SysCall/linux s文档教程操作系统Linux 内核系统调用加速机制linux-insides-zhvsyscall 与 vDSO 原理与实现深度解析Linux 内核系统调用加速机制linux insides zhvsyscall 与 vDSO 原理与实现深度解析 vsyscall 与 vDSO 是 LLinux 内核系统调用机制解析vsyscall 与 vDSO 详解Linux 内核系统调用机制解析vsyscall 与 vDSO 详解 前言 在 Linux 系统编程中系统调用是用户空间程序与内核交互的重要接口。然而传统文档教程操作系统上一篇AppleRa1n5步免费解锁iOS 15-16设备激活锁的完整指南下一篇Windows安卓驱动安装终极指南告别手动配置的ADB Fastboot一键安装工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考