借助DeepSeek查看动态链接库的导出函数

起因是chdb.h只公布了很少的导出函数,而libchdb.so有500多MB,好奇能否挖掘出未公开的函数来实现自定义功能。

于是问DeepSeek:

如何知道动态链接库so提供了哪些函数可供调用?

回答如下:

要查看动态链接库(.so 文件)提供的函数,可以使用以下方法:

1. 使用 nm 命令(最简单)

nm -D libchdb.so
  • 查看动态符号表(导出函数会显示为 T 类型)
  • 示例输出:
    0000000000012345 T chdb_connect
    0000000000016789 T chdb_query
                    U printf
    
    T 表示库提供的函数,U 表示库依赖的函数)

2. 使用 objdump(更详细)

objdump -T libchdb.so
  • 显示动态符号表(导出函数)
  • 可以清晰看到函数名和对应的内存地址

3. 使用 readelf(ELF文件专用)

readelf -s --wide libchdb.so | grep FUNC
  • 过滤只显示函数符号
  • 适合查看复杂的动态库

4. 结合 c++filt 解析C++符号(如果是C++库)

nm -D libchdb.so | c++filt
  • 将混淆后的C++函数名(如 _Z5funcv)转换为可读形式(如 func()

5. 查看头文件(最直接)

grep -r "CHDB_EXPORT" include/  # 假设导出宏是CHDB_EXPORT
  • 直接查看库提供的头文件中的函数声明

6. 交互式查看(Python方式)

import ctypes
lib = ctypes.CDLL("./libchdb.so")
print([name for name in dir(lib) if not name.startswith('_')])

注意事项:

  1. 如果函数被静态链接或隐藏,上述方法可能看不到
  2. C++库需要使用 extern "C" 导出的函数才能被清晰识别
  3. 调试版本可能包含更多符号信息

示例工作流:

# 1. 查看导出函数
nm -D libchdb.so | grep " T "

# 2. 结合头文件确认参数
cat chdb.h | grep -A10 "chdb_connect"

# 3. 验证函数签名
c++filt _Z15chdb_connect_v2iPPc

首先查看头文件,这其实与查看动态链接库无关,只为了显示作者公开的导出函数的数量,cat chdb.h|grep CHDB_EXPORT
输出了29行,包括一行宏定义,实际是28个函数

#define CHDB_EXPORT __attribute__((visibility("default")))
CHDB_EXPORT struct local_result * query_stable(int argc, char ** argv);
CHDB_EXPORT void free_result(struct local_result * result);
..

针对动态链接库文件,我尝试了以下方法。

  1. nm -D libchdb.so | grep " T " >libch.h
    输出了123817行,但有许多行不是正常的函数名,比如_ZN10num_bigint7biguint5shift12biguint_shl217h79dc2a6c64f30890E
    利用c++filt工具解码,c++filt _ZN10num_bigint7biguint5shift12biguint_shl217h79dc2a6c64f30890E,得到
num_bigint::biguint::shift::biguint_shl2::h79dc2a6c64f30890

可读性提高了,所以采用如下改进方法。

  1. nm -D libchdb.so | c++filt | grep " T " >libch2.h
    输出行数不变,所有行都是可读的,查出来的函数大致分为以下几类:
    1)没有前后缀和参数表,如mysql_thread_init

2)有前后缀,没有参数表,如num_bigint::biguint::shift::biguint_shl2::h79dc2a6c64f30890

3)有前缀和参数表,如double_conversion::BignumDtoa(double, double_conversion::BignumDtoaMode, int, double_conversion::Vector<char>, int*, int*)

最后一种容易理解,就针对前两种情况分别询问DeepSeek:

有的函数没有参数列表,怎么解决?比如mysql_thread_init

回答如下:

对于动态链接库中导出的 mysql_thread_init 这种没有显示参数列表的函数,可以通过以下方法解决:

1. 优先查找官方文档

这是最可靠的方式。MySQL官方文档明确说明该函数的原型:

my_bool mysql_thread_init(void);

2. 分析函数名和上下文

  • mysql_开头的函数通常属于MySQL C API
  • thread_init表明是线程初始化相关函数
  • 无参数常见于初始化/清理函数

3. 使用动态链接库分析工具

nm -D libmysqlclient.so | grep mysql_thread_init
objdump -T libmysqlclient.so | grep mysql_thread_init

4. 通过头文件确认

MySQL头文件中会有明确定义:

grep -r "mysql_thread_init" /usr/include/mysql/

5. 实验性调用(风险较高)

对于没有文档的函数:

typedef int (*func_ptr)(); // 无参数函数指针
void* handle = dlopen("libmysqlclient.so", RTLD_LAZY);
func_ptr f = (func_ptr)dlsym(handle, "mysql_thread_init");
f(); // 谨慎调用

6. 反汇编分析(高级)

使用objdump反汇编:

objdump -d libmysqlclient.so --disassemble=mysql_thread_init

针对您具体问题的解决方案

对于mysql_thread_init函数,标准用法是:

#include <mysql.h>

int main() {
    if (!mysql_thread_init()) {
        // 线程初始化成功
    }
    mysql_thread_end(); // 配套清理函数
}

总结建议

  1. 优先通过官方文档确认(MySQL/MariaDB文档)
  2. 检查对应版本的头文件
  3. 对于没有参数的函数
    • 初始化函数通常返回状态值
    • 清理函数通常是void返回
  4. 谨慎调用未知函数,可能引发稳定性问题

有的函数后面跟一串数字,是什么含义?比如
000000001fe930b0 T num_bigint::biguint::shift::biguint_shl2::h79dc2a6c64f30890

回答如下:

这种带有长哈希后缀的函数名是 Rust 编译器(rustc)生成的符号名称,属于名称修饰(Name Mangling)的结果。具体解释如下:


1. 结构组成分析

num_bigint::biguint::shift::biguint_shl2::h79dc2a6c64f30890 为例:

  • num_bigint::biguint::shift::biguint_shl2

    • 表示函数的 Rust 模块路径:
      num_bigint(crate名)→ biguint(模块名)→ shift(子模块名)→ biguint_shl2(函数名)。
    • 这是一个大整数(biguint)的左移操作函数(shl = shift left)。
  • h79dc2a6c64f30890

    • 编译器生成的唯一哈希值,用于区分同名函数的不同版本(如泛型特化或编译优化后的变体)。

2. 为什么 Rust 会这样命名?

  • 避免符号冲突
    Rust 支持泛型和模块化编程,同一函数可能有多个特化版本(如 Vec<i32>Vec<u8> 会生成不同符号)。
  • 包含完整路径信息
    确保即使不同 crate 有同名模块/函数,也能通过完整路径区分。
  • 哈希值的作用
    基于函数签名和上下文生成唯一标识,防止链接时冲突。

3. 如何解读这类函数?

方法一:使用 rustfilt 工具反修饰(推荐)
# 安装 rustfilt
cargo install rustfilt

# 解析符号名
echo "num_bigint::biguint::shift::biguint_shl2::h79dc2a6c64f30890" | rustfilt

输出结果:

num_bigint::biguint::shift::biguint_shl2
方法二:手动分析
  • 忽略 ::h... 后缀,关注前面的模块路径和函数名。
  • 例如:
    biguint_shl2 → 大整数左移操作(shl = shift left, 2 可能指操作位数)。

4. 动态库中如何处理这类函数?

  • 通过头文件或文档调用
    Rust 库通常会提供 extern "C" 包装函数(无修饰符号)供外部调用。
  • 直接调用需用 #[no_mangle]
    如果库未提供 C 接口,需在 Rust 中声明:
    #[no_mangle]
    pub extern "C" fn my_function() { ... }
    
    这样生成的符号会是简单的 my_function

5. 对比其他语言的符号修饰

语言修饰示例特点
Rustfunc::h1234abcd带模块路径和哈希
C++_Z3fooi参数类型编码
Cfunc无修饰
Gopkg.Func带包名前缀

总结

  • 带哈希后缀的函数是 Rust 的默认符号修饰结果,包含模块路径和唯一标识。
  • 对外提供接口时,Rust 库应通过 extern "C"#[no_mangle] 导出无修饰符号。
  • 调试时rustfiltnm --demangle 可读化符号名。

我用命令行分别对函数反汇编,得到汇编语言源代码文件

objdump -d libchdb.so --disassemble=mysql_thread_init >mysql_thread_init.asm
objdump -d libchdb.so --disassemble=chdb_query >chdb_query.asm

前者非常简单


libchdb.so:     file format elf64-x86-64


Disassembly of section .init:

Disassembly of section .fini:

Disassembly of section .text:

000000001d91b080 <mysql_thread_init@@Base>:
    1d91b080:	31 c0                	xor    %eax,%eax
    1d91b082:	c3                   	ret

Disassembly of section malloc_hook:

Disassembly of section .plt:

后者则有110行,我把文件作为附件交给DeepSeek解读,结果如下

从反汇编代码分析 chdb_query 函数的参数和调用约定:


1. 函数调用约定

  • System V AMD64 ABI (x86-64 Linux标准约定):
    • 前6个参数通过寄存器传递:
      • rdi (第1参数)
      • rsi (第2参数)
      • rdx (第3参数)
      • rcx (第4参数)
      • r8 (第5参数)
      • r9 (第6参数)
    • 更多参数通过栈传递
  • 返回值保存在 rax 寄存器

2. 参数解析

从反汇编代码的关键片段:

13a16009: 48 89 d3              mov    %rdx,%rbx      ; 保存第3参数到 rbx
13a1600c: 49 89 f6              mov    %rsi,%r14      ; 保存第2参数到 r14
13a1600f: 49 89 ff              mov    %rdi,%r15      ; 保存第1参数到 r15
13a1603c: 4c 89 f6              mov    %r14,%rsi      ; 恢复第2参数到 rsi
13a1603f: 48 89 da              mov    %rbx,%rdx      ; 恢复第3参数到 rdx
13a16042: 31 c9                 xor    %ecx,%ecx      ; 清空 rcx (第4参数=0)
13a16044: 45 31 c0              xor    %r8d,%r8d      ; 清空 r8 (第5参数=0)
13a16047: 45 31 c9              xor    %r9d,%r9d      ; 清空 r9 (第6参数=0)

可推断参数列表:

  1. 第1参数 (rdi)chdb_connection* 连接对象指针
    • 检查 0x8(%r15) 判断连接有效性 (代码地址 13a1602c)
  2. 第2参数 (rsi)const char* SQL查询字符串
  3. 第3参数 (rdx)const char* 输出格式字符串 (如 “CSV”)
  4. 后三个参数 (rcx, r8, r9) 固定为0 (可能是保留参数)

3. 对应C函数原型

// 重建的函数声明
chdb_result* chdb_query(
    chdb_connection* conn,    // rdi
    const char* query,        // rsi
    const char* format,       // rdx
    int unused1 = 0,          // rcx
    int unused2 = 0,          // r8
    int unused3 = 0           // r9
);

4. 关键代码逻辑

  1. 连接有效性检查

    13a16027: 4d 85 ff              test   %r15,%r15     ; 检查conn是否为NULL
    13a1602a: 74 71                 je     13a1609d      ; 如果为NULL跳转到错误处理
    13a1602c: 41 80 7f 08 01        cmpb   $0x1,0x8(%r15); 检查conn->is_valid
    
  2. 核心调用

    13a1604a: e8 71 f4 ff ff        call   13a154c0 <query_conn@@Base+0x180>
    
    • 调用内部函数 query_conn 执行实际查询
  3. 返回值

    13a1604f: 48 89 c3              mov    %rax,%rbx     ; 保存结果指针
    13a16111: 48 89 d8              mov    %rbx,%rax     ; 返回结果
    

5. 错误处理

  • 如果连接无效,会分配一个错误结果对象:
    13a1609d: bf 58 00 00 00        mov    $0x58,%edi    ; 分配88字节内存
    13a160a2: e8 e9 ac fb 0d        call   219d0d90 <_Znwm@plt> ; 调用operator new
    

总结

  • 参数:3个有效参数 + 3个保留参数(总6个寄存器参数)
  • 返回值chdb_result* 结构指针
  • 行为:验证连接后执行查询,返回结果或错误对象

此分析与头文件 chdb.h 中的声明一致:

CHDB_EXPORT chdb_result* chdb_query(chdb_connection conn, const char* query, const char* format);

我还尝试对clickhouse可执行文件执行 nm -D clickhouse | c++filt | grep " T " >libch3.h,导出了大约100个函数,除了main函数,还有__libc_malloc、dlopen、exp等函数。
而对我自行编译的chsql可执行文件,执行nm -D chsqlmo | c++filt | grep " T " >chsqlmo.h
则只有一行:
0000000000402510 T main

  1. readelf -s --wide libchdb.so | grep FUNC >libch4.h
    这个命令速度比较慢,我等不及中断了它的执行,得到的libch4.h有397600行,约85MB。比起前面nm命令得到的函数,多了库函数。如下所示:
   560: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strtof_l@GLIBC_2.3 (5)
   ...
   581: 000000000f1d4960   470 FUNC    WEAK   DEFAULT   16 _ZN2DB17NumComparisonImplI8BFloat16dNS_8EqualsOpIS1_dEEE22vectorConstantImplAVX2ERKNS_8PODArrayIS1_Lm4096E9AllocatorILb0ELb0EELm63ELm64EEEdRNS5_IDuLm4096ES7_Lm63ELm64EEE

同样有很多混淆函数名,需要c++filt才能阅读。
4. objdump -T libchdb.so >odmp.h
这个命令速度较快,但加上调用c++filt工具objdump -T libchdb.so | c++filt > odmp2.h就慢了一些,仍比readelf快。
得到的结果文件有485332行,不带c++filt有100MB,c++filt后有170MB。它的内容详细,也有库函数,有函数返回值类型,但有一个缺点,函数名不是按字母顺序排列的,很难找。比如

0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.2.5) strlen
...
0000000022848080 g    DO .data	0000000000000008  Base        DB::ObjectStorageQueueSetting::output_format_msgpack_uuid_representation
000000001e9c38a0 g    DF .text	0000000000000020  Base        crc::crc64::nolookup::<impl crc::Digest<u64,crc::Table<0_usize>>>::update::h16fd5c3c6b993029
0000000011a677e0  w   DF .text	0000000000000081  Base        void DB::DecimalComparison<DB::Decimal<wide::integer<256ul, int> >, char8_t, DB::NotEqualsOp>::vectorVector<true, true, false>(DB::PODArray<DB::Decimal<wide::integer<256ul, int> >, 4096ul, Allocator<false, false>, 63ul, 64ul> const&, DB::PODArray<char8_t, 4096ul, Allocator<false, false>, 63ul, 64ul> const&, DB::PODArray<char8_t, 4096ul, Allocator<false, false>, 63ul, 64ul>&, wide::integer<256ul, int>)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值