更新时间:2021-07-08 GMT+08:00
分享
开发指导

开发指导

使用场景

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

动态加载技术可以较好地解决上述静态链接中存在的问题,在程序需要执行外部模块中的代码时,动态地将外部模块加载进内存,不需要该模块时再卸载,可以实现公共代码的共享以及模块的平滑升级等功能。

功能

Huawei LiteOS 的动态加载模块为用户提供下面几种功能,接口详细信息可以查看API参考。

功能分类

接口名

描述

销毁动态加载

LOS_LdDestroy

销毁动态加载模块

动态加载模块文件

LOS_SoLoad

动态加载一个so模块(依赖文件系统)

LOS_MemLoad

动态加载一个so模块(不依赖文件系统,从指定内存空间加载)

LOS_ObjLoad

动态加载一个obj模块(依赖文件系统)

查找符号地址

LOS_FindSymByName

在模块或系统符号表中查找符号地址

卸载模块

LOS_ModuleUnload

卸载一个模块

设置动态加载的搜索路径/参数/内存池地址

LOS_PathAdd

添加模块的搜索路径

LOS_DynParamReg

设置so模块的动态加载参数,使用该接口需要打开LOSCFG_DYNLOAD_DYN_FROM_FS宏开关,即只支持从文件系统中加载so模块时才能设置其动态加载参数

LOS_DynMemPoolSet

设置动态加载使用的内存池地址

LOS_DynMemPoolSet接口入参必须是经过LOS_MemInit初始化的内存池地址,即通过Huawei LiteOS内存管理算法管理,并且保证该内存池与系统内存池不重合。该接口需在加载.so或者.o文件前使用。

准备编译环境

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

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

    以下列出的编译选项,可以根据实际需要自行选择。

    -z max-page-size=value

    设置.o和.so模块可加载的program segment的对齐参数为value值,添加该选项,可以有效减少各相邻可加载的segment的虚拟地址之间由于对齐需要而产生的空白区域。如果不添加该选项,默认对齐参数为0x10000。

    • IPC的动态加载需要用户保证所提供的模块文件中所有LD_SHT_PROGBITS、LD_SHT_NOBITS类型的section起始地址都是4字节对齐,否则拒绝加载该模块。
    • 生成.o和.so模块时禁止链接编译器中的标准库(即不允许使用-ldl,-lpthread,-lc等),且务必使用编译选项 -nostdlib。

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

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

  2. 修改系统的Makefile。

    编译系统镜像的Makefile必须include根目录下的config.mk文件,并使用config.mk中的LITEOS_CFLAGSLITEOS_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) $(LITEOS_DYNLDFLAGS)
        $(OBJCOPY) -O binary $(SAMPLE_OUT)/sample $(SAMPLE_OUT)/sample.bin
        $(OBJDUMP) -d $(SAMPLE_OUT)/sample >$(SAMPLE_OUT)/sample.asm

编译待加载的模块文件

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

  1. 编译.o和.so模块,并将运行所需的所有.o和.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作为编译参数,否则需要在加载a.so之前事先加载b.so。

  2. 进入Huawei_LiteOS/tools/scripts/dynload_tools目录执行sym.sh脚本,示例如下:

    $ ./sym.sh /home/wmin/customer/out 
    • sym.sh脚本的入参/home/wmin/customer/out,是.o和.so文件所在的目录的绝对路径。
    • 注意如果所需加载的.o或.so被更新了,需要重新执行该脚本。该脚本会提取.o和.so文件中的所有系统符号(非用户定义的符号),以便在编译系统镜像时由编译器计算出对应符号的地址。
    • 必须要在Huawei_LiteOS/tools/scripts/dynload_tools目录下执行该脚本。
    • 执行完sym.sh脚本后,确保Huawei_LiteOS/kernel/extended/dynload/src目录下生成了los_dynload_gsymbol.c,并且保证该文件在编译阶段参与编译,以及生成的目标文件参与链接。否则,在加载.o和.so过程中会出现符号不能定位的问题。

编写动态加载的业务代码

  1. 执行make menuconfig命令,进入Kernel ---> Enable Extend Kernel ---> Enable Dynamic Load Feature菜单,完成动态加载模块的配置。

    配置项

    含义

    取值范围

    默认值

    依赖

    LOSCFG_KERNEL_DYNLOAD

    动态加载模块的裁剪开关

    YES/NO

    YES

    LOSCFG_KERNEL_EXTKERNEL

    LOSCFG_KERNEL_DYNLOAD_DYN

    使能so文件加载

    YES/NO

    YES

    LOSCFG_KERNEL_DYNLOAD

    LOSCFG_DYNLOAD_DYN_FROM_FS

    使能so文件从文件系统加载

    YES/NO

    YES

    LOSCFG_KERNEL_DYNLOAD_DYN

    LOSCFG_DYNLOAD_DYN_FROM_MEM

    使能so文件从指定内存中加载

    YES/NO

    YES

    LOSCFG_KERNEL_DYNLOAD_DYN

    LOSCFG_KERNEL_DYNLOAD_REL

    使能obj文件加载

    YES/NO

    YES

    LOSCFG_KERNEL_DYNLOAD

    LOSCFG_DYNLOAD_REL_FROM_FS

    使能obj文件从文件系统加载

    YES/NO

    YES

    LOSCFG_KERNEL_DYNLOAD_REL

    LOSCFG_KERNLE_DYN_HEAPSIZE

    配置动态加载使用的堆大小,单位为M

    [0, OS_SYS_MEM_SIZE)

    2

    LOSCFG_KERNEL_NX && LOSCFG_KERNEL_DYNLOAD,且依赖Cortex-A芯片

    如果同时使能LOSCFG_KERNEL_NX(数据段不可执行,依赖Cortex-A芯片,其在menuconfig中对应的菜单项为:Kernel ---> Enable Data Sec NX Feature)和动态加载模块,则只支持动态加载.so文件,其他形式的加载可能会有异常,同时需要配置动态加载使用的堆大小(位于系统动态内存池的尾部)。

  2. 设置动态加载使用的内存池地址(可选)。

    调用LOS_DynMemPoolSet接口可以设置动态加载使用的内存池。如果不设置,默认使用系统内存池。

  3. 设置so文件的动态加载策略。

    在不同的应用场景下,so文件可能以ZIP或者NOZIP的形式存放于存储介质中。如果需要从文件系统中加载so文件,由于压缩文件与非压缩文件读写操作的差异性,在初始化动态加载模块之前需要指明具体的加载策略。
    DYNLOAD_PARAM_S dynloadParam = {ZIP}; // 设置ZIP或NOZIP策略
    LOS_DynParamReg(&dynloadParam);       // 设置具体的加载策略

    以ZIP格式存储的so文件必须采用ZIP加载策略,而普通的so文件使用上述两种策略都可以加载成功,建议使用NOZIP策略。如果没有设置默认采用NOZIP加载策略。

  4. 使用相对路径(可选)。

    如果在动态加载模块时想使用相对路径,可以通过LOS_PathAdd接口添加.so和.o文件所在的绝对路径:

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

    添加路径后,调用LOS_SoLoad、LOS_ObjLoad、LOS_MemLoad接口时传入文件名即可,动态加载会在添加的路径下查找指定文件。

    • 只有在调用LOS_PathAdd接口添加路径后,才能在调用动态加载模块接口时使用相对路径。
    • 可以多次调用LOS_PathAdd接口添加多个相对路径。
    • 如果添加的多个路径下有相同文件名的模块,则在加载模块时按照添加的先后依次在所有路径中查找,且只加载第一个查找到的文件。

  5. 加载用户模块。

    动态加载模块支持加载.o和.so模块。

    • 使用LOS_ObjLoad接口动态加载obj文件(注意只能从文件系统中加载obj模块):
      if ((handle = LOS_ObjLoad("/yaffs/bin/dynload/foo.o")) == NULL) {
          printf("load module ERROR!!!!!!\n");
          return 1;
      }
    • 使用LOS_SoLoad接口动态加载so文件:
      if ((handle = LOS_SoLoad("/yaffs/bin/dynload/foo.so")) == NULL) {
          printf("load module ERROR!!!!!!\n");
          return 1;
      }

      对于so文件的动态加载:

      • 如果模块A需要模块B,即模块A依赖模块B,如果明确指明了A.so依赖B.so(编译A.so时将B.so作为编译参数),那么加载A模块时会自动将B模块也加载进来。如果没有明确指明A模块与B模块的依赖关系,那么在加载A模块之前,必须保证B模块已经被成功加载。
      • 对于不支持文件系统的平台,支持直接从指定内存空间动态加载so,可以调用LOS_MemLoad接口实现。

  6. 获取用户模块中的符号地址。

    • 在特定用户模块中查找符号

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

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

      需要在全局符号表(即OS模块,包括本模块和所有其他用户模块)中查找某个符号地址时,调用LOS_FindSymByName接口,并将LOS_FindSymByName的第一个参数置NULL

      if ((funTestCase0 = LOS_FindSymByName(NULL, "printf")) == NULL) {
          printf("symbol not found\n");
          return 1;
      }

  7. 使用获取到的符号地址。

    LOS_FindSymByName返回符号地址(VOID *指针),对该符号地址转换类型后,可以使用该符号。下面针对数据类型符号和函数类型符号举例说明。
    • 整数类型符号

      现有待加载的test.c,有一全局变量UINT32 g_test = 0,可以通过如下代码获取g_test的地址。

      const char *g_pscOsOSSymtblFilePath = "/yaffs/bin/dynload/test.so";
      UINT32 * g_testPtr = NULL;
      INT8 *ptr = (INT8 *)NULL;
      if ((pOSSymtblHandler = LOS_SoLoad(g_pscOsOSSymtblFilePath)) == NULL) {
          return LOS_NOK;
      }
      if ((ptr = LOS_FindSymByName(pOSSymtblHandler, "g_test")) == NULL) {
          printf("g_uwTest not found\n");
          return LOS_NOK;
      }
      g_testPtr = (UINT32 *)ptr; /* 强制类型转换成真实的指针类型 */
    • 函数类型符号

      foo.c中定义了一个无参的函数test_0和一个有两个参数的函数test_2,编译生成foo.o。

      foo.c:
      int test_0(void)
      { 
          return 0;
      }
      int test_2(int i, int j)
      {
          return 0;
      }

      以下代码演示在demo.c中获取foo.o模块中的函数并调用。

      demo.c:
      typedef int (* TST_CASE_FUNC)();                /* 无形参函数指针类型声明 */
      typedef int (* TST_CASE_FUNC1)(UINT32);         /* 单形参函数指针类型声明 */
      typedef int (* TST_CASE_FUNC2)(UINT32, UINT32); /* 双形参函数指针类型声明 */
      
      TST_CASE_FUNC funTestCase0 = NULL;              /* 函数指针定义 */
      TST_CASE_FUNC2 funTestCase2 = NULL;
      int ret;
      
      handle = LOS_ObjLoad("/yaffs/bin/dynload/foo.o");
      funTestCase0 = LOS_FindSymByName(handle, "test_0");
      if (funTestCase0 == NULL) {
          printf("can not find the function name\n");
          return 1;
      }
      ret = funTestCase0();
      
      funTestCase2 = LOS_FindSymByName(NULL, "test_2");
      if (funTestCase2 == NULL){
          printf("can not find the function name\n");
          return 1;
      }
      ret = funTestCase2(42, 57);

  8. 卸载模块。

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

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

  9. 销毁动态加载模块。

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

    • 销毁动态加载模块时会自动卸载所有已被加载的模块,销毁后模块不能再使用。
    • 销毁动态加载模块前需确认模块不再使用。

编译系统镜像

在Huawei LiteOS源码根目录下执行make,编译系统镜像。

编译完成后,在根目录out/平台名的目录下,可以看到生成的系统镜像vs_server.bin文件。

如果待加载的.o和.so中包含了未定义的外部符号(既没有定义在这些.o和.so文件中,也不是一个合法的系统全局符号),在编译系统镜像文件时会提示相应错误,需排查错误信息,确保系统镜像编译正确。

准备系统环境

.so文件(或.o文件)需要和系统镜像文件配合使用。

  • 如果选择从文件系统加载模块,则模块文件必须放置在文件系统中,例如jffs2、yaffs、fat等文件系统。
  • 如果选择从指定内存空间加载(只能加载.so文件),则忽略下文的步骤2,只需要将模块文件烧写到指定内存空间即可。

建议操作顺序:

  1. 烧写系统镜像文件到开发板的flash中。
  2. 将.so文件(或.o文件)存储到开发板的flash中,这里分为两种情况:

    • 如果模块文件保存在可热拔插的SD卡设备上,直接将SD卡插到开发板上即可。如果需要更新.so文件(或.o文件),可将SD卡插到电脑上更新。
    • 如果模块文件保存在不可热插拔的存储设备上,可通过如下两种方式更新文件:
      1. 将模块文件编译进文件系统镜像,然后烧写文件系统镜像到开发板的flash中。
      2. 启动Huawei LiteOS系统后,通过tftp命令下载.so文件(或.o文件),示例命令如下:
        tftp -g -l /yaffs/bin/dynload/foo.so -r foo.so 10.67.211.235

  3. 启动系统进行验证。

Shell调试

在Shell里封装了一系列与动态加载有关的命令,方便用户进行调试。具体的Shell命令详细说明参见动态加载命令参考

相关文档