Zephyr 从 v3.5.0 开始引入动态模块,可以在运行时加载可执行文件,由于 module 这个名称已经被外部模块占用,因此 Zephyr 动态模块被叫做 llext ( Linkable Loadable Extensions ) , 关于起名有很长的讨论,有兴趣的同学可以在下面 PR 查看到 https://github.com/zephyrproject-rtos/zephyr/pull/57086
使用示例 链接到标题
Demo 链接到标题
Zephyr 提供一个简单的示例展示如何使用 llext
准备支持 llext 的 Zephyr Image 链接到标题
在 zephyr/samples/subsys/llext/shell_loader
下是一个 llext 的示例代码,由于 llext 的 shell loader 已经实现在 zephyr/subsys/llext/shell.c
下,因此 sample 无需写和 llext 相关的测试代码,开启下面配置项即可
CONFIG_LLEXT=y
CONFIG_LLEXT_HEAP_SIZE=32
CONFIG_LLEXT_SHELL=y
由于目前只有 armv7 支援 llext,因此该示例需要跑在一颗 armv7 的 SOC 上,我用的是 mm_feather, 按照下面方式进行编译烧写
west build -b mm_feather zephyr/samples/subsys/llext/shell_loader
west flash
烧写完成后通过 shell 就可以执行 llext 相关的命令
uart:~$ llext help
llext - Loadable extension commands
Subcommands:
list : List loaded extensions and their size in memory
load_hex : Load an elf file encoded in hex directly from the shell input.
Syntax:
<ext_name> <ext_hex_string>
unload : Unload an extension by name. Syntax:
<ext_name>
list_symbols : List extension symbols. Syntax:
<ext_name>
call_fn : Call extension function with prototype void fn ( void ) . Syntax:
<ext_name> <function_name>
准备被加载模块 链接到标题
在 zephyr 中为 llext 准备了一个简单的测试程序 zephyr/tests/subsys/llext/hello_world/hello_world.c
# include <stdint.h>
extern void printk ( char *fmt, ... ) ;
static const uint32_t number = 42;
extern void hello_world ( void )
{
printk ( "hello world\n" ) ;
printk ( "A number is %lu\n", number ) ;
}
对该文件只编译不链接
arm-zephyr-eabi-gcc -mlong-calls -mthumb -c -o hello_world.elf tests/subsys/llext/hello_world/hello_world.c
将生成的 elf 转换为 16 进制的文本格式,方便传送到 shell
xxd -p hello_world.elf | tr -d '\n'
得到的结果是
7f454c4601010100000000000000000001002800010000000000000000000000680200000000000534000000000028000b000a0080b500af084b1800084b00f013f82a22074b11001800054b00f00cf8c046bd4680bc01bc0047c0460400000000000000140000001847c0462a00000068656c6c6f20776f726c640a0000000041206e756d62657220697320256c750a00004743433a20285a65706879722053444b20302e31362e31292031322e322e30004129000000616561626900011f000000053454000602080109011204140115011703180119011a011e06000000000000000000000000000000000100000000000000000000000400f1ff000000000000000000000000030001000000000000000000000000000300030000000000000000000000000003000400000000000000000000000000030005000f00000000000000000000000000050012000000000000000400000001000500190000000000000000000000000001000f0000002800000000000000000001001900000034000000000000000000010000000000000000000000000003000600000000000000000000000000030007001c000000010000003400000012000100280000000000000000000000100000000068656c6c6f5f776f726c642e63002464006e756d6265720024740068656c6c6f5f776f726c64007072696e746b000028000000020500002c000000020e00003000000002050000002e73796d746162002e737472746162002e7368737472746162002e72656c2e74657874002e64617461002e627373002e726f64617461002e636f6d6d656e74002e41524d2e6174747269627574657300000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f0000000100000006000000000000003400000038000000000000000000000004000000000000001b000000090000004000000000000000fc0100001800000008000000010000000400000008000000250000000100000003000000000000006c00000000000000000000000000000001000000000000002b0000000800000003000000000000006c0000000000000000000000000000000100000000000000300000000100000002000000000000006c00000025000000000000000000000004000000000000003800000001000000300000000000000091000000210000000000000000000000010000000100000041000000030000700000000000000000b20000002a0000000000000000000000010000000000000001000000020000000000000000000000dc000000f0000000090000000d000000040000001000000009000000030000000000000000000000cc0100002f0000000000000000000000010000000000000011000000030000000000000000000000140200005100000000000000000000000100000000000000
运行动态模块 链接到标题
通过 shell 命令将 llext 的 hex 文本加载进内存,llext 名为 hello_world
llext load_hex hello_world 7f454c4601010100000000000000000001002800010000000000000000000000680200000000000534000000000028000b000a0080b500af084b1800084b00f013f82a22074b11001800054b00f00cf8c046bd4680bc01bc0047c0460400000000000000140000001847c0462a00000068656c6c6f20776f726c640a0000000041206e756d62657220697320256c750a00004743433a20285a65706879722053444b20302e31362e31292031322e322e30004129000000616561626900011f000000053454000602080109011204140115011703180119011a011e06000000000000000000000000000000000100000000000000000000000400f1ff000000000000000000000000030001000000000000000000000000000300030000000000000000000000000003000400000000000000000000000000030005000f00000000000000000000000000050012000000000000000400000001000500190000000000000000000000000001000f0000002800000000000000000001001900000034000000000000000000010000000000000000000000000003000600000000000000000000000000030007001c000000010000003400000012000100280000000000000000000000100000000068656c6c6f5f776f726c642e63002464006e756d6265720024740068656c6c6f5f776f726c64007072696e746b000028000000020500002c000000020e00003000000002050000002e73796d746162002e737472746162002e7368737472746162002e72656c2e74657874002e64617461002e627373002e726f64617461002e636f6d6d656e74002e41524d2e6174747269627574657300000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f0000000100000006000000000000003400000038000000000000000000000004000000000000001b000000090000004000000000000000fc0100001800000008000000010000000400000008000000250000000100000003000000000000006c00000000000000000000000000000001000000000000002b0000000800000003000000000000006c0000000000000000000000000000000100000000000000300000000100000002000000000000006c00000025000000000000000000000004000000000000003800000001000000300000000000000091000000210000000000000000000000010000000100000041000000030000700000000000000000b20000002a0000000000000000000000010000000000000001000000020000000000000000000000dc000000f0000000090000000d000000040000001000000009000000030000000000000000000000cc0100002f0000000000000000000000010000000000000011000000030000000000000000000000140200005100000000000000000000000100000000000000
通过 shell 命令调用 hello_world 模块中 hello_world 符号
llext call_fn hello_world hello_world
将会执行到 hello_world
函数,显示下面结果
hello world
A number is 42
代码使用 链接到标题
这里演示如何通过代码加载和执行文件系统内的 elf,其作用和前面的 shell demo 一致,只是将加载的文件放到了 lfs 文件系统,将 llext 名称改为了 hello
实现文件系统的 elf loader 链接到标题
elf loader 的作用是读取 elf 数据,Zephyr 只提供了基于内存 buf_loader,这里需要实现基于文件系统 loader。
读取 elf 由 seek 和 read 两个函数完成,其原型定义在 loader.h
中
struct llext_loader {
int ( *read ) ( struct llext_loader *ldr, void *out, size_t len ) ;
int ( *seek ) ( struct llext_loader *s, size_t pos ) ;
/** @cond ignore */
elf_ehdr_t hdr;
elf_shdr_t sects [LLEXT_SECT_COUNT];
uint32_t *sect_map;
uint32_t sect_cnt;
uint32_t sym_cnt;
/** @endcond */
};
实现
static struct fs_file_t elf_file;
static struct llext_loader elf_loader;
//按照 read 和 seek 的原型实现
int elf_lfs_read ( struct llext_loader *ldr, void *out, size_t len )
{
return fs_read ( &elf_file, out, len ) ;
}
int elf_lfs_seek ( struct llext_loader *s, size_t pos )
{
return fs_seek ( &elf_file, pos, FS_SEEK_SET ) ;
}
int dym_elf_init ( const char *path, char *name )
{
//打开要加载的文件
fs_file_t_init ( &elf_file ) ;
rc = fs_open ( &elf_file, path, FS_O_READ ) ;
if ( rc < 0 ) {
LOG_ERR ( "FAIL: open %s: %d", fname, rc ) ;
return rc;
}
//将 loader 函数注册进 elf_loader
elf_loader.read = elf_lfs_read;
elf_loader.seek = elf_lfs_seek;
//执行 elf 加载
struct llext *ext;
return llext_load ( &elf_loader, name, &ext ) ;
}
加载执行 链接到标题
//加载 lfs 中存放的 hello_world.elf 文件,llext 名指定为 hello
dym_elf_init ( "/lfs/hello_world.elf", "hello" ) ;
//获取名为 hello 的 llext
struct llext *ext = llext_by_name ( "hello" ) ;
//执行 hello 中 hello_workd 符号
llext_call_fn ( ext, "hello_world" ) ;
卸载 链接到标题
如果不再执行动态模块中的函数,可以将其从内存中移除,释放出内存
//获取名为 hello 的 llext
struct llext *ext = llext_by_name ( "hello" ) ;
//将其从内存中卸载出去
llext_unload ( ext ) ;
动态模块链接 Zephyr 符号说明 链接到标题
前面举例的动态模块在运行时需要重定位链接 Zephyr 中的函数符号,这需要在 Zephyr 中导出这个符号,例如 printk
在 zephyr/lib/os/printk.c
中进行了导出
EXPORT_SYMBOL ( printk ) ;
所以编写代码的时候不用再写一次,但如果你要用到新的符号就需要自行进行导出,例如再动态模块中要使用 strcpy
,就需要在 Zephyr 这一侧添加
EXPORT_SYMBOL ( strcpy ) ;
llext 现状 链接到标题
架构支持方面:
- 支持 armv7
- Xtensa 进行中,路还长 https://github.com/zephyrproject-rtos/zephyr/pull/62433
构建方面: 目前没有支持动态模块的构建系统:
- 需要手写 CMake/Makefile 来完成多文件的构建。
- 需要人工管理动态模块和 Zephyr Image 之间的版本匹配
- 没有灵活的符号导出机制,当动态模块使用到 Zephyr 代码的函数,需要在 Zephyr 一侧通过
EXPORT_SYMBOL
逐个导出
限制与安全:
- 动态模块 code, stack, heap 没有单独内存区, 所有的内容都在一个 heap 内 ( 使用 llext 时无法使用 MPU ),有安全新问题
- 不支援 XIP,对内存大小有要求
- 不支持 PLT, ( ARM 下使用 -mlong-calls 解决,但会导致性能下降 ) 更多的细节可以参考 https://github.com/zephyrproject-rtos/zephyr/pull/57086 https://github.com/zephyrproject-rtos/zephyr/issues/63545 https://github.com/zephyrproject-rtos/zephyr/issues/63535
可见目前 Zephyr 的动态模块支持还在非常基础的阶段,无论时功能性还是使用的便利性都不理想,因此现阶段 ( v3.5.0 ) 不建议使用 Zephyr 的动态模块。
参考 链接到标题
https://docs.zephyrproject.org/3.5.0/samples/subsys/llext/llext.html