技術分享

NEWS

新闻

了解openKylin最新资讯,关注社区和产品动态。

NEWS

Learn about the latest news.

「​技术分享」二进制翻译中的动态库封装技术

2023-02-14 12:53:10

openKylin RISC-V版本操作系统由openKylin 社区RISC-V SIG组主导开发与维护,目前已适配了Hifive Unmatched、VisionFive等主流开发板。

自社区成立以来,openKylin社区RISC-V SIG组便致力于RISC-V领域的先进技术研究。但是由于目前RISC-V架构生态还不够完善,对RISC-V版本系统的发展产生了很大的阻碍,因此为了丰富RISC-V软硬件生态,研究动态二进制翻译技术,使x86架构的大量软件能够在RISC-V上运行便成为了openKylin社区RISC-V SIG组新的技术研究方向。

下面就和大家谈一谈目前RISC-V SIG组基于Box64在动态二进制翻译中所研究的动态库封装技术。

1.Box64 使用动态库的两种方式

一种是将x86架构的动态库移植到RISC-V的系统中,使用BOX64_LD_LIBRARY_PATH环境变量指定的amd64架构的动态库,此动态库在运行过程中通过再次动态翻译执行的方式去读取调用。简单来说就是指定环境变量到提供的amd64动态库的路径,但整个过程需要将amd64架构的动态库全部翻译,这样就会极大的影响性能。

另一种就是读取RISC-V 机器上本地的一些库,但是这种方式其实是Box64本身基于这些库的接口使用 dlopen 、dlsym等动态调用接口去读取本地动态库并使用的。虽然接口需要编写实现,但是在使用过程中却不需要再次进行指令翻译,因为这些库原本就是RISC-V 版本的本地库,因此这种方法可以极大的提升性能。


2. Wrapped架构及工作流程

wrapped是Box64中所使用的一种动态库封装技术,主要作用是对本地动态库进行统一的管理,提供统一的API获取接口,使在经过指令翻译后的可执行文件在执行过程中能够快速准确的加载使用本地动态库,实现性能的提升。

本地调用的库的明细列表都存放在Box64源代码中的src/library_lisr.h文件中,如下图所示(只显示了部分列表):

openKylin(开放麒麟)

通过上图可以看出,每个动态库都是通过GO添加到Box64的列表中的,其中GO是个宏定义,具体定义如下图所示:

openKylin(开放麒麟)

在宏定义GO中定义了四个通用的接口函数,分别是库的初始化函数、结束初始化函数、非弱函数获取接口和弱函数获取接口,并且定义了一个wrappedlib_t类型的数组wrappedlibs,数组中内容为#include "library_list.h"。根据第二个宏定义GO定义,可以理解为表示数组包含了library_list.h中的每个库,并且都按照排列顺序对应了数组中的一个元素,元素内容为库的名称以及四个功能函数接口。也就是说,添加库只需要我们往library_list.h中使用宏定义GO添加,便会自动生成对应的库的init等调用接口,并且自动添加到wrappedlibs数组中。而添加到数组的目的便是能够统一完成数组初始化及结束初始化等工作。

而在wrapped中,所有的文件命名都要以wrapped开始,对应的每个动态库首先都要包含wrappedsdl2.c和wrappedsdl2_private.h文件(以sdl2库为示例,下面都会以此动态库为示例)。其中wrappedsdl2.c文件主要完成动态库名字的定义,设置当前动态库的依赖库,以及根据自己的需求将库函数重新封装成自定义的函数接口,并通过#define EXPORT __attribute__((visibility("default")))宏定义向外暴露接口,而自定义的接口则通过GOM/GOWM(弱函数和非弱函数)添加到MAPNAME(mysymbolmap)中。此外,每个.c文件都必须包含“wrappedlib_init.h”文件,下图为“wrappedlib_init.h”文件部分内容:

openKylin(开放麒麟)

openKylin(开放麒麟)

根据上图中宏定义MAPNAME(N)和GO可以看出,其定义了多个map_onesymbol_t类型的数组,

openKylin(开放麒麟)

数组的名字是LIBNAME##symbolmap,如sdl2symbolmap[],LIBNAME在wrappedsdl2.c文件中会有定义。并且由于宏定义作用域的限制,每个数组存储的内容不同,如MAPNAME(symbolmap)中只存储GO和GOW两个宏的内容,MAPNAME(mysymbolmap)只存储GOM和GOWM宏的内容。数组的内容是#include wrappedsdl2_private.h,此文件中通过宏定义GO声明了库中需要引用的函数及数据(包括动态库中暴露出来的需要使用的一些变量等)列表,如图:

openKylin(开放麒麟)

wrapped中所有的GO相关的宏定义都会通过#undef的方式设置作用域,根据当前wrappedlib_init.h文件中的宏定义GO,数组内容为sdl2symbolmap[]={{SDL_abs, iFi, 0}....};上图宏定义GO中的第二个参数,可以理解为对应统一类型函数的签名,每个字符具有特定的意义,如iFi中, 第一个i表示返回值类型,F表示这是一个函数包装器,第二个i 表示函数的参数类型(列表定义在wrapper.c及wrapper.h文件中)。

通过宏定义GO将他们一起存到数组里,且宏定义GO中的0和1为弱函数标志。如上文中提到的,每个动态库都会默认有四个通用的函数接口wrapped##N##_init(),wrapped##N##_fini(),wrapped##N##_get(),wrapped##N##_getnoweak(); 这四个接口同样定义在wrappedlib_init.h文件中,init函数会自动调用 dlopen 打开对应的本地动态库,并将库函数的名字及其签名,以及数据机器对应的长度等存放进不同的哈希表中,并且会将弱函数与非弱函数、动态库中原始的函数与自定义的函数区别开。如下图,共定义了三个哈希表名字,但指向不同的哈希表地址。

openKylin(开放麒麟)

openKylin(开放麒麟)

其中,finit函数会在调用时释放资源,清空数据结构,并且关闭动态库链接。_get函数和_getnoweak函数定义了如何从哈希表中获取正确的函数接口 ,并调用dlsym 去获取需要调用的接口函数并使用。


3.自定义数据类型及自定义函数

在wrapped中还有generated这个文件夹,里面包含了每个库的defs.h、types.h和undefs.h三个文件。这个文件夹可能很多动态库中的内容都为空,但他们却有着实际的作用。在一些动态库提供的接口中,会有除去基本数据类型之外的其他自定义的数据类型,如在wrappedsdl2_private.h文件中有这样一句,

openKylin(开放麒麟)

标注为G (签名)对应的数据类型为 SDL_JoystickGUID 等同于包装中的字符UU ,即在查找要使用的包装器时替换为UU。这些自定义的数据类型在private.h文件中进行备注,在defs.h文件中进行定义,并在undefs.h中取消定义。如在wrappedsdl2defs.h和wrappedsdlundefs.h中会有如下代码:

openKylin(开放麒麟)

openKylin(开放麒麟)

types.h文件中列举了需要自定义封装的函数列表,并且自定义了函数类型,如下图展示了部分内容:

openKylin(开放麒麟)

GO的第一个参数为对应的库函数,第二为对应的自定义的函数类型,每个字符可以理解为函数签名,同时所有的函数都会在wrappedsdl2.c文件中完成二次封装并向外暴露接口。而需要自定义封装的库函数则会在wrappercallback.h中定义一个结构体,

openKylin(开放麒麟)

每个动态库都会对应一个结构体,其中内容为自定义的类型,对应值为库函数。

openKylin(开放麒麟)

然后会通过调用getMy以及freeMy对结构体进行初始化以及释放操作。在对库函数进行二次封装时,直接通过结构体变量调用库函数即可。

如上所述则是Box64中的wrapped功能,当我们需要往Box64添加RISC-V本地库时,只需要添加wrapped##LIBNAME.c,wrapped##LIBNAME_private.h,wrapped##LIBNAMEdefs.h,wrapped##LIBNAMEundefs.h,wrapped##LIBNAMEtypes.h5个文件,wrapped便会自动完成初始化及通用接口的定义。