文章

嵌入式动态库

嵌入式动态库

简介

在 Linux 系统中,动态库(.so)依靠 ELF 动态链接器完成符号解析与运行时绑定,实现了模块复用、升级与内存共享。

而在嵌入式系统中,由于缺乏动态链接器、内存受限或执行环境受控,无法使用完整的 ELF 动态加载机制。但嵌入式固件仍常常需要一种“可复用、可升级、可替换”的通用功能模块。

本文实现了一种适合嵌入式项目使用的动态库的实现,采用固定地址入口、跳转表(function table)等方式,构建一个“简化版动态库接口”。这种机制本质上是将共享库的概念提前到链接期或固件布局阶段:

  • 公共模块放在 Flash 固定区域
  • 导出函数入口地址作为 ABI
  • 应用通过跳板或函数指针表调用
  • 允许升级公共模块而不改动应用

分区

以 512k Flash 和 64k RAM 为例,包含 bootloader

如果不使用独立的动态库,将库的内容直接链接到程序中,布局会这样:

alt text

如果使用了动态库,bootloader 和 app 可以共享库:

alt text

为了让应用之间,以及库和应用之间能进行消息通信,可以划定一片共享内存区域。

alt text

链接脚本中的分区布局如下:

bootloader:

MEMORY
{
    FLASH(rx) : ORIGIN = 0x00000000, LENGTH = 16k
    SHARED_RAM(rwx) : ORIGIN = 0x10000000, LENGTH = 1K
    RAM(rwx) : ORIGIN = 0x10000400, LENGTH = 64k - 1k
}

动态库:

MEMORY
{
    FLASH(rx) : ORIGIN = 0x00004000, LENGTH = 32k
    SHARED_RAM(rwx) : ORIGIN = 0x10000000, LENGTH = 1K
    RAM(rwx) : ORIGIN = 0x10000400, LENGTH = 64k - 1k
}

主应用:

MEMORY
{
    FLASH(rx) : ORIGIN = 0x0000C000, LENGTH = 512k - 16k - 32k
    SHARED_RAM(rwx) : ORIGIN = 0x10000000, LENGTH = 1K
    RAM(rwx) : ORIGIN = 0x10000400, LENGTH = 64k - 1k
}

动态库的实现

动态库需要将其接口(API)放置在固定位置,以便其他的程序可以找到该接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// sharedlib.h
#pragma once
typedef struct {
    void (*init)(void);
    void (*entry)(void);
} ShareLibAPITable;

#define SharedLibAPIAddr 0x4000
#ifdef SharedLibAPIAddr // api table固定放在0x4000位置,并用宏封装
    #define ShareLibAPI ((const ShareLibAPITable*)SharedLibAPIAddr)
#else // 也允许不通过动态库的方式直接调用,让其直接变为调用者程序的一部分
    extern const ShareLibAPI shareLibAPITable;
    #define ShareLibAPI ((const ShareLibAPITable*)&shareLibAPITable)
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// sharedlib.c
#include "sharedlib.h"

void shareLibInit()
{
    // ...
}

void sharedLibEntry()
{
    // ...
}

// 指定该 api table 的 section 为 shared_lib_api
__attribute__((section(.shared_lib_api),used))
const ShareLibAPITable shareLibAPITable = {
    .init = shareLibInit,
    .entry = sharedLibEntry,
}

不作为动态库时,sharedlib.c 可以直接链接到主程序中。

动态库链接脚本:

SECTION
{
    .shared_lib_api 0x4000 :
    {
        /* 名字为 shared_lib_api 段放到0x4000位置 */
        KEEP(*(.shared_lib_api))
    } > FLASH

    /* 其他代码段放到FLASH的后续位置 */
    .text :
    {
        *(.text*)
        *(.rodata*)
    } > FLASH

    /* 如果动态库 */
    .data :
    {
        *(.data*)
    } > FLASH
    .bss :
    {
        *(.bss*)
    } > FLASH
}

动态库的使用

动态库的使用很简单,只要引入相应的头文件,该头文件包含了 API table(function 跳转表) 的入口,并已经被封装成了结构体指针,直接使用即可:

1
2
3
4
5
6
7
// app.cpp
#include "sharedlib.h"

int main()
{
    ShareLibAPI->init();
}

共享内存

由于动态库和其他程序是分开链接的,导致它们之间的全局变量是无法共享的,即使是同名的变量,其地址也是不同的,所有需要提供共享内存,让它们能访问到相同的变量。

// sharedram.hpp
#pragma once

__attribute__((section(".shared_ram"), used))
inline struct {
    unsigned char sharedAttr1[4];
    unsigned char sharedAttr2[8];
} sharedRam;

在所有链接脚本中加入该 .shared_ram

SECTION
{
    /* ... */
    .shared_ram {
        . = ALIGN(4)
        KEEP(*(.shared_ram))
    } > SHARED_RAM
    /* ... */
}

这样就能保证所有程序都能访问到这片位置固定的共享内存:

1
2
3
4
5
6
7
8
// app.cpp
#include "sharedram.hpp"

int main()
{
    auto& attr1 = reinterpret_cast<long&>(sharedRam.sharedAttr1);
    attr1 = 2;
}
1
2
3
4
5
6
7
8
// sharedlib.c
#include "sharedram.hpp"

void shareLibInit()
{
    auto& attr1 = reinterpret_cast<long&>(sharedRam.sharedAttr1);
    int x = attr1;
}

参考

本文由作者按照 CC BY-SA 4.0 进行授权