Skip to main content

运行时符号调用工具(SymbolHelper)

SymbolHelper 是 Verilua 提供的一个实用工具模块,用于在运行时获取任意全局符号(函数、变量)的地址,并动态调用 C 函数。它主要应用于以下场景:

  • 调用任意 C 函数(包括 DPI-C 导出的函数、仿真器内部函数、系统库函数、自定义函数等)
  • 调试或性能测量时获取符号信息
  • 动态绑定未通过 FFI 声明的函数

该模块基于 LuaJIT 的 FFI 和 Verilua 内置的 get_symbol_address 功能实现。

如果你第一次接触 LuaJIT 的 cdata / FFI,可以先阅读LuaJIT 与标准 Lua 的常见差异

初始化

SymbolHelper 不需要显式初始化,直接 require 即可使用。

local SymbolHelper = require "verilua.utils.SymbolHelper"

函数参考

get_executable_name()

获取当前可执行文件的完整路径。

  • 返回值string 路径字符串
  • 示例
    local path = SymbolHelper.get_executable_name()
    print("Executable:", path)
    -- 可能的输出:Executable: /home/user/verilua/project/simv

get_self_cmdline()

获取启动当前进程的命令行参数(以空格分隔)。

  • 返回值string 命令行字符串
  • 示例
    local cmdline = SymbolHelper.get_self_cmdline()
    print("Command line:", cmdline)
    -- 可能的输出:Command line: ./simv +vcs+initreg+0

get_global_symbol_addr(symbol_name)

获取指定符号在可执行文件或动态库中的运行时内存地址。

  • 参数
    • symbol_name (string):符号名称(函数名、变量名等)
  • 返回值integer 符号的地址(64 位整数),若符号不存在则返回 0
  • 说明:该函数会解析当前进程的 ELF 符号表,并考虑 ASLR 偏移,返回最终可用的地址。
  • 示例
    local addr = SymbolHelper.get_global_symbol_addr("svSetScope")
    if addr ~= 0 then
    print("svSetScope address: 0x" .. bit.tohex(addr))
    end
    -- 可能的输出:svSetScope address: 0x7f8d4a2b1c00

ffi_cast(type_str, value)

将给定的值(符号名或地址)转换为指定类型的 C 函数指针或数据指针。

  • 参数
    • type_str (string):FFI 类型描述,例如 "void (*)(void *)""int (*)(int)"
    • value:可以是符号名字符串、整数地址或已存在的 cdata 对象
  • 返回值:对应类型的 cdata 函数指针或数据指针
  • 注意:如果 value 是字符串,内部会自动调用 get_global_symbol_addr 获取地址,如果地址为 0 则会触发断言错误。
  • 示例
    -- 通过符号名获取函数指针
    local svSetScope = SymbolHelper.ffi_cast("void *(*)(void *)", "svSetScope")
    local scope = svSetScope(some_scope_handle)

    -- 通过地址转换
    local addr = SymbolHelper.get_global_symbol_addr("my_function")
    local my_func = SymbolHelper.ffi_cast("int (*)(int)", addr)
    print(my_func(42))

try_ffi_cast(decl)

尝试获取函数指针,如果符号存在则通过 ffi_cast 转换,否则回退到 FFI 声明。这是调用任意 C 函数最安全的方式。

  • 参数
    • decl (string):完整的 C 函数声明,例如 "void *svSetScope(void *scope);"
  • 返回值function 可调用的 Lua 函数
  • 说明:解析器从声明中提取最后一个 ( 之前的标识符作为函数名,并把该标识符替换为 (*) 得到函数指针类型;解析失败会立即报错(不会静默回退)。先尝试在全局符号表中查找符号,找到则用 ffi_cast 转换;未找到则执行 ffi.cdef 并返回 ffi.C[func_name]
  • 示例
    local svSetScope = SymbolHelper.try_ffi_cast("void *svSetScope(void *scope);")

使用场景示例

1. 调用任意 C 函数(包括仿真器内部函数和自定义函数)

SymbolHelper 可以获取并调用任何全局可见的 C 函数,无论是仿真器提供的内部函数(如 svSetScope)、DPIC 导出的函数,还是您自己编写的 C 函数。

示例:调用仿真器内部函数 svSetScopesvGetScopeFromName

local SymbolHelper = require "verilua.utils.SymbolHelper"

-- 极简形式:只传完整声明,函数名和指针类型由解析器推导
local svSetScope = SymbolHelper.try_ffi_cast("void *svSetScope(void *scope);")
local svGetScopeFromName = SymbolHelper.try_ffi_cast("void *svGetScopeFromName(const char *name);")

-- 使用
local scope = svGetScopeFromName("tb_top")
svSetScope(scope)

示例:调用自定义 C 函数

假设您在 dpic.cpp 中定义了一个函数:

#include <iostream>
extern "C" void hello_from_c(const char* name) {
std::cout << "Hello, " << name << " from C!" << std::endl;
}

编译后,在 Lua 中通过 SymbolHelper 调用它:

local SymbolHelper = require "verilua.utils.SymbolHelper"

-- 方式1:直接 ffi_cast(符号必须存在)
local hello = SymbolHelper.ffi_cast("void (*)(const char*)", "hello_from_c")
hello("Verilua")

-- 方式2:使用 try_ffi_cast 更安全(极简形式:只传完整声明)
local hello_safe = SymbolHelper.try_ffi_cast("void hello_from_c(const char* name);")
hello_safe("World")

2. 获取当前可执行文件路径

local exe = SymbolHelper.get_executable_name()
print("Running from:", exe)
-- 可能的输出:Running from: /home/user/verilua/project/simv

3. 调试时打印函数地址

local addr = SymbolHelper.get_global_symbol_addr("vpi_control")
if addr ~= 0 then
print("vpi_control @ 0x" .. bit.tohex(addr))
end
-- 可能的输出:vpi_control @ 0x7f8d4a2b1c00

符号查找范围与 ffi.load 的关系

SymbolHelper 内部有两条符号查找路径,适用范围不同:

查找路径实现方式能找到的符号
get_global_symbol_addr解析主可执行文件的 ELF .symtab仅限链接进仿真器二进制的符号(DPI-C 函数、静态链接的库等)
ffi.C[name]try_ffi_cast 的 fallback)dlsym(RTLD_DEFAULT, ...)主程序 + 所有以 RTLD_GLOBAL 加载的共享库

ffi.load 加载的 .so 中的符号

ffi.load(path) 默认使用 RTLD_LOCAL,加载的符号SymbolHelper 完全不可见

local ffi = require "ffi"
ffi.cdef [[ int32_t my_func(int32_t x); ]]

local lib = ffi.load("/path/to/libfoo.so") -- RTLD_LOCAL

-- ❌ get_global_symbol_addr 返回 0(只看主 ELF)
SymbolHelper.get_global_symbol_addr("my_func") --> 0

-- ❌ try_ffi_cast 也失败(ffi.C fallback 找不到 RTLD_LOCAL 符号)
SymbolHelper.try_ffi_cast("int32_t my_func(int32_t x);") --> error

如果需要让 try_ffi_cast 的 fallback 路径能找到 .so 中的符号,使用 ffi.load(path, true) 加载——第二个参数为 true 表示以 RTLD_GLOBAL 模式打开,使库中所有导出符号对 dlsym(RTLD_DEFAULT, ...) 可见:

local lib = ffi.load("/path/to/libfoo.so", true) -- RTLD_GLOBAL

-- ❌ get_global_symbol_addr 仍然返回 0(它只解析主 ELF,不用 dlsym)
SymbolHelper.get_global_symbol_addr("my_func") --> 0

-- ✅ try_ffi_cast 通过 ffi.C fallback 成功(dlsym 能找到 RTLD_GLOBAL 符号)
local fn = SymbolHelper.try_ffi_cast("int32_t my_func(int32_t x);")
fn(42) --> OK

总结:如何调用外部 .so 中的 C 函数

方式是否需要 SymbolHelper说明
ffi.load(path) 返回的 handle 直接调用最简单,需要 ffi.cdef 声明函数签名
ffi.load(path, true) + try_ffi_cast符号全局可见,可通过 ffi.Ctry_ffi_cast 调用
.so 链接进仿真器(add_files-LDFLAGS符号在主 ELF 中,get_global_symbol_addrffi_cast 均可用
何时用 SymbolHelper,何时用 ffi.load
  • 如果函数已经链接进仿真器(DPI-C、VPI 扩展、仿真器内部函数),用 SymbolHelper
  • 如果函数在一个独立的 .so 中且不想修改构建流程,直接用 ffi.load + handle 调用即可,不需要 SymbolHelper

注意事项

  • 符号可见性:某些符号可能被编译器优化掉(例如未使用的静态函数),或者被链接器隐藏。为确保符号可被找到,编译时需要保留符号(如使用 -rdynamic-Wl,--export-dynamic)。在 Verilator 下,如果 DPI-C 函数未被 SV 代码引用,需要通过 -LDFLAGS "-u symbol_name" 强制保留。
  • get_global_symbol_addr 的局限:该函数只解析主可执行文件的 ELF .symtab 段,不会搜索动态加载的共享库。如果主程序被 strip 过,.symtab 可能不存在,此时所有符号都会返回 0。
  • ffi.cdefSymbolHelper 的关系:通过 ffi.load handle 调用 C 函数时,必须先用 ffi.cdef 声明函数签名,否则访问会报错。而 SymbolHelper.ffi_cast 不需要 ffi.cdef——它通过原始地址 + 内联类型字符串完成转换。try_ffi_cast 的 fallback 路径会自动调用 ffi.cdef,用户无需手动声明。
  • 性能get_global_symbol_addr 首次调用时会解析 ELF 文件并缓存结果,后续调用极快。
  • 线程安全:该模块内部使用互斥锁保护缓存,可在多线程环境中安全使用(但 Verilua 仿真通常是单线程)。
  • 错误处理ffi_cast 在符号不存在时会触发错误,建议在不确定时使用 try_ffi_cast