编辑
2025-01-22
记录知识
0

根据yocto版本构建(二)安装已有二进制已经可以具备安装已有的二进制,这样一个完全仿照livebuild的方式(kybuilder)的yocto工程只需要花点时间就可以配置出来,但是我们不止如此,我们知道,麒麟v10操作系统软件包的开发是和deb开发方式一致的,通过dsc即可正常构建,如下是实现路径

一、deb.bbclass

为了支持debian包的构建,我们需要根据deb的安装方式设计一个bbclass用作bbfile的调用,我们知道,针对系统的软件开发,主要步骤如下:

wget https://xxxx.dsc dpkg -x xxxx.dsc dpkg-buildpackage -uc -us / dh_make

为了让yocto上也能使用dsc来通过源码的方式构建出一个deb包,我们能需要定义函数类似如下:

do_unpack_dsc() { dpkg-source -x ${DL_DIR}/${PN}_${PV}.dsc ${S} } do_compile_dsc() { dpkg-buildpackage -uc -us }

不仅如此,我们还需要在do_package中将其生成的deb文件,复制到recipes的work目录下。这样我们不需要使用yocto的本身构建方式来生成deb

二、bbfile

对于bbfile,我们需要继承我们的类,然后提供一个宏定义,由我们的deb.bbclass获取代码开始构建,如下:

inherit kylin inherit deb DSC_URI = "https://dev.kylinos.cn/kylin-desktop/+archive/primary/+files/ukui-menu_3.0.1-0720.1.dsc;md5sum=1b897ae127a2d8076a0b318daa720f91"

三、调试

为了让我们的bbfile支持调试,我们可以通过如下命令

bitback ukui-menu -c listtasks

这样可以列出我们的步骤,方便调试细节

如果我们需要进入devshell中,具体调试dpkg-buildpackage -uc -us出现的错误,我们可以如下

bitback ukui-menu -c devshell

这样我们可以手动运行chroot进入此环境

四、结论

至此,我们可以通过指定一个dsc文件,就可以触发yocto执行源码的编译动作

编辑
2025-01-22
记录知识
0

我们知道yocto是基于源码构建的工具,如果我们在开发系统的时候,直接使用全部构建的方式来生成操作系统,那代价将会异常的大。所以可以通过二进制安装

一、提供sysroot环境

默认情况下,yocto会根据编译来构建一个sysroot,而每个程序都有自己单独的sysroot作为隔离。这种情况下,如果我们不需要通过构建的方式产生sysroot,我们需要如下:

在meta-kylin下定义一个recipes-debootstrap 运行debootstrap命令,通过麒麟发布的源地址,构建一个chroot环境,此环境是SYSROOT_DESTDIR环境变量 yocto默认使用此sysroot作为版本构建 对于的bb如下:

do_build() { sudo -E debootstrap --variant=minibase --include=systemd,apt kylin ${SYSROOT_DESTDIR} ${KYLIN_REPO} sudo -E chroot ${SYSROOT_DESTDIR} apt-get update sudo -E chroot ${SYSROOT_DESTDIR} apt-get install -y ${DEPENDS} }

二、提供安装类bbclass

我们知道yocto分如下步骤:

image.png 对于此,我们需要将此流程定制,inherit 我们自己的kylin.bbclass,如下:

  • do_fetch : 设置为noexec
  • do_unpack: 设置为noexec
  • do_patch: 设置为noexec
  • do_configure:设置为noexec
  • do_compile:设置为noexec
  • do_install: 集成kylin的bbclass,实现chroot到系统中,apt-get install 的方式安装
  • do_package:设置为noexec

三、设置bb file

在我们的layer中的bb file,需要inherit kylindeb这样的类,这样默认就指向了我们自己的流程

我们在do_install中会进入chroot环境中进行apt-get 安装包,这里bb file需要提供一个packages-list文件,用于解析packages-list里面的包列表,用于安装,如下示例:

PACKAGE_LIST = "ukui-tablet-desktop ukui-control-center ukui-menu" do_install() { apt-get update apt-get install -y ${PACKAGE_LIST} }

四、构建recipes

最后我们通过命令即可构建

bitback kylin

至此,yocto可以具备通过安装二进制的方式来构建系统环境。

编辑
2025-01-22
记录知识
0

我们知道yocto可以用于操作系统版本构建,它与openembedded相辅相成。

  • 其优点在于,客户良好的按照菜谱和分层结构,实现多个操作系统的源码构建任务
  • 缺点是yocto的构建是通过源码编译的,而不是通过二进制构建的,构建系统花费时间太长。

而我们的工作内容中,我们是通过livebuild(u系系统)的方式构建的操作系统。这种方式是预先通过服务器进行预编译的二进制,我们通过livebuild工具执行一次构建操作,从而生成一个操作系统版本,这个优点是操作系统构建速度快,缺点是系统无法进行良好的定制扩展。

对于上述的问题,我们知道,对于livebuild的缺点,我们无法解决,因为软件的定制扩展需要依赖服务器的二进制发布,而针对yocto的缺点,我们可以解决,我们可以让yocto在构建系统的过程中,可选择性的执行全量编译,或部分编译,甚至进二进制构建的行为。

一、什么是yocto

Yocto Project 是一个开源协作项目,可帮助开发人员为嵌入式产品和其他目标环境创建基于 Linux 的自定义系统,而不管硬件架构如何。该项目提供了一套灵活的工具和空间,世界各地的嵌入式开发人员可以在其中共享技术、软件堆栈、配置和最佳实践,这些技术、软件堆栈、配置和最佳实践可用于为嵌入式设备创建定制的 Linux 和 RTOS 映像

https://www.yoctoproject.org/development/technical-overview/

根据上面我们可以知道,yocto是一个可以创建基于Linux的自定义系统的工具,如果我们掌握yocto工具,那么可以为其他目标环境创建一个自定义的操作系统。

二、为什么要用yocto

我们已经知道yocto是一套工具,它可以创建操作系统,那我们为什么要用yocto,而不去选择其他的系统构建工具呢,这里以livebuild为例,详细解释主要原因

2.1 版本现场可定制

对于yocto,它是基于源码构建操作系统的方式,如果系统中存在某个软件包需要根据实际情况定制修改,其步骤相比于livebuild更方便,如下:

对于livebuild:

在服务器上下载软件的源码 修改源码 编译源码生成二进制deb包 推送到二进制发布平台上 重新执行livebuild构建,形成新的操作系统

对于yocto:

修改源码,提供修改源码的patch文件 修改bb file,将patch文件通过quilt方式合入源码 执行recipes的编译 执行images的构建,形成新的操作系统

根据上面我们可以知道,对于livebuild,我们需要有专门的编译服务器,需要将编译的二进制包推送到发布平台,再通过livebuild脚本来执行构建,需要依赖的东西多,步骤复杂

而对于yocto,我们仅仅需要针对代码的patch,修改bb file,添加补丁,然后执行构建即可,在其规则内,简单方便即可完成定制需求

2.2 不同定制系统易维护

对于yocto,不同的bb file可以通过append方式修改和定制,不同的版本可以通过设置不同的layer实现,也就意味着基于yocto的构建版本,可以很好的处理多版本的现状,而livebuild很难做到,如下:

对于livebuild:

不同的定制版本需要上传到不同的二进制发布平台上 修改livebuild脚本,针对不同的版本设置不同的hook脚本,用于构建不同的版本 根据livebuild脚本构建操作系统镜像

对于yocto:

设置不同的meta-projectname的layer 构建不同的layer的操作系统镜像

根据上面我们可以知道,对于livebuild,如果定制版本特别多

例如上百量级,那么我们不同的二进制包需要上百个,二进制发布平台的源地址需要上百个,维护的hook脚本也有上百个
而yocto不同的是,我们所有的改动都放在一个layer中,那么我们只需要上百个layer即可。对应改动在layer上,而且yocto的layer之间可以实现overlay

也就意味着,我们不需要发布上百个二进制或源码配置,而仅仅是在原来的layer上做overlay,实现差分内容管理即可。

2.3 自顶向下和自底向上

关于此概念,其实是构建系统的两个思路,如下解释:

  • 自顶向下是描述操作系统默认是一个单独的发行版本,对于不同的需求,通过增加或移除软件包的方式来定制它。可以理解为减法
  • 自底向上是描述一个操作系统版本,是根据其根本需求来从零逐步增加软件包来实现一个系统版本。可以理解为加法

根据这里,我们可以发现,正常情况下,如果我们的操作系统功能涵盖了一切,对于其用户需求而言,仅需要增加和减少两个功能,那么自顶向下的的方法并没有问题。所以它比较适用于例如服务器(对操作系统功能越丰富越好,性能问题无需格外关心),桌面(提供一个基本操作系统环境,用户通过安装包来提供新的功能)

但是它不适用于嵌入式,通常情况下,嵌入式是一个定制性非常强的系统,例如车机系统,机器人系统,工业控制系统等。每个系统的定制功能不同,但整体性能有限,所以无法安装服务器的版本(性能和空间受限),也无法使用桌面版本(操作系统环境无法定制)。

这里,livebuild是一种减法类操作系统构建工具,而yocto默认是一种加法类操作系统构建工具。

但是不一样的是:

  • livebuild无法成为加法类构建工具,因为其只能通过二进制发布源上拉取成果
  • yocto能够成为减法类构建工具,因为其既可以通过二进制源上拉取成果,也可以通过源码构建成果

所以我们可以发现,yocto比livebuild更优。

2.4 跨平台构建

我们知道livebuild十分依赖本机环境,即使我们通过chroot进入一个新的环境,我们也是在chroot环境中进行构建版本,它没办法跨平台,例如在x86上构建arm64的操作系统。

当然,我们可以通过qemu static来实现跨平台的构建方式,但它并不友好。

yocto与其不同的是,yocto可以配置不同的平台层,使用不同的cross compile工具,使其能够从源码上构建不同平台的二进制软件

当然,yocto也可以通过qemu static来实现跨平台的二进制安装。

根据上述,我们可以知道,对于操作系统的二进制加减法,我们都可以通过qemu static工具实现,但是对于源码定制和构建,只有yocto能够做到。

放大了看,对于整个操作系统的构建行为,livebuild严格意义上是无法完成的,而yocto本身可以轻松的完成跨平台的构建系统版本,包括但不局限于如下:

x86 windows/linux 上构建 arm64 loongarch64 risc5-64 arm64 linux上构建x86-64 loongarch64 risc5-64 loongarch64 linux上构建 arm64 x86-64 risc5-64 risc5-64 linux上构建 arm64 x86-64 loongarch64

虽然我们通常是x64上构建其他平台,但是也足够说明了yocto可以做到其livebuild无法做到的事。

三、我们的现状

针对我们的行业,我们可以知道,我们主要提供操作系统的定制方案,我们已经获取了发布的操作系统版本。我们需要做的是,将这些操作系统版本应用到我们的行业场景上去。

3.1 行业上

假设我们是自顶向下的减法,定制操作系统版本,那么我们根据我们的各种行业,需要提供基于v10的各种版本。那么就会出现如下问题:

操作系统都叫V10,但是版本参差不齐,版本的数量太多,版本之间差异过多,但不清楚差异在哪,兼容困难 版本与版本之间都是通过kybuilder平台定制修改的,但是并没有直接明了的知道版本间的差异。

3.2 项目上

我们可能在某个行业,例如车载上有100个项目,那么这100个项目都集成某个基于车载的版本,最后衍生了100个项目版本。这100个项目版本均有差异。

操作系统都是车载V10版本,但是每个车载V10版本都不一样,只知道有100个车载项目版本要维护,但理不清楚每个车载版本的差异点 我们找不到这100个项目的差异,我们需要为这100个项目版本做好维护工作,而不是维护一个yocto配置这种行为。

3.3 发展方向上

统一的操作系统

四、yocto改进我们的现状

4.1 行业上的改进

yocto可以定义每个meta layer,我们可以根据行业来设置layer,不同的场景客户都基于原始v10版本上,新增layer。

这样,不同的layer中就是不同场景的定制功能需求,这些layer里面是各种功能的bb 文件

4.2 项目上的改进

yocto可以为不同的项目设置项目的layer,然后项目的改动通过设置bbappend实现。bbappend可以针对原来的行业的layer上进行追加修改和定制。例如:

meta-vehicle-project/recipes-xxx/xxx_0.1/xxx.bbappend

这里xxx是具体功能,bbappend是对原meta-vehicle的定制追加

4.3 统一的操作系统

yocto可以统一拉取操作系统源码或二进制,统一输出镜像,能够使得成为真正意义上的全场景OS。

五、结论

至此,我们的版本基于v10,提供了yocto的工程,根据此工程构建的版本都是基于v10,其他的衍生版本通过bb 和 bbappend实现。

如果客户拿到了我们的yocto仓库,客户不会认为我们存在100个不同的版本,而是1个统一的yocto版本,通过灵活构建生成了100个行业场景。

编辑
2025-01-22
记录知识
0

在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的印象

编辑
2025-01-22
记录知识
0

根据vDSO--内核原理我们可以对一个syscall来进行vDSO的优化,再根据vDSO--示例之实现系统调用我们实现了一个自己的系统调用__do_sys_kylin,这里我们将__do_sys_kylin加入到vDSO中

一、vDSO的符号

我们从lds可以发现需要导出符号,所以我们添加一个__kernel_kylin的符号给应用,代码如下:

# git diff vdso/vdso.lds.S diff --git a/arch/arm64/kernel/vdso/vdso.lds.S b/arch/arm64/kernel/vdso/vdso.lds.S index b840ab1b705c..c766afed0ec8 100644 --- a/arch/arm64/kernel/vdso/vdso.lds.S +++ b/arch/arm64/kernel/vdso/vdso.lds.S @@ -84,6 +84,7 @@ VERSION global: __kernel_rt_sigreturn; __kernel_gettimeofday; + __kernel_kylin; __kernel_clock_gettime; __kernel_clock_getres; local: *;

二、实现vDSO的调用

为了完成vDSO的实验,我没有去故意设置vvar的值来获取,而是在vDSO中嵌套了一个syscall,这样方便大家理解,如下:

# git diff vdso/vgettimeofday.c diff --git a/arch/arm64/kernel/vdso/vgettimeofday.c b/arch/arm64/kernel/vdso/vgettimeofday.c index 4236cf34d7d9..0005f42565d9 100644 --- a/arch/arm64/kernel/vdso/vgettimeofday.c +++ b/arch/arm64/kernel/vdso/vgettimeofday.c @@ -18,6 +18,22 @@ int __kernel_gettimeofday(struct __kernel_old_timeval *tv, return __cvdso_gettimeofday(tv, tz); } +int __kernel_kylin(char* words) +{ + register char *word asm("x0") = words; + register long ret asm("x0"); +#define __NR_sys_kylin 449 + register long nr asm("x8") = __NR_sys_kylin; + + asm volatile( + " svc #0\n" + : "=r" (ret) + : "r" (word), "r" (nr) + : "memory"); + + return ret; +} +

三、生成vdso.so

至此vDSO的函数实现已经完成了,我们编译vdso.so即可。编译后文件如下:

arch/arm64/kernel/vdso/vdso.so

此时我们看看符号是否增加,如下:

# readelf -s arch/arm64/kernel/vdso/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

我们看一下符号的代码段内容

# objdump --disassemble=__kernel_kylin arch/arm64/kernel/vdso/vdso.so arch/arm64/kernel/vdso/vdso.so: 文件格式 elf64-littleaarch64 Disassembly of section .text: 0000000000000770 <__kernel_kylin@@LINUX_2.6.39>: 770: d2803828 mov x8, #0x1c1 // #449 774: d4000001 svc #0x0 778: d65f03c0 ret

以上完全正确

四、测试vDSO调用

我们将arch/arm64/kernel/vdso/vdso.so作为标准动态库看待,直接复制到机器中,编写测试代码如下:

#include <sys/syscall.h> #include <stdio.h> extern int __kernel_kylin(char* words); int main(int argc, char *argv[]) { int ret = 0; char* words = "Userspace say:hello kylin!"; ret = __kernel_kylin(words); printf("vdso ret=%d \n", ret); return 0; }

此时我们构建的时候应该把vdso当作标准动态库,如下编译

gcc test_kylin_vdso.c -o test_kylin_vdso -L. -lvdso

此时我们运行:

# ./test_kylin_vdso vdso ret=0

然后查看日志

# dmesg [ 5586.379813] kylin: Get sys_kylin call:[Userspace say:hello kylin!]. ret=0

可以发现能够正常的syscall到我的系统调用,我们ltrace看看

# ltrace ./test_kylin_vdso __libc_start_main(0x5569b3083c, 1, 0x7fdfd074f8, 0x5569b30880 <unfinished ...> __kernel_kylin(0x5569b30920, 0x7fdfd074f8, 0x7fdfd07508, 0x5569b3083c) = 0 printf("vdso ret=%d \n", 0vdso ret=0 ) = 12 exit(0 <unfinished ...> __cxa_finalize(0x5569b41008, 0x5569b307f0, 0x10d68, 1) = 0x7fba4bec70 +++ exited (status 0) +++

正常调用__kernel_kylin,至此我们可以正常通过vdso来调用syscall的内容。