编辑
2025-01-22
记录知识
0
请注意,本文编写于 156 天前,最后修改于 59 天前,其中某些信息可能已经过时。

目录

一、libc如何调用的vdso
二、内核测试程序
三、修改内核测试程序
四、原理解析
4.1 命令获取信息
4.2 代码打印
五、结论

在vDSO--示例之将__do_sys_kylin加入vDSO中我们实现了vdso调用自定义的syscall,但是缺点是我们还是通过ld来链接的vdso.so,这种情况下还没有完全达到libc实现的vdso功能。因为我们所有程序在编译的时候并没有-lvdso去链接。本文基于libc去链接vdso的理解,实现一个vdso程序,这样无需-lvdso就能直接使用vdso的程序

一、libc如何调用的vdso

根据文档,我们可以知道如下:

image.png 我们关注libc init相关的代码,直接下载源码即可分析,本文基于内核提供的示例来解析,就不去翻libc的代码了,意思是一样的。

二、内核测试程序

代码位置如下:

tools/testing/selftests/vDSO

我们关注两个文件:

parse_vdso.c vdso_test_gettimeofday.c

此时我们运行make,则可以获取二进制vdso_test_gettimeofday

将其拿到系统运行即可。

三、修改内核测试程序

内核的vdso_test_gettimeofday.c不是我们的目的,我们需要调用自己的函数"__kernel_kylin",所以我们新建一个文件如下:

vdso_test_kylin.c

代码内容如下:

// SPDX-License-Identifier: GPL-2.0-only /* * vdso_test_kylin.c: Sample code to test parse_vdso.c and vDSO kylin() */ #include <stdint.h> #include <elf.h> #include <stdio.h> #include <sys/auxv.h> #include "../kselftest.h" #include "parse_vdso.h" const char *version = "LINUX_2.6.39"; const char *name = "__kernel_kylin"; typedef long (*kylin_t)(char* words); int main(int argc, char **argv) { unsigned long sysinfo_ehdr; char* words = "Userspace say:hello kylin!"; long ret; sysinfo_ehdr = getauxval(AT_SYSINFO_EHDR); vdso_init_from_sysinfo_ehdr(getauxval(AT_SYSINFO_EHDR)); kylin_t kylin = (kylin_t)vdso_sym(version, name); if(!kylin){ printf("Could not find %s\n", name); return KSFT_SKIP; } ret = kylin(words); return 0; }

此时我们修改Makefile如下:

# git diff Makefile diff --git a/tools/testing/selftests/vDSO/Makefile b/tools/testing/selftests/vDSO/Makefile index 0069f2f83f86..9ffeaded2168 100644 --- a/tools/testing/selftests/vDSO/Makefile +++ b/tools/testing/selftests/vDSO/Makefile @@ -4,7 +4,7 @@ include ../lib.mk uname_M := $(shell uname -m 2>/dev/null || echo not) ARCH ?= $(shell echo $(uname_M) | sed -e s/i.86/x86/ -e s/x86_64/x86/) -TEST_GEN_PROGS := $(OUTPUT)/vdso_test_gettimeofday $(OUTPUT)/vdso_test_getcpu +TEST_GEN_PROGS := $(OUTPUT)/vdso_test_gettimeofday $(OUTPUT)/vdso_test_getcpu $(OUTPUT)/vdso_test_kylin ifeq ($(ARCH),x86) TEST_GEN_PROGS += $(OUTPUT)/vdso_standalone_test_x86 endif @@ -19,6 +19,7 @@ endif all: $(TEST_GEN_PROGS) $(OUTPUT)/vdso_test_gettimeofday: parse_vdso.c vdso_test_gettimeofday.c $(OUTPUT)/vdso_test_getcpu: parse_vdso.c vdso_test_getcpu.c +$(OUTPUT)/vdso_test_kylin: parse_vdso.c vdso_test_kylin.c $(OUTPUT)/vdso_standalone_test_x86: vdso_standalone_test_x86.c parse_vdso.c $(CC) $(CFLAGS) $(CFLAGS_vdso_standalone_test_x86) \ vdso_standalone_test_x86.c parse_vdso.c \

然后make即可生成文件vdso_test_kylin

此时我们运行vdso_test_kylin来验证是否调用,如下:

# ./vdso_test_kylin root@kylin:~/vdso# dmesg [72296.088102] kylin: Get sys_kylin call:[Userspace say:hello kylin!]. ret=0

可以发现代码正常调用了syscall,我们通过getauxval(AT_SYSINFO_EHDR);获取了vdso的代码地址,然后通过函数vdso_sym获取了"__kernel_kylin"的函数地址,然后直接运行kylin(words);,这样就实现了vdso的调用,这里我们没有-lvdso去编译。故已经完全实现了vdso的功能

四、原理解析

关于vdso的原理,我们需要具备一点elf的知识,这里elf的知识就不重复了。

首先我们通过命令获取以下信息

.dynamic .dynstr .dynsym

我们还需要知道一个知识如下:

符号地址是dynsym地址获取到st_name,然后通过dynstr的首地址+st_name获得 下面围绕这一个知识来进行验证

4.1 命令获取信息

首先获取dynamic地址,值为0x0000000000000860如下:

# readelf -l vdso.so | grep DYNAMIC DYNAMIC 0x0000000000000860 0x0000000000000860 0x0000000000000860

然后获取dynstr地址,值为0x00000000000001f8,如下:

# readelf -S vdso.so | grep dynstr -A 1 [ 3] .dynstr STRTAB 00000000000001f8 000001f8 0000000000000086 0000000000000000 A 0 0 1

然后获取dynsym地址,值为0x0000000000000150,如下:

# readelf -S vdso.so | grep dynsym -A 1 [ 2] .dynsym DYNSYM 0000000000000150 00000150 00000000000000a8 0000000000000018 A 3 1 8

此时我们获取符号表,如下:

# readelf -s vdso.so Symbol table '.dynsym' contains 7 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 OBJECT GLOBAL DEFAULT ABS LINUX_2.6.39 2: 0000000000000780 108 FUNC GLOBAL DEFAULT 7 __kernel_clock_getres@@LINUX_2.6.39 3: 00000000000007f0 8 NOTYPE GLOBAL DEFAULT 7 __kernel_rt_sigreturn@@LINUX_2.6.39 4: 00000000000005c0 424 FUNC GLOBAL DEFAULT 7 __kernel_gettimeofday@@LINUX_2.6.39 5: 0000000000000770 12 FUNC GLOBAL DEFAULT 7 __kernel_kylin@@LINUX_2.6.39 6: 0000000000000320 664 FUNC GLOBAL DEFAULT 7 __kernel_clock_gettime@@LINUX_2.6.39

目的符号是序号为5的__kernel_kylin函数

然后我们获取dynsym的size,如下:

# readelf -S vdso.so | grep dynsym -A 1 [ 2] .dynsym DYNSYM 0000000000000150 00000150 00000000000000a8 0000000000000018 A 3 1 8

这里注意size为0x0000000000000018,我们计算如下:

offset = size * index

这样的到

0x18 * 5 = 0x78

然后与dynsym的起始地址相加,如下:

0x0000000000000150 + 0x78 = 0x00000000000001c8

这里,我们获取到了dynsym里面关于符号__kernel_kylin的结构体Elf64_Sym,我们可以到elf-64-gen.pdf找到定义如下:

image.png

我们需要获取st_name的值。我们借助hexdump,如下:

# hexdump -s $((0x150)) -n $((0xa8)) vdso.so -C 00000150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000160 00 00 00 00 00 00 00 00 79 00 00 00 11 00 f1 ff |........y.......| 00000170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000180 3d 00 00 00 12 00 07 00 80 07 00 00 00 00 00 00 |=...............| 00000190 6c 00 00 00 00 00 00 00 53 00 00 00 10 00 07 00 |l.......S.......| 000001a0 f0 07 00 00 00 00 00 00 08 00 00 00 00 00 00 00 |................| 000001b0 18 00 00 00 12 00 07 00 c0 05 00 00 00 00 00 00 |................| 000001c0 a8 01 00 00 00 00 00 00 2e 00 00 00 12 00 07 00 |................| 000001d0 70 07 00 00 00 00 00 00 0c 00 00 00 00 00 00 00 |p...............| 000001e0 01 00 00 00 12 00 07 00 20 03 00 00 00 00 00 00 |........ .......| 000001f0 98 02 00 00 00 00 00 00 |........| 000001f8

此时我们查看0x1c8的值是0x0000002e,所以我们知道st_name是0x2e。

我们这时候计算符号位置即可,如下:

__kernel_kylin = dynstr + st_name

所以如下运算

0x00000000000001f8 + 0x2e = 0x0000000000000226

此时我们拿到了0x226的地址,然后计算.dynstr偏移看看对不对,如下:

# readelf -x .dynstr vdso.so “.dynstr”节的十六进制输出: 0x000001f8 005f5f6b 65726e65 6c5f636c 6f636b5f .__kernel_clock_ 0x00000208 67657474 696d6500 5f5f6b65 726e656c gettime.__kernel 0x00000218 5f676574 74696d65 6f666461 79005f5f _gettimeofday.__ 0x00000228 6b65726e 656c5f6b 796c696e 005f5f6b kernel_kylin.__k 0x00000238 65726e65 6c5f636c 6f636b5f 67657472 ernel_clock_getr 0x00000248 6573005f 5f6b6572 6e656c5f 72745f73 es.__kernel_rt_s 0x00000258 69677265 7475726e 006c696e 75782d76 igreturn.linux-v 0x00000268 64736f2e 736f2e31 004c494e 55585f32 dso.so.1.LINUX_2 0x00000278 2e362e33 3900 .6.39.

我们可以发现0x226的位置就是__kernel_kylin

至此通过命令计算完成。

4.2 代码打印

为了更好的分析代码,我们需要为parse_vdso.c添加print如下:

# git diff parse_vdso.c diff --git a/tools/testing/selftests/vDSO/parse_vdso.c b/tools/testing/selftests/vDSO/parse_vdso.c index 413f75620a35..a327a85879dc 100644 --- a/tools/testing/selftests/vDSO/parse_vdso.c +++ b/tools/testing/selftests/vDSO/parse_vdso.c @@ -20,6 +20,7 @@ #include <string.h> #include <limits.h> #include <elf.h> +#include <stdio.h> #include "parse_vdso.h" @@ -98,8 +99,10 @@ void vdso_init_from_sysinfo_ehdr(uintptr_t base) vdso_info.load_offset = base + (uintptr_t)pt[i].p_offset - (uintptr_t)pt[i].p_vaddr; + printf("kylin: vdso load_offset=%p\n", vdso_info.load_offset); } else if (pt[i].p_type == PT_DYNAMIC) { dyn = (ELF(Dyn)*)(base + pt[i].p_offset); + printf("kylin: dynamic=%p\n", dyn); } } @@ -120,11 +123,13 @@ void vdso_init_from_sysinfo_ehdr(uintptr_t base) vdso_info.symstrings = (const char *) ((uintptr_t)dyn[i].d_un.d_ptr + vdso_info.load_offset); + printf("kylin: dynstr=%p\n", vdso_info.symstrings); break; case DT_SYMTAB: vdso_info.symtab = (ELF(Sym) *) ((uintptr_t)dyn[i].d_un.d_ptr + vdso_info.load_offset); + printf("kylin: dynsym=%p\n", vdso_info.symtab); break; case DT_HASH: hash = (ELF(Word) *) @@ -217,6 +222,7 @@ void *vdso_sym(const char *version, const char *name) continue; if (sym->st_shndx == SHN_UNDEF) continue; + printf("kylin: dynsym[%d]=%p dynsym_name=%s[%p]\n", chain, sym, vdso_info.symstrings + sym->st_name, vdso_info.symstrings + sym->st_name); if (strcmp(name, vdso_info.symstrings + sym->st_name)) continue;

此时我们运行程序得到输出如下:

# ./vdso_test_kylin kylin: vdso load_offset=0x7f94e97000 kylin: dynamic=0x7f94e97860 kylin: dynstr=0x7f94e971f8 kylin: dynsym=0x7f94e97150 kylin: dynsym[5]=0x7f94e971c8 dynsym_name=__kernel_kylin[0x7f94e97226]

我们可以轻松的得到如下信息:

  • vdso代码加载地址是0x7f94e97000
  • dynamic节加载地址是0x7f94e97860
  • dynstr节加载地址是0x7f94e971f8
  • dynsym节加载地址是0x7f94e97150
  • 通过计算dynsym的第5个符号的加载地址是0x7f94e971c8
  • 符号__kernel_kylin的地址是0x7f94e97226

可以发现,和我们命令计算的完全一致。

五、结论

本文模拟了libc如何实施的vdso功能,这样所有程序都可以自动加载vdso代码段,希望能够加深大家对vdso的印象