liteos动态加载(十三)

1. 概述

1.1 基本概念

动态加载是一种程序加载技术。

静态链接是在链接阶段将程序各模块文件链接成一个完整的可执行文件,运行时作为整体一次性加载进内存。动态加载允许用户将程序各模块编译成独立的文件而不将它们链接起来,在需要使用到模块时再动态地将其加载到内存中。

静态链接将程序各模块文件链接成一个整体,运行时一次性加载入内存,具有代码装载速度快等优点。但当程序的规模较大,模块的变更升级较为频繁时,会存在内存和磁盘空间浪费、模块更新困难等问题。

动态加载技术可以较好地解决上述静态链接中存在的问题,在程序需要执行所依赖的模块中的代码时,动态地将外部模块加载链接到内存中,不需要该模块时可以卸载,可以提供公共代码的共享以及模块的平滑升级等功能。Huawei LiteOS提供支持OBJ目标文件和SO共享目标文件的动态加载机制。

liteos动态加载(十三)

Huawei LiteOS的动态加载功能需要SO共享目标文件(或OBJ目标文件)、基础符号表elf_symbol.so、系统镜像bin文件配合使用。

1.2 动态加载相关概念

符号表

符号表在表现形式上是记录了符号名及其所在内存地址信息的数组,符号表在动态加载模块初始化时被载入到动态加载模块的符号管理结构中。在加载用户模块进行符号重定位时,动态加载模块通过查找符号管理结构得到相应符号所在地址,对相应重定位项进行重定位。

2. 开发指导

接口名 描述
LOS_LdInit 初始化动态加载模块
LOS_LdDestroy 销毁动态加载模块
LOS_SoLoad 动态加载一个so模块
LOS_ObjLoad 动态加载一个obj模块
LOS_FindSymByName 在模块或系统符号表中查找符号地址
LOS_ModuleUnload 卸载一个模块
LOS_PathAdd 添加一个相对路径

2.1 开发流程

动态加载主要有以下几个步骤:

  1. 编译环境准备
  2. 基础符号表elf_symbol.so的获取及镜像编译
  3. 动态加载接口使用
  4. 系统环境准备

2.2 编译环境准备

步骤1 添加.o和.so模块编译选项

  • .o模块的编译选项中需要添加-mlong-calls -nostdlib选项
  • .so模块的编译选项中需要添加-nostdlib -fPIC -shared选项

IPC的动态加载需要用户保证所提供的模块文件中所有LD_SHT_PROGBITS、LD_SHT_NOBITS类型节区起始地址都4字节对齐,否则拒绝加载该模块

.o和.so模块编译选项添加示例如下:

RM = -rm -rf
CC = arm-hisiv500-linux-gcc
SRCS = $(wildcard *.c)
OBJS = $(patsubst %.c,%.o,$(SRCS))
SOS = $(patsubst %.c,%.so,$(SRCS))
all: $(SOS)
$(OBJS): %.o : %.c
@$(CC) -mlong-calls -nostdlib -c $< -o $@
$(SOS): %.so : %.c
@$(CC) -mlong-calls -nostdlib $< -fPIC -shared -o $@
clean:
@$(RM) $(SOS) $(OBJS)
.PHONY: all clean

步骤2 系统镜像

系统镜像bin文件编译makefile必须include根目录下config.mk文件,并使用其中的LITEOS_CFLAGS或LITEOS_CXXFLAGS编译选项,示例如下:

LITEOSTOPDIR ?= ../..
SAMPLE_OUT = .
include $(LITEOSTOPDIR)/config.mk
RM = -rm -rf
LITEOS_LIBDEPS := --start-group $(LITEOS_LIBDEP) --end-group
SRCS = $(wildcard sample.c)
OBJS = $(patsubst %.c,$(SAMPLE_OUT)/%.o,$(SRCS))
all: $(OBJS)
clean:
@$(RM) *.o sample *.bin *.map *.asm
$(OBJS): $(SAMPLE_OUT)/%.o : %.c
$(CC) $(LITEOS_CFLAGS) -c $< -o $@
$(LD) $(LITEOS_LDFLAGS) -uinit_jffspar_param --gc-sections -Map=$(SAMPLE_OUT)/sample.map -o $
(SAMPLE_OUT)/sample ./$@ $(LITEOS_LIBDEPS) $(LITEOS_TABLES_LDFLAGS)
$(OBJCOPY) -O binary $(SAMPLE_OUT)/sample $(SAMPLE_OUT)/sample.bin
$(OBJDUMP) -d $(SAMPLE_OUT)/sample >$(SAMPLE_OUT)/sample.asm

2.3 基础符号表 elf_symbol.so 的获取及镜像编译

请严格按如下步骤进行编译。

步骤1 编译.o和.so模块,并将系统运行所需的所有.o和.so文件拷贝到一个目录下,例如

/home/wmin/customer/out/so

说明

  1. 如果a.so需要调用b.so中的函数,或者a.so引用到了b.so中的数据,则称a.so依赖b.so。
  2. 当用户中a.so模块依赖b.so,且需要在加载a.so时自动将b.so也加载进来,则在编译a.so时需要将b.so作为编译参数。

步骤2 进入Huawei_LiteOS/tools/scripts/dynload_tools目录执行如下脚本命令

$ ./ldsym.sh /home/wmin/customer/out/so
  1. “$”是linux shell提示符,下同
  2. ldsym.sh脚本只需传入系统运行所需的所有.o和.so文件所在的那个目录绝对路径即可。
  3. 目录路径必须是绝对路径。
  4. 必须要在Huawei_LiteOS/tools/scripts/dynload_tools目录下执行该命令

步骤3 编译系统镜像bin文件, 同时生成镜像文件,例如该目录在/home/wmin/customer/out/bin/,编译后在该目录下生成了sample镜像文件和用于烧写flash的sample.bin文件。

步骤4 进入Huaw_LiteOS/tools/scripts/dynload_tools目录执行sym.sh脚本得到基础符号表elf_symbol.so文件,示例如下:

$ ./sym.sh /home/wmin/customer/out/so arm-hisiv500-linux- /home/wmin/customer/out/bin/vs_server
  1. sym.sh的三个参数分别是:系统运行所需的所有.o和.so文件所在的那个目录绝对路径,编译

    器类型,系统镜像文件(不是烧写flash用的bin文件)。
  2. 所有参数中路径都必须是绝对路径。
  3. 第三个参数必须是系统镜像文件,不是烧写flash用的bin文件。
  4. 基础符号表elf_symbol.so文件生成在系统镜像文件同一路径下。
  5. 注意每次系统镜像的重新编译,都要将基础符号表elf_symbol.so重新生成并更新。
  6. 必须要在Huawei_LiteOS/tools/scripts/dynload_tools目录下执行该命令。

步骤5 进入Huawi_LiteOS/tools/scripts/dynload_tools目录执行failed2reloc.py脚本得到用户模块中无法完成重定位的符号:

$ ./failed2reloc.py /home/wmin/customer/out/bin/vs_server /home/wmin/customer/out/so
  1. failed2reloc.py的两个参数分别是:系统镜像文件(不是烧写flash用的bin文件),系统运行所

    需的所有.o和.so文件所在的那个目录绝对路径。
  2. 所有参数中路径都必须是绝对路径。
  3. 第一个参数必须是系统镜像文件,不是烧写flash用的bin文件。
  4. 该脚本输出用户模块中无法完成重定位的符号信息。

2.4 动态加载接口

步骤1 初始化动态加载模块

  • 在使用动态加载特性前,需要调用LOS_LdInit接口初始化动态加载模块:
if (LOS_OK != LOS_LdInit("/yaffs/bin/dynload/elf_symbol.so", NULL, 0)) {
printf("ld_init failed!!!!!!\n");
return 1;
}

LOS_LdInit函数第一个参数是基础符号表文件路径,第二个参数是动态加载模块进行内存分配的堆起始地址,第三个参数是这块作为堆使用的内存的长度,在LOS_LdInit接口中会将这段内存初始化为堆;如果用户希望动态加载模块从系统堆上分配内存,第二个参数传入NULL,第三个参数被忽略。

  1. 动态加载模块的初始化只需要在业务启动时调用一次即可,重复初始化动态加载模块会返回失败。

上面这段代码演示使用系统堆,如下代码演示自定义堆:

#define HEAP_SIZE0x8000
INT8 usrHeap[HEAP_SIZE];
if (LOS_OK != LOS_LdInit("/yaffs/bin/dynload/elf_symbol.so, usrHeap,sizeof(usrHeap)")) {
printf("ld_init failed!!!!!!\n")
return 1;
}
  1. 用户不需要对自定义的这块内存进行初始化动作。
  2. 动态加载所需分配的堆内存大小视要加载的模块而定,因此如果用户需要指定自定义堆时,需要保证堆长度足够大,否则建议使用系统堆。

步骤2 加载用户模块

  • IPC的动态加载模块支持对.o以及.so模块的动态加载,对于obj文件的动态加载使用LOS_ObjLoad接口:
if ((handle = LOS_ObjLoad("/yaffs/bin/dynload/foo.o")) == NULL){
printf("load module ERROR!!!!!!\n");
return 1;
}
  • 对于so文件的动态加载使用LOS_SoLoad接口:
if ((handle = LOS_SoLoad("/yaffs/bin/dynload/foo.so")) == NULL){
printf("load module ERROR!!!!!!\n");
return 1;
}

对于so文件的动态加载,如果一个模块A需要另一个模块B,也就是存在模块A依赖于模块B的关系,则在加载A模块时会将模块B也加载进来。

步骤3 获取用户模块中的符号地址

  • 在特定模块中查找符号

需要在某个特定模块中查找用户模块的符号地址时,调用LOS_FindSymByName接口,并将LOS_FindSymByName的第一个参数置为需要查找的用户模块的句柄。

if ((ptr_magic = LOS_FindSymByName(handle, "os_symbol_table")) == NULL) {
printf("symbol not found\n");
return 1;
}
  • 在全局符号表中查找符号

    需要在全局符号表(即OS模块,包括本模块和所有其他用户模块)中查找某个符号的地址时,调用LOS_FindSymByName接口,并将LOS_FindSymByName的第一个参数置NULL。
if ((pFunTestCase0 = LOS_FindSymByName(NULL, "printf")) == NULL) {
printf("symbol not found\n");
return 1;
}

步骤4 使用获取到的符号地址: LOS_FindSymByName返回一个符号的地址(VOID *指针),用户在拿到这个指针之后可以做相应的类型转换来使用该符号,下面举两个例子说明,一个针对数据类型符号,一个针对函数类型符号。

  • 结构体数组类型符号(演示说明数据类型的符号使用)现有结构体KERNEL_SYMBOL定义如下:
typedef struct KERNEL_SYMBOL {
UINT32 uwAddr;
INT8 *pscName;
} KERNEL_SYMBOL;

"/bin/dynload/elf_symbol.so"模块中定义了结构体数组:

KERNEL_SYMBOL los_elf_symbol_table[LENGTH_ARRAY] = {
{0x83040000, "__exception_handlers"},
{0x8313b864, "cmd_version"},

{0, 0},
};

通过如下代码遍历los_elf_symbol_table中的各项:

const char *g_pscOsOSSymtblFilePath = "/yaffs/bin/dynload/elf_symbol.so";
const char *g_pscOsSymtblInnerArrayName = "los_elf_symbol_table";
typedef KERNEL_SYMBOL (*OS_SYMTBL_ARRAY_PTR)[LENGTH_ARRAY];/* 结构体数组指针类型声明 */
OS_SYMTBL_ARRAY_PTR pstSymtblPtr = (OS_SYMTBL_ARRAY_PTR)NULL;
VOID *pPtr = (VOID *)NULL;
UINT32 uwIdx = 0;
UINT32 uwAddr;
INT8 *pscName = (INT8 *)NULL;
if ((pOSSymtblHandler = LOS_SoLoad(g_pscOsOSSymtblFilePath)) == NULL) {
return LOS_NOK;
}
if ((pPtr = LOS_FindSymByName(pOSSymtblHandler, g_pscOsSymtblInnerArrayName)) == NULL) {
printf("os_symtbl not found\n");
return LOS_NOK;
}
pstSymtblPtr = (OS_SYMTBL_ARRAY_PTR)pPtr;/* 强制类型转换成真实的指针类型 */
uwAddr = (*pstSymtblPtr)[0].uwAddr;
pscName = (*pstSymtblPtr)[0].pscName;
while (uwAddr != 0 && pscName != 0) {
++uwIdx;
uwAddr= (*pstSymtblPtr)[uwIdx].uwAddr;
pscName= (*pstSymtblPtr)[uwIdx].pscName;
}
  • 函数类型符号

    foo.c中定义了一个无参的函数test_0和一个有两个参数的函数test_2,编译生成foo.o,代码演示在demo.c中获取foo.o模块中的函数并调用。
foo.c:
int test_0(void) { return 0; }
int test_2(int i, int j) { return 0; }
demo.c
typedef unsigned int (* TST_CASE_FUNC)();/* 无形参函数指针类型声明 */
typedef unsigned int (* TST_CASE_FUNC1)(UINT32); /* 单形参函数指针类型声明 */
typedef unsigned int (* TST_CASE_FUNC2)(UINT32, UINT32); /* 双形参函数指针类型声明 */
TST_CASE_FUNC pFunTestCase0 = NULL;/* 函数指针定义 */
TST_CASE_FUNC2 pFunTestCase2 = NULL;
handle = LOS_ObjLoad("/yaffs/bin/dynload/foo.o");
pFunTestCase0 = NULL;
pFunTestCase0 = LOS_FindSymByName(handle, "test_0");
if (pFunTestCase0 == NULL){
printf("can not find the function name\n");
return 1;
}
uwRet = pFunTestCase0();
pFunTestCase2 = NULL;
pFunTestCase2 = LOS_FindSymByName(NULL, "test_2");
if (pFunTestCase2 == NULL){
printf("can not find the function name\n");
return 1;
}
uwRet = pFunTestCase2(42, 57);

步骤5 卸载模块

当要卸载一个模块时,调用LOS_ModuleUnload接口,将需要卸载的模块句柄作为参数传入该接口。对于已被加载过的obj或so文件的句柄,卸载时统一使用LOS_ModuleUnload接口。

uwRet = LOS_ModuleUnload(handle);
if (uwRet != LOS_OK) {
printf("unload module failed");
return 1;
}

步骤6 销毁动态加载模块

不再需要动态加载功能时,调用LOS_LdDestroy接口,卸载动态加载模块。

销毁动态加载模块时会自动卸载掉所有已被加载的模块。

uwRet = LOS_LdDestroy();
if (uwRet != LOS_OK) {
printf("destroy dynamic loader failed");
return 1;
}

在业务不再需要动态加载模块时销毁动态加载模块,该接口是与LOS_LdInit配对的接口。在销毁动态加载模块后,如果业务后续再需要动态加载必须再调用LOS_LdInit重新初始化动态加载模块。

步骤7 使用相对路径

用户在使用动态加载接口时,如果想使用相对路径,也即使用类似环境变量的机制时,需要通过LOS_PathAdd接口添加相对路径:

uwRet = LOS_PathAdd("/yaffs/bin/dynload");
if (uwRet != LOS_OK) {
printf("add relative path failed");
return 1;
}

添加相对路径后,用户在调用LOS_LdInit、 LOS_SoLoad、 LOS_ObjLoad接口时传入文件名即可,而无需再传入完整的绝对路径,动态加载会在用户添加的相对路径下查找相应模块。

如果用户传入的多个路径下有相同文件名的模块,则动态加载在加载模块时按照添加的先后依次在所有相对路径中查找,且只加载第一个查找到的文件。

  1. 只有在调用LOS_PathAdd接口添加相对路径后,才能在调用动态加载接口时使用相对路径。
  2. 用户可以通过多次调用LOS_PathAdd接口添加多个相对路径

2.5 系统环境准备

SO共享目标文件(或OBJ目标文件)、基础符号表elf_symbol.so、系统镜像bin文件配合使用。

其中SO共享目标文件(或OBJ目标文件)、基础符号表elf_symbol.so必须放置在文件系统中,例如jffs2、 yaffs、 fat等文件系统。

建议操作顺序:

  1. 烧写系统镜像bin文件到flash中,该镜像默认不启动动态加载功能
  2. 如果SO共享目标文件(或OBJ目标文件)、基础符号表elf_symbol.so路径为可热拔插的SD卡设备,则可在电脑更新SO共享目标文件(或OBJ目标文件)、基础符号表elf_symbol.so到SD卡指定路径
  3. 如果SO共享目标文件(或OBJ目标文件)、基础符号表elf_symbol.so路径为jffs2、 yaffs文件系统,则可通过如下两种方式更新:
    1. 烧写文件系统镜像
    2. 系统启动后tftp命令下载SO共享目标文件(或OBJ目标文件)、基础符号表elf_symbol.so,示例命令如下:
tftp -g -l /yaffs0/foo.so -r foo.so 10.67.211.235
tftp -g -l / yaffs0/elf_symbol.so -r elf_symbol.so 10.67.211.235
  1. 启动系统动态加载功能,进行验证

2.6 Shell 调试

在Shell里我们封装了一系列与动态加载有关的命令,方便用户进行调试。

具体的Shell命令详细说明参见命令参考。

  • 初始化动态加载模块

当用户需要在Shell中调试动态加载特性的时候,需要首先初始化动态加载模块。

Shell命令: ldinit

Huawei LiteOS# ldinit /yaffs/bin/dynload/elf_symbol.so
Huawei LiteOS#

动态加载过程中发现符号重定义只作为一个warning而不作为error处理,所以仅反馈符号重定义而不返回其他错误信息表示动态加载模块初始化成功.

  • 加载一个模块

Shell命令: mopen

Huawei LiteOS# mopen /yaffs/bin/dynload/foo.o
module handle: 0x80391928
Huawei LiteOS#

(1)模块路径必须要用绝对路径

(2)必须要先初始化动态加载模块再加载模块。

  • 查找一个符号

    Shell命令: findsym
Huawei LiteOS# findsym 0 printf
symbol address:0x8004500c
Huawei LiteOS#
Huawei LiteOS# findsym 0x80391928 test_0
symbol address:0x8030f241
Huawei LiteOS#
  • 调用一个符号

    Shell 命令: call
Huawei LiteOS# call 0x8030f241
test_0
Huawei LiteOS#
  • 卸载一个模块

Shell命令: mclose

Huawei LiteOS# mclose 0x80391928
Huawei LiteOS#

3. 编程实例

3.1 实例描述

foo.c:
int test_0(void) { printf("test_0\n"); return 0; }
int test_2(int i, int j) { printf("test_2: %d %d\n", i, j); return 0; }
demo.c:
typedef int (* TST_CASE_FUNC)(); /* 无形参函数指针类型声明 */
typedef int (* TST_CASE_FUNC1)(UINT32); /* 单形参函数指针类型声明 */
typedef int (* TST_CASE_FUNC2)(UINT32, UINT32); /* 双形参函数指针类型声明 */
if (LOS_OK != LOS_LdInit("/yaffs/bin/dynload/elf_symbol.so", NULL, 0)) {
printf("ld_init failed!!!!!!\n");
return 1;
}
unsigned int uwRet;
TST_CASE_FUNC pFunTestCase0 = NULL;/* 函数指针定义 */
TST_CASE_FUNC2 pFunTestCase2 = NULL;
handle = LOS_ObjLoad("/yaffs/bin/dynload/foo.o");
pFunTestCase0 = NULL;
pFunTestCase0 = LOS_FindSymByName(handle, "test_0");
if (pFunTestCase0 == NULL){
printf("can not find the function name\n");
return 1;
}
uwRet = pFunTestCase0(); /* 调用该函数指针 */
pFunTestCase2 = NULL;
pFunTestCase2 = LOS_FindSymByName(NULL, "test_2");
if (pFunTestCase2 == NULL){
printf("can not find the function name\n");
return 1;
}
uwRet = pFunTestCase2(42, 57); /* 调用该函数指针 */
uwRet = LOS_ModuleUnload(handle);
if (uwRet != LOS_OK) {
printf("unload module failed");
return 1;
}
uwRet = LOS_LD_Destroy();
if (uwRet != LOS_OK) {
printf("destroy dynamic loader failed");
return 1;
}

3.2 结果验证

编译运行得到的结果为:

Huawei LiteOS#
*****************************
*****************************
test_0
test_2:42 57
*****************************
*****************************
上一篇:Netty堆外内存泄漏排查,这一篇全讲清楚了


下一篇:补习系列(10)-springboot 之配置读取