Linux vdso机制

前言

系统调用入口与退出:
(1)入口准备:

; x86_64 示例(通过 syscall 指令触发)
mov rax, 60   ; syscall number (e.g. exit)
mov rdi, 0    ; arg1
syscall       ; 切入内核

内核保存用户态寄存器状态(pt_regs 结构)
检查调用号合法性(通过 sys_call_table 跳转)

(2)退出处理:
恢复用户态上下文
处理信号和调度检查(TIF_NEED_RESCHED 标志)
通过 sysretq/iretq 返回用户空间

传统系统调用需要完整的上下文切换,成为高频系统调用(如 gettimeofday)的性能瓶颈。

一个经常被使用的系统调用是 gettimeofday。这个系统调用既会被用户空间的应用程序直接调用,也会被 C 库间接调用。想想时间戳、定时循环或轮询 —— 所有这些操作常常都需要知道当前的时间。这个信息也并非机密 —— 处于任何特权模式(超级用户或任何普通用户)的任何应用程序都会得到相同的答案。因此,内核会安排将回答这个问题所需的信息放置在进程可以访问的内存中。这样一来,对 gettimeofday的调用就从一次系统调用变成了一次普通的函数调用以及几次内存访问操作。

一、vsyscalls

vsyscall 和 vdso 被设计用来加速系统调用的处理,vsyscall 或 virtual system call 是第一种也是最古老的一种用于加快系统调用的机制。 vsyscall 的工作原则其实十分简单。Linux 内核在用户空间映射一个包含一些变量及一些系统调用的实现的内存页。

在内核与用户态之间建立一段共享内存区域,由内核定期“推送”最新值到该共享内存区域,然后用户态程序在调用这些系统调用的glibc库函数的时候,库函数并不真正执行系统调用,而是通过vsyscall page来读取该数据的最新值,相当于将系统调用改造成了函数调用,直接提升了执行性能。
在这里插入图片描述
大小为 0x1000 = 4096 ,一个页大小。

vDSO与vsyscall最大的不同体现在以下方面:
(1)vDSO本质上是一个ELF共享目标文件;而vsyscall只是一段内存代码和数据。
(2)vsyscall位于内核地址空间,采用静态地址映射方式;而vDSO借助共享目标文件天生具有的PIC特性,可以以进程为粒度动态映射到进程地址空间中。

vsyscall是一个固定的内核地址,而vdso是一个地址随机化的用户空间虚拟地址。

// arch/x86/entry/vsyscall/vsyscall_64.c
void __init map_vsyscall(void) {
    __set_fixmap(VSYSCALL_PAGE, physaddr, PAGE_KERNEL_VVAR);
}

静态映射到内核地址空间固定位置。

vsyscall (传统机制)vDSO
内存映射方式静态固定地址(0xffffffffff600000)动态共享库映射(地址随机化,ASLR 兼容)
实现形式硬编码的内核页动态链接库(如 linux-vdso.so.1)
安全性低(固定地址易受攻击)高(地址随机化 + 权限隔离)

vsyscall 的问题:
固定地址导致安全漏洞(如 ret2libc 攻击)
仅支持 3 个调用(time, gettimeofday, getcpu)

由于vDSO取代了vsyscall,内核开发人员也就不再在vsyscall上添加新的快速系统调用函数了。

关于vsyscalls相关资料请参考:
https://xinqiu.gitbooks.io/linux-insides-cn/content/SysCall/linux-syscall-3.html
https://zhuanlan.zhihu.com/p/436454953

二、vdso

2.1 简介

Linux中的vdso(Virtual Dynamic Shared Object)是一种特殊的动态共享对象,它在用户空间和内核空间之间提供了一种高效的接口。vdso机制的目的是减少用户空间程序与内核之间频繁的上下文切换开销,提高系统性能。
用户态直接调用 vDSO 函数 无需陷入内核。

“vDSO”(虚拟动态共享对象)是一个小型的共享库,内核会自动将其映射到所有用户空间应用程序的地址空间中。应用程序通常无需关注这些细节,因为vDSO最常由C库调用。这样,可以以正常方式编码,使用标准函数,而C库会负责使用通过vDSO可用的任何功能。

vDSO的存在是为什么?内核提供了一些系统调用,用户空间代码经常使用这些调用,以至于这些调用可能主导整体性能。这既是由于调用的频率,又是由于从用户空间退出并进入内核所产生的上下文切换开销。

vdso包含一组特定的函数,这些函数在用户空间中执行,但其实现是由内核提供的。用户空间程序可以通过调用这些函数来访问一些系统功能,而无需陷入内核态。

vdso的一个重要用途是实现系统调用的快速路径。当用户空间程序执行系统调用时,通常需要进行一次上下文切换,将控制权从用户态切换到内核态。然而,某些系统调用是非常频繁且开销较小的,这种上下文切换的开销可能会成为性能瓶颈。vdso提供了一个快速路径,通过在用户空间中执行特定的系统调用函数,避免了不必要的上下文切换,从而提高了系统调用的性能。

在Linux中,vdso通常以linux-vdso.so.X的形式存在于/proc/self/maps中,并且被映射到每个进程的地址空间中。这样,用户空间程序可以直接调用vdso中的函数,而无需显式加载和链接vdso库。

总结来说,vdso是Linux中用于优化系统调用性能的一种机制,它提供了一组在用户空间执行的特定系统调用函数,以减少用户态和内核态之间的上下文切换开销,并提高系统性能。

备注:
vdso只包括了几个特定的系统调用:

clock_gettime
gettimeofday
getcpu
time
clock_getres

比如gettimeofday:
一个经常被使用的系统调用是gettimeofday(2)。这个系统调用既可以被用户空间应用程序直接调用,也可以被C库间接调用。想象一下时间戳、定时循环或轮询,所有这些都经常需要知道当前的时间。这些信息也不是机密的,任何特权模式(root或非特权用户)的应用程序都会得到相同的答案。因此,内核会安排将回答这个问题所需的信息放置在进程可以访问的内存中。现在,调用gettimeofday(2)变成了一个普通的函数调用和几次内存访问。

2.2 用户态

$ ldd `which bash`
	linux-vdso.so.1 (0x00007ffebeed9000)
	libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f67a01f9000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f679fe00000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f67a039d000)
	
$ ldd `which bash` | grep vdso
	linux-vdso.so.1 (0x00007ffc5b5bb000)
	
$ ldd `which bash` | grep vdso
	linux-vdso.so.1 (0x00007ffd10bdd000)
	
$ ldd `which bash` | grep vdso
	linux-vdso.so.1 (0x00007ffd143b5000)

vDSO mapping的名称叫linux-vdso.so.1,其映射的基地址每次都是不同的。

这利用了内核的ASLR特性,以解决vsyscall page固定映射地址的安全问题。为了方便地利用ASLR特性,vDSO mapping的本体是一个ELF共享目标文件(x86-64下的文件名称叫做vdso64.so,位于内核源码arch/x86/entry/vdso/下)。

# cat /proc/1/maps
55637a23d000-55637a26f000 r--p 00000000 08:05 59514907                   /usr/lib/systemd/systemd
55637a26f000-55637a32d000 r-xp 00032000 08:05 59514907                   /usr/lib/systemd/systemd
55637a32d000-55637a383000 r--p 000f0000 08:05 59514907                   /usr/lib/systemd/systemd
55637a383000-55637a3c9000 r--p 00145000 08:05 59514907                   /usr/lib/systemd/systemd
55637a3c9000-55637a3ca000 rw-p 0018b000 08:05 59514907                   /usr/lib/systemd/systemd
55637c14a000-55637c416000 rw-p 00000000 00:00 0                          [heap]

......

7f008b2bf000-7f008b2c0000 rw-p 00000000 00:00 0
7ffd3bc40000-7ffd3bd42000 rw-p 00000000 00:00 0                          [stack]
7ffd3bd4f000-7ffd3bd53000 r--p 00000000 00:00 0                          [vvar]
7ffd3bd53000-7ffd3bd55000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

其中:

7ffd3bd53000-7ffd3bd55000 r-xp 00000000 00:00 0                          [vdso]

可以看到vdso内存大小:0x2000 = 4096 * 2,即两个虚拟页面的大小。

vdso的起始虚拟地址在进程1是:0x7ffd3bd53000,转化为十进制即140725607280640,将这段内存dump到文件中:

# dd if=/proc/1/mem of=/tmp/linux-vdso.so skip=140725607280640 ibs=1 count=8192
dd: /proc/1/mem: cannot skip to specified offset
8192+0 records in
16+0 records out
8192 bytes (8.2 kB, 8.0 KiB) copied, 0.00971912 s, 843 kB/s

由于vDSO是一个完整的ELF镜像,可以对其进行符号查找:

# objdump -T /tmp/linux-vdso.so

/tmp/linux-vdso.so:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000a10  w   DF .text  0000000000000413  LINUX_2.6   clock_gettime
0000000000000690 g    DF .text  0000000000000348  LINUX_2.6   __vdso_gettimeofday
0000000000000e30  w   DF .text  0000000000000060  LINUX_2.6   clock_getres
0000000000000e30 g    DF .text  0000000000000060  LINUX_2.6   __vdso_clock_getres
0000000000000690  w   DF .text  0000000000000348  LINUX_2.6   gettimeofday
00000000000009e0 g    DF .text  0000000000000029  LINUX_2.6   __vdso_time
0000000000000ec0 g    DF .text  000000000000009c  LINUX_2.6   __vdso_sgx_enter_enclave
00000000000009e0  w   DF .text  0000000000000029  LINUX_2.6   time
0000000000000a10 g    DF .text  0000000000000413  LINUX_2.6   __vdso_clock_gettime
0000000000000000 g    DO *ABS*  0000000000000000  LINUX_2.6   LINUX_2.6
0000000000000e90 g    DF .text  0000000000000025  LINUX_2.6   __vdso_getcpu
0000000000000e90  w   DF .text  0000000000000025  LINUX_2.6   getcp

找到虚拟动态共享对象(vDSO)
如果存在 vDSO,其基地址会由内核通过初始辅助向量(参见 getauxval (3)),以 AT_SYSINFO_EHDR 标签的形式传递给每个程序。

       #include <sys/auxv.h>

       void *vdso = (uintptr_t) getauxval(AT_SYSINFO_EHDR);

由于虚拟动态共享对象(vDSO)是一个完整的可执行与可链接格式(ELF)镜像,你可以对其进行符号查找。这使得在更新的内核版本发布时能够添加新的符号,并且让 C 库在不同内核版本下运行时,能够在运行时检测到可用的功能。通常情况下,C 库会在第一次调用时进行检测,然后将结果缓存起来供后续调用使用。

所有的符号也都有版本标识(使用 GNU 版本格式)。这使得内核能够在不破坏向后兼容性的情况下更新函数签名。这意味着可以更改函数接受的参数以及返回值。因此,当在 vDSO 中查找一个符号时,你必须始终包含版本信息,以匹配你所期望的应用程序二进制接口(ABI)。

通常,vDSO 遵循这样的命名约定:给所有符号添加前缀 “_vdso” 或 “_kernel”,以便将它们与其他标准符号区分开来。例如,“gettimeofday” 函数被命名为 “__vdso_gettimeofday”。

当调用这些函数中的任何一个时,你要使用标准的 C 调用约定。无需担心奇怪的寄存器或堆栈行为。

你不能假定 vDSO 会被映射到用户内存映射中的任何特定位置。每次创建新的进程映像时(即在执行 execve (2) 时),基地址通常会在运行时被随机化。这样做是出于安全考虑,以防止 “返回 libc” 攻击。

在 x86_64或者arm64 架构中,每当内核加载一个ELF可执行程序时,内核都会在其进程地址空间中建立一个叫做vDSO mapping的内存区域。

//v5.13/source/arch/x86/entry/vdso/vma.c
#ifdef CONFIG_X86_64
/*
 * Put the vdso above the (randomized) stack with another randomized
 * offset.  This way there is no hole in the middle of address space.
 * To save memory make sure it is still in the same PTE as the stack
 * top.  This doesn't give that many random bits.
 *
 * Note that this algorithm is imperfect: the distribution of the vdso
 * start address within a PMD is biased toward the end.
 *
 * Only used for the 64-bit and x32 vdsos.
 */
static unsigned long vdso_addr(unsigned long start, unsigned len)
{
	unsigned long addr, end;
	unsigned offset;

	/*
	 * Round up the start address.  It can start out unaligned as a result
	 * of stack start randomization.
	 */
	start = PAGE_ALIGN(start);

	/* Round the lowest possible end address up to a PMD boundary. */
	end = (start + len + PMD_SIZE - 1) & PMD_MASK;
	if (end >= TASK_SIZE_MAX)
		end = TASK_SIZE_MAX;
	end -= len;

	if (end > start) {
		offset = get_random_int() % (((end - start) >> PAGE_SHIFT) + 1);
		addr = start + (offset << PAGE_SHIFT);
	} else {
		addr = start;
	}

	/*
	 * Forcibly align the final address in case we have a hardware
	 * issue that requires alignment for performance reasons.
	 */
	addr = align_vdso_addr(addr);

	return addr;
}

static int map_vdso_randomized(const struct vdso_image *image)
{
	unsigned long addr = vdso_addr(current->mm->start_stack, image->size-image->sym_vvar_start);

	return map_vdso(image, addr);
}
#endif

#ifdef CONFIG_X86_64
int arch_setup_additional_pages(struct linux_binprm *bprm, int uses_interp)
{
	if (!vdso64_enabled)
		return 0;

	return map_vdso_randomized(&vdso_image_64);
}

每个进程独立映射(受 ASLR 保护)

2.2.1 /proc/pid/maps

/proc/pid/maps文件是一个特殊的文件,它提供了与指定进程的内存映射相关的信息。每个正在运行的进程都有一个相应的/proc/pid/maps文件,其中pid是进程的ID。

/proc/pid/maps文件的内容包含了进程的内存映射区域的详细信息,每行表示一个内存映射区域。每行的格式如下:

start-end permissions offset dev inode pathname

其中,各字段的含义如下:
(1)start-end:表示内存区域的起始地址和结束地址。
(2)permissions:表示内存区域的访问权限,如读取(r)、写入(w)、执行(x)等。
(3)offset:表示内存区域相对于文件的偏移量。
(4)dev:表示内存区域所在设备的标识符。
(5)inode:表示内存区域所对应的文件的索引节点号。
(6)pathname:表示内存区域所对应的文件的路径名。

通过解析/proc/pid/maps文件,可以获取进程的内存映射信息,包括可执行文件、共享库、堆、栈和匿名映射等。这对于分析进程的内存布局以及诊断内存相关问题非常有用。

对应的还有/proc/pid/smaps文件,smaps文件相比maps的内容更详细,可以理解为是对maps的一个扩展,比如进程1的vdso内存区间:

7ffd3bd53000-7ffd3bd55000 r-xp 00000000 00:00 0                          [vdso]
Size:                  8 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                   8 kB
Pss:                   2 kB
Shared_Clean:          8 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:            8 kB
Anonymous:             0 kB
LazyFree:              0 kB
AnonHugePages:         0 kB
ShmemPmdMapped:        0 kB
FilePmdMapped:         0 kB
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
Swap:                  0 kB
SwapPss:               0 kB
Locked:                0 kB
THPeligible:    0
VmFlags: rd ex mr mw me de sd

而pmap是解析的/proc里的文件,具体文件是/proc/[pid]/maps和/proc/[pid]/smaps:

# strace -e trace=file pmap 1
execve("/usr/bin/pmap", ["pmap", "1"], 0x7ffe63462218 /* 28 vars */) = 0
......
openat(AT_FDCWD, "/proc/self/maps", O_RDONLY) = 3
......
# strace -e trace=file pmap -x 1
execve("/usr/bin/pmap", ["pmap", "-x", "1"], 0x7fff89d1b6c0 /* 28 vars */) = 0
......
openat(AT_FDCWD, "/proc/1/smaps", O_RDONLY) = 3
......

2.2.2 /proc/pid/mem

/proc/pid/mem文件是一个特殊的文件,用于表示一个进程的内存。它允许读取和写入进程的物理内存,但需要使用适当的权限来访问。

$ ls -l /proc/1/mem
-rw------- 1 root root 0 1213 14:17 /proc/1/mem

以下是有关/proc/pid/mem文件的一些重要信息:
(1)访问权限:只有具有足够权限的用户(通常是root用户或具有适当权限的用户)才能读取和写入/proc/pid/mem文件。
(2)文件路径:每个进程的pid将替换文件路径中的pid部分。例如,要访问进程ID为123的进程的内存,可以使用/proc/123/mem路径。
(3)内存访问:/proc/pid/mem文件提供了对进程内存的直接访问。可以将其视为一个包含整个进程地址空间的二进制文件。通过读取或写入该文件,可以读取或修改进程中的数据。

读取:通过打开/proc/pid/mem文件,并使用pread()或pread64()系统调用 --> lseek + read,可以从该文件中读取进程的内存数据。

写入:通过打开/proc/pid/mem文件,并使用pwrite()或pwrite64()系统调用 --> lseek + write,可以向该文件中写入进程的内存数据。

注意:写入/proc/pid/mem文件可能会对进程的稳定性和正确性产生严重影响,因此在修改进程内存之前务必小心谨慎。

(4)偏移量限制:/proc/pid/mem文件中的偏移量指示要读取或写入的内存位置。需要注意的是,偏移量必须是页面大小(通常为4KB)的倍数。

需要特别注意的是,使用/proc/pid/mem文件来访问进程内存是一个高级且潜在危险的操作,需要谨慎使用,遵守适当的权限和安全措施。在大多数情况下,更安全和可靠的方法是使用进程调试工具(例如gdb)来访问和修改进程的内存。

2.3 内核态

vDSO 会向用户提供的 syscall:

// linux-5.13/arch/x86/entry/vdso/vdso.lds.S

/*
 * This controls what userland symbols we export from the vDSO.
 */
VERSION {
	LINUX_2.6 {
	global:
		clock_gettime;
		__vdso_clock_gettime;
		gettimeofday;
		__vdso_gettimeofday;
		getcpu;
		__vdso_getcpu;
		time;
		__vdso_time;
		clock_getres;
		__vdso_clock_getres;
		__vdso_sgx_enter_enclave;
	local: *;
	};
}

即:

__vdso_clock_gettime
__vdso_gettimeofday
__vdso_getcpu
__vdso_time
__vdso_clock_getres
__vdso_sgx_enter_enclave

通常,vDSO遵循将所有符号以“_vdso”或“_kernel”作为前缀的命名约定,以便将它们与其他标准符号区分开。例如,“gettimeofday”函数的名称是“__vdso_gettimeofday”。

可以看到用户态vdso虚拟地址内容一样。

2.4 内核源码解析

内核具体源码解析请参考:
https://tinylab.org/riscv-syscall-part4-vdso-implementation/
https://www.bookstack.cn/read/linux-insides-zh/SysCall-linux-syscall-3.md
https://zhuanlan.zhihu.com/p/611286101

三、Enclave vdso

在5.11Linux内核版本,Linux Intel支持了sgx硬件特性,vdso增加了一个函数:__vdso_sgx_enter_enclave。

进入enclave只能通过SGX特定的EENTER和ERESUME函数进行,这是一个非常复杂的过程。由于从enclave进入和退出的复杂性,enclave通常使用一个库来处理实际的过渡过程。这与大多数应用程序使用glibc实现来封装系统调用的方式类似。

enclave的另一个关键特征是,在其正常操作过程中可能会生成异常,这些异常需要在enclave内部进行处理,或者是SGX特有的异常。

SGX不使用传统的信号机制来处理这些异常,而是利用由虚拟动态共享对象(vDSO)提供的特殊异常修复功能。内核提供的vDSO函数封装了与enclave之间的低层次过渡,如EENTER和ERESUME。vDSO函数拦截本应生成信号的异常,并将错误信息直接返回给其调用者。这避免了需要处理信号处理程序的复杂性。

/**
 * struct sgx_enclave_run - the execution context of __vdso_sgx_enter_enclave()
 * @tcs:			TCS used to enter the enclave
 * @function:			The last seen ENCLU function (EENTER, ERESUME or EEXIT)
 * @exception_vector:		The interrupt vector of the exception
 * @exception_error_code:	The exception error code pulled out of the stack
 * @exception_addr:		The address that triggered the exception
 * @user_handler:		User provided callback run on exception
 * @user_data:			Data passed to the user handler
 * @reserved			Reserved for future extensions
 *
 * If @user_handler is provided, the handler will be invoked on all return paths
 * of the normal flow.  The user handler may transfer control, e.g. via a
 * longjmp() call or a C++ exception, without returning to
 * __vdso_sgx_enter_enclave().
 */
struct sgx_enclave_run {
	__u64 tcs;
	__u32 function;
	__u16 exception_vector;
	__u16 exception_error_code;
	__u64 exception_addr;
	__u64 user_handler;
	__u64 user_data;
	__u8  reserved[216];
};

/**
 * typedef vdso_sgx_enter_enclave_t - Prototype for __vdso_sgx_enter_enclave(),
 *				      a vDSO function to enter an SGX enclave.
 * @rdi:	Pass-through value for RDI
 * @rsi:	Pass-through value for RSI
 * @rdx:	Pass-through value for RDX
 * @function:	ENCLU function, must be EENTER or ERESUME
 * @r8:		Pass-through value for R8
 * @r9:		Pass-through value for R9
 * @run:	struct sgx_enclave_run, must be non-NULL
 *
 * NOTE: __vdso_sgx_enter_enclave() does not ensure full compliance with the
 * x86-64 ABI, e.g. doesn't handle XSAVE state.  Except for non-volatile
 * general purpose registers, EFLAGS.DF, and RSP alignment, preserving/setting
 * state in accordance with the x86-64 ABI is the responsibility of the enclave
 * and its runtime, i.e. __vdso_sgx_enter_enclave() cannot be called from C
 * code without careful consideration by both the enclave and its runtime.
 *
 * All general purpose registers except RAX, RBX and RCX are passed as-is to the
 * enclave.  RAX, RBX and RCX are consumed by EENTER and ERESUME and are loaded
 * with @function, asynchronous exit pointer, and @run.tcs respectively.
 *
 * RBP and the stack are used to anchor __vdso_sgx_enter_enclave() to the
 * pre-enclave state, e.g. to retrieve @run.exception and @run.user_handler
 * after an enclave exit.  All other registers are available for use by the
 * enclave and its runtime, e.g. an enclave can push additional data onto the
 * stack (and modify RSP) to pass information to the optional user handler (see
 * below).
 *
 * Most exceptions reported on ENCLU, including those that occur within the
 * enclave, are fixed up and reported synchronously instead of being delivered
 * via a standard signal. Debug Exceptions (#DB) and Breakpoints (#BP) are
 * never fixed up and are always delivered via standard signals. On synchronously
 * reported exceptions, -EFAULT is returned and details about the exception are
 * recorded in @run.exception, the optional sgx_enclave_exception struct.
 *
 * Return:
 * - 0:		ENCLU function was successfully executed.
 * - -EINVAL:	Invalid ENCL number (neither EENTER nor ERESUME).
 */
typedef int (*vdso_sgx_enter_enclave_t)(unsigned long rdi, unsigned long rsi,
					unsigned long rdx, unsigned int function,
					unsigned long r8,  unsigned long r9,
					struct sgx_enclave_run *run);

注意:
__vdso_sgx_enter_enclave()函数不能确保完全符合x86-64 ABI,例如,它不处理XSAVE状态。除了非易失性通用寄存器、EFLAGS.DF和RSP对齐之外,根据x86-64 ABI保留/设置状态是enclave及其运行时的责任,也就是说,不经过enclave和其运行时的仔细考虑,不能从C代码中调用__vdso_sgx_enter_enclave()函数。

描述:
除了RAX、RBX和RCX寄存器之外,所有通用寄存器都按原样传递给enclave。RAX、RBX和RCX寄存器由EENTER和ERESUME使用,并分别加载函数、异步退出指针和run.tcs。

RBP和堆栈用于将__vdso_sgx_enter_enclave()锚定到enclave之前的状态,例如,在enclave退出后检索run.exception和run.user_handler。所有其他寄存器都可以由enclave及其运行时使用,例如,enclave可以将附加数据推送到堆栈上(并修改RSP)以将信息传递给可选的用户处理程序。

大多数在ENCLU上报告的异常(包括在enclave内部发生的异常)都会被修复并同步报告,而不是通过标准信号传递。调试异常(#DB)和断点(#BP)永远不会被修复,并始终通过标准信号传递。对于同步报告的异常,将返回-EFAULT,并将异常的详细信息记录在run.exception中,这是可选的sgx_enclave_exception结构

返回值:
0:ENCLU函数成功执行。
-EINVAL:无效的ENCL号码(既不是EENTER也不是ERESUME)。

参考于:https://www.kernel.org/doc/html/next/x86/sgx.html#

四、代码示例

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include <sys/auxv.h>
#include <elf.h>
#include <link.h>

/* 确保ElfW宏存在 */
#ifndef ElfW
# ifdef __x86_64__
#  define ElfW(type) Elf64_##type
# else
#  define ElfW(type) Elf32_##type
# endif
#endif

/* vDSO函数指针 */
static long (*vdso_time)(time_t *) = NULL;
static int (*vdso_gettimeofday)(struct timeval *, void *) = NULL;
static int (*vdso_clock_gettime)(clockid_t, struct timespec *) = NULL;

/* ELF符号表结构 */
struct vdso_sym {
    const char *name;
    void **func;
};

/* 需要查找的符号 */
static struct vdso_sym vdso_symbols[] = {
    {"__vdso_time", (void **)&vdso_time},
    {"__vdso_gettimeofday", (void **)&vdso_gettimeofday},
    {"__vdso_clock_gettime", (void **)&vdso_clock_gettime},
    {NULL, NULL}
};

/* 安全内存读取 */
static uintptr_t safe_read(const void *addr, size_t size) {
    uintptr_t val = 0;
    memcpy(&val, addr, size);
    return val;
}

/* 初始化vDSO */
void init_vdso() {
    uintptr_t vdso_addr = getauxval(AT_SYSINFO_EHDR);
    if (!vdso_addr) {
        fprintf(stderr, "vDSO not available\n");
        return;
    }

    /* 解析ELF头 */
    ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)vdso_addr;
    if (memcmp(ehdr->e_ident, ELFMAG, SELFMAG) != 0) {
        fprintf(stderr, "Invalid ELF header\n");
        return;
    }

    /* 查找节区 */
    ElfW(Shdr) *shdr = (ElfW(Shdr) *)(vdso_addr + ehdr->e_shoff);
    const char *shstrtab = (const char *)(vdso_addr + 
                       safe_read(&shdr[ehdr->e_shstrndx].sh_offset, sizeof(uintptr_t)));

    ElfW(Shdr) *dynsym = NULL, *dynstr = NULL;
    for (int i = 0; i < ehdr->e_shnum; i++) {
        const char *name = shstrtab + safe_read(&shdr[i].sh_name, sizeof(uint32_t));
        if (!strcmp(name, ".dynsym")) dynsym = &shdr[i];
        else if (!strcmp(name, ".dynstr")) dynstr = &shdr[i];
    }

    if (!dynsym || !dynstr) {
        fprintf(stderr, "Missing symbol tables\n");
        return;
    }

    /* 遍历符号表 */
    uintptr_t symtab = vdso_addr + safe_read(&dynsym->sh_offset, sizeof(uintptr_t));
    uintptr_t strtab = vdso_addr + safe_read(&dynstr->sh_offset, sizeof(uintptr_t));
    size_t sym_count = safe_read(&dynsym->sh_size, sizeof(uintptr_t)) / sizeof(ElfW(Sym));

    for (size_t i = 0; i < sym_count; i++) {
        ElfW(Sym) *sym = (ElfW(Sym) *)(symtab + i * sizeof(ElfW(Sym)));
        const char *name = (const char *)(strtab + safe_read(&sym->st_name, sizeof(uint32_t)));

        for (struct vdso_sym *vsym = vdso_symbols; vsym->name; vsym++) {
            if (!strcmp(name, vsym->name)) {
                *vsym->func = (void *)(vdso_addr + safe_read(&sym->st_value, sizeof(uintptr_t)));
                break;
            }
        }
    }
}

int main() {
    init_vdso();

    /* 测试time */
    if (vdso_time) {
        time_t t;
        long ret = vdso_time(&t);
        printf("vDSO time: %ld\n", ret);
    } else {
        printf("vDSO time not available\n");
    }

    /* 测试gettimeofday */
    if (vdso_gettimeofday) {
        struct timeval tv;
        vdso_gettimeofday(&tv, NULL);
        printf("gettimeofday: %ld.%06ld\n", tv.tv_sec, tv.tv_usec);
    }

    /* 新增clock_gettime测试 */
    if (vdso_clock_gettime) {
        struct timespec ts;
        vdso_clock_gettime(CLOCK_REALTIME, &ts);
        printf("clock_gettime: %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
        
        /* 测试CLOCK_MONOTONIC */
        vdso_clock_gettime(CLOCK_MONOTONIC, &ts);
        printf("monotonic time: %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
    } else {
        printf("vDSO clock_gettime not available\n");
    }

    return 0;
}

参考资料

Linux 5.13
https://man7.org/linux/man-pages/man7/vdso.7.html
https://blog.rustforever.top/2022/02/10/linux/syscall/vdso/
https://zhuanlan.zhihu.com/p/620578643
https://cloud.tencent.com/developer/article/1517837

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值