起因是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('_')])
注意事项:
- 如果函数被静态链接或隐藏,上述方法可能看不到
- C++库需要使用
extern "C"
导出的函数才能被清晰识别 - 调试版本可能包含更多符号信息
示例工作流:
# 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);
..
针对动态链接库文件,我尝试了以下方法。
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
可读性提高了,所以采用如下改进方法。
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(); // 配套清理函数
}
总结建议
- 优先通过官方文档确认(MySQL/MariaDB文档)
- 检查对应版本的头文件
- 对于没有参数的函数:
- 初始化函数通常返回状态值
- 清理函数通常是void返回
- 谨慎调用未知函数,可能引发稳定性问题
有的函数后面跟一串数字,是什么含义?比如
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)。
- 表示函数的 Rust 模块路径:
-
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. 对比其他语言的符号修饰
语言 | 修饰示例 | 特点 |
---|---|---|
Rust | func::h1234abcd | 带模块路径和哈希 |
C++ | _Z3fooi | 参数类型编码 |
C | func | 无修饰 |
Go | pkg.Func | 带包名前缀 |
总结
- 带哈希后缀的函数是 Rust 的默认符号修饰结果,包含模块路径和唯一标识。
- 对外提供接口时,Rust 库应通过
extern "C"
或#[no_mangle]
导出无修饰符号。 - 调试时用
rustfilt
或nm --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参数)
- 更多参数通过栈传递
- 前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参数 (
rdi
):chdb_connection*
连接对象指针- 检查
0x8(%r15)
判断连接有效性 (代码地址13a1602c
)
- 检查
- 第2参数 (
rsi
):const char*
SQL查询字符串 - 第3参数 (
rdx
):const char*
输出格式字符串 (如 “CSV”) - 后三个参数 (
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. 关键代码逻辑
-
连接有效性检查:
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
-
核心调用:
13a1604a: e8 71 f4 ff ff call 13a154c0 <query_conn@@Base+0x180>
- 调用内部函数
query_conn
执行实际查询
- 调用内部函数
-
返回值:
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
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>)